Feature Extraction
Transformers
Safetensors
esmfold2
biology
protein-structure
multimodal-protein-model
custom_code
Instructions to use Synthyra/ESMFold2-Experimental with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/ESMFold2-Experimental with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="Synthyra/ESMFold2-Experimental", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Synthyra/ESMFold2-Experimental", trust_remote_code=True, dtype="auto") - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- LICENSE +9 -9
- README.md +234 -109
- __init__.py +4 -5
- configuration_esmfold2.py +313 -283
- esmfold2_affine3d.py +560 -560
- esmfold2_aligner.py +101 -101
- esmfold2_atom_indexer.py +15 -15
- esmfold2_conformers.py +291 -291
- esmfold2_constants.py +562 -562
- esmfold2_constants_esm3.py +137 -137
- esmfold2_input_builder.py +254 -254
- esmfold2_metrics.py +373 -373
- esmfold2_misc.py +504 -504
- esmfold2_mmcif_parsing.py +469 -469
- esmfold2_molecular_complex.py +0 -0
- esmfold2_msa.py +506 -506
- esmfold2_msa_filter_sequences.py +82 -82
- esmfold2_normalize_coordinates.py +79 -79
- esmfold2_output.py +224 -224
- esmfold2_paired_msa.py +245 -245
- esmfold2_parsing.py +112 -112
- esmfold2_predicted_aligned_error.py +104 -104
- esmfold2_prepare_input.py +0 -0
- esmfold2_processor.py +355 -355
- esmfold2_protein_chain.py +0 -0
- esmfold2_protein_complex.py +0 -0
- esmfold2_protein_structure.py +306 -306
- esmfold2_residue_constants.py +1223 -1223
- esmfold2_sequential_dataclass.py +157 -157
- esmfold2_system.py +45 -45
- esmfold2_types.py +33 -33
- esmfold2_utils_types.py +33 -33
- modeling_esmfold2.py +0 -0
- modeling_esmfold2_common.py +0 -0
- modeling_esmfold2_experimental.py +998 -994
- protein_utils.py +488 -488
LICENSE
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
**License (MIT)**
|
| 2 |
-
|
| 3 |
-
Copyright 2026 Chan Zuckerberg Biohub, Inc.
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
| 6 |
-
|
| 7 |
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
| 8 |
-
|
| 9 |
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
| 1 |
+
**License (MIT)**
|
| 2 |
+
|
| 3 |
+
Copyright 2026 Chan Zuckerberg Biohub, Inc.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
| 6 |
+
|
| 7 |
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
| 8 |
+
|
| 9 |
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
README.md
CHANGED
|
@@ -1,122 +1,247 @@
|
|
| 1 |
-
---
|
| 2 |
-
library_name: transformers
|
| 3 |
-
tags:
|
| 4 |
-
- biology
|
| 5 |
-
- protein-structure
|
| 6 |
-
- esmfold2
|
| 7 |
-
- multimodal-protein-model
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# FastPLMs ESMFold2
|
| 11 |
-
|
| 12 |
-
FastPLMs ESMFold2 is a self-contained Hugging Face `AutoModel` wrapper for
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
```python
|
| 17 |
-
import torch
|
| 18 |
-
from transformers import AutoModel
|
| 19 |
-
|
| 20 |
model = AutoModel.from_pretrained(
|
| 21 |
"Synthyra/ESMFold2-Fast",
|
| 22 |
-
trust_remote_code=True,
|
| 23 |
-
|
| 24 |
-
).
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
Use `Synthyra/ESMFold2` for the full model and `Synthyra/ESMFold2-Fast` for the faster release variant.
|
| 28 |
-
The folding trunk runs in fp32; the 6B ESMC backbone is loaded in bf16 by default via `esmc_precision="bf16"`.
|
| 29 |
-
|
| 30 |
-
## Fold One Protein
|
| 31 |
-
|
| 32 |
-
```python
|
| 33 |
-
sequence = "MKTLLILAVVAAALA"
|
| 34 |
-
|
| 35 |
-
result = model.fold_protein(
|
| 36 |
-
sequence,
|
| 37 |
-
num_loops=3,
|
| 38 |
-
num_sampling_steps=50,
|
| 39 |
-
num_diffusion_samples=1,
|
| 40 |
-
seed=0,
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
print(float(result.plddt.mean()))
|
| 44 |
-
print(float(result.ptm))
|
| 45 |
-
```
|
| 46 |
-
|
| 47 |
-
## Save mmCIF or PDB
|
| 48 |
-
|
| 49 |
-
```python
|
| 50 |
-
model.save_as_cif(result, "prediction.cif")
|
| 51 |
-
model.save_as_pdb(result, "prediction.pdb")
|
| 52 |
-
|
| 53 |
-
cif_text = model.result_to_cif(result)
|
| 54 |
-
pdb_text = model.result_to_pdb(result)
|
| 55 |
-
```
|
| 56 |
-
|
| 57 |
-
`result_to_cif` preserves the full `MolecularComplex`. `result_to_pdb` converts through Biohub's protein-only `ProteinComplex` representation, so use mmCIF for complexes with ligands or nucleic acids.
|
| 58 |
-
|
| 59 |
-
## Fold Complexes
|
| 60 |
-
|
| 61 |
-
```python
|
| 62 |
-
types = model.input_types
|
| 63 |
-
|
| 64 |
-
complex_input = types.StructurePredictionInput(
|
| 65 |
-
sequences=[
|
| 66 |
-
types.ProteinInput(id="A", sequence="MKTLLILAVVAAALA"),
|
| 67 |
-
types.DNAInput(id="B", sequence="GATAGC"),
|
| 68 |
-
types.LigandInput(id="L", ccd=["SAH"]),
|
| 69 |
-
]
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
result = model.fold(
|
| 73 |
-
complex_input,
|
| 74 |
-
num_loops=3,
|
| 75 |
-
num_sampling_steps=50,
|
| 76 |
-
num_diffusion_samples=1,
|
| 77 |
-
seed=0,
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
model.save_as_cif(result, "complex_prediction.cif")
|
| 81 |
-
```
|
| 82 |
-
|
| 83 |
-
## Use MSAs
|
| 84 |
-
|
| 85 |
-
```python
|
| 86 |
-
types = model.input_types
|
| 87 |
-
|
| 88 |
-
msa = types.MSA.from_a3m("query.a3m", max_sequences=128)
|
| 89 |
-
input_with_msa = types.StructurePredictionInput(
|
| 90 |
-
sequences=[
|
| 91 |
-
types.ProteinInput(id="A", sequence=msa.query, msa=msa),
|
| 92 |
-
]
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
result = model.fold(input_with_msa, num_sampling_steps=50, seed=0)
|
| 96 |
-
```
|
| 97 |
-
|
| 98 |
-
## Raw Tensor Inference
|
| 99 |
-
|
| 100 |
-
```python
|
| 101 |
-
features, chain_infos = model.prepare_structure_input(complex_input, seed=0)
|
| 102 |
-
|
| 103 |
-
with torch.inference_mode():
|
| 104 |
-
output = model(
|
| 105 |
-
**features,
|
| 106 |
-
num_loops=3,
|
| 107 |
-
num_sampling_steps=50,
|
| 108 |
-
num_diffusion_samples=1,
|
| 109 |
-
)
|
| 110 |
-
|
| 111 |
-
decoded = model.input_builder.decode(output, features, chain_infos)
|
| 112 |
```
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
|
| 116 |
```python
|
| 117 |
model = AutoModel.from_pretrained(
|
| 118 |
"Synthyra/ESMFold2-Fast",
|
| 119 |
trust_remote_code=True,
|
| 120 |
-
|
| 121 |
).cuda().eval()
|
| 122 |
```
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
tags:
|
| 4 |
+
- biology
|
| 5 |
+
- protein-structure
|
| 6 |
+
- esmfold2
|
| 7 |
+
- multimodal-protein-model
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# FastPLMs ESMFold2
|
| 11 |
+
|
| 12 |
+
FastPLMs ESMFold2 is a self-contained Hugging Face `AutoModel` wrapper for
|
| 13 |
+
Biohub's ESMFold2, ESMFold2-Fast, and experimental ESMFold2 structure
|
| 14 |
+
predictors. It vendors the released Biohub ESMFold2 model code, input builder,
|
| 15 |
+
MSA helpers, and structure export utilities, while loading the PLM backbone
|
| 16 |
+
through FastPLMs ESM++.
|
| 17 |
+
|
| 18 |
+
## Load With AutoModel
|
| 19 |
+
|
| 20 |
+
```python
|
| 21 |
+
import torch
|
| 22 |
+
from transformers import AutoModel
|
| 23 |
+
|
| 24 |
+
model = AutoModel.from_pretrained(
|
| 25 |
+
"Synthyra/ESMFold2-Fast",
|
| 26 |
+
trust_remote_code=True,
|
| 27 |
+
dtype=torch.float32,
|
| 28 |
+
).eval().cuda()
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Use `Synthyra/ESMFold2` for the full model, `Synthyra/ESMFold2-Fast` for the
|
| 32 |
+
faster release variant, and the `Synthyra/ESMFold2-Experimental*` checkpoints
|
| 33 |
+
for differentiable binder design and experimental critic ensembles.
|
| 34 |
+
The folding trunk runs in fp32; the 6B FastPLMs ESM++ backbone is loaded in
|
| 35 |
+
bf16 by default via `esmc_precision="bf16"` and uses the flex attention backend
|
| 36 |
+
by default inside ESMFold2.
|
| 37 |
+
|
| 38 |
+
## Fold One Protein
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
sequence = "MKTLLILAVVAAALA"
|
| 42 |
+
|
| 43 |
+
result = model.fold_protein(
|
| 44 |
+
sequence,
|
| 45 |
+
num_loops=3,
|
| 46 |
+
num_sampling_steps=50,
|
| 47 |
+
num_diffusion_samples=1,
|
| 48 |
+
seed=0,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
print(float(result.plddt.mean()))
|
| 52 |
+
print(float(result.ptm))
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Experimental Test-Time Training
|
| 56 |
+
|
| 57 |
+
TTT is disabled by default. Standard `fold_protein(...)`, `fold(...)`, raw tensor
|
| 58 |
+
inference, and `state_dict()` keys are unchanged unless you explicitly pass
|
| 59 |
+
`ttt=True` or call `fold_protein_ttt(...)`.
|
| 60 |
+
|
| 61 |
+
The ESMFold2 TTT path is experimental and protein-only in v1. It trains local
|
| 62 |
+
LoRA adapters only on `_esmc` with a masked language modeling objective. The
|
| 63 |
+
folding trunk, confidence head, diffusion head, and structure input pipeline are
|
| 64 |
+
frozen. TTT can improve difficult low-confidence folds, but it adds substantial
|
| 65 |
+
test-time compute and can degrade already confident predictions.
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
result = model.fold_protein(
|
| 69 |
+
"MSTNPKPQRKTKRNT",
|
| 70 |
+
num_loops=1,
|
| 71 |
+
num_sampling_steps=10,
|
| 72 |
+
num_diffusion_samples=1,
|
| 73 |
+
seed=0,
|
| 74 |
+
ttt=True,
|
| 75 |
+
ttt_config={
|
| 76 |
+
"steps": 1,
|
| 77 |
+
"ags": 1,
|
| 78 |
+
"batch_size": 1,
|
| 79 |
+
"lora_rank": 8,
|
| 80 |
+
"lora_alpha": 32.0,
|
| 81 |
+
},
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
print(result.ttt_metrics["losses"])
|
| 85 |
+
print(result.ttt_metrics["step_plddts"])
|
| 86 |
+
print(result.ttt_metrics["best_step"])
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
`load_esmc=True` is required for TTT because the ESM++ MLM head is loaded lazily
|
| 90 |
+
from `config.esmc_id`. If that pretrained MLM head cannot be loaded, TTT raises
|
| 91 |
+
an assertion instead of silently using a random head.
|
| 92 |
+
|
| 93 |
+
## Save mmCIF or PDB
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
model.save_as_cif(result, "prediction.cif")
|
| 97 |
+
model.save_as_pdb(result, "prediction.pdb")
|
| 98 |
+
|
| 99 |
+
cif_text = model.result_to_cif(result)
|
| 100 |
+
pdb_text = model.result_to_pdb(result)
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
`result_to_cif` preserves the full `MolecularComplex`. `result_to_pdb` converts through Biohub's protein-only `ProteinComplex` representation, so use mmCIF for complexes with ligands or nucleic acids.
|
| 104 |
+
|
| 105 |
+
## Fold Complexes
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
types = model.input_types
|
| 109 |
+
|
| 110 |
+
complex_input = types.StructurePredictionInput(
|
| 111 |
+
sequences=[
|
| 112 |
+
types.ProteinInput(id="A", sequence="MKTLLILAVVAAALA"),
|
| 113 |
+
types.DNAInput(id="B", sequence="GATAGC"),
|
| 114 |
+
types.LigandInput(id="L", ccd=["SAH"]),
|
| 115 |
+
]
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
result = model.fold(
|
| 119 |
+
complex_input,
|
| 120 |
+
num_loops=3,
|
| 121 |
+
num_sampling_steps=50,
|
| 122 |
+
num_diffusion_samples=1,
|
| 123 |
+
seed=0,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
model.save_as_cif(result, "complex_prediction.cif")
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## Binder Design With FastPLMs ESMFold2
|
| 130 |
+
|
| 131 |
+
FastPLMs includes a FastPLMs-only port of the Biohub ESMFold2 binder design
|
| 132 |
+
tutorial at `cookbook/tutorials/binder_design_fastplms.py`. The workflow uses
|
| 133 |
+
ESMFold2 experimental checkpoints for differentiable folding losses, ESM++ for
|
| 134 |
+
sequence regularization, and ESMFold2 hero critics for final confidence scoring.
|
| 135 |
+
|
| 136 |
+

|
| 137 |
+
|
| 138 |
+
The optimizer follows the official strategy:
|
| 139 |
+
|
| 140 |
+
1. Optimize mutable `#` residues as continuous amino acid logits.
|
| 141 |
+
2. Suppress cysteine design by masking cysteine logits and gradients.
|
| 142 |
+
3. Backpropagate through ESMFold2 `res_type_soft` using intra-contact,
|
| 143 |
+
inter-contact, and globularity losses from the distogram.
|
| 144 |
+
4. Add an ESM++ masked-LM pseudoperplexity regularizer on mutable binder
|
| 145 |
+
residues.
|
| 146 |
+
5. Keep the late-trajectory sequence with the best iPTM.
|
| 147 |
+
6. Fold the selected sequence with the final critic ensemble and write
|
| 148 |
+
`results.parquet`, `selection.parquet`, `trajectory.jsonl`,
|
| 149 |
+
`best_sequences.fasta`, and per-critic PDB/CIF/logit files.
|
| 150 |
+
|
| 151 |
+
Run the verified EGFR 128 amino acid de novo minibinder example:
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
cd /home/ubuntu/FastPLMs
|
| 155 |
+
|
| 156 |
+
sudo -n docker run --gpus all --rm \
|
| 157 |
+
-v /home/ubuntu/FastPLMs:/app \
|
| 158 |
+
-v /home/ubuntu/FastPLMs:/workspace \
|
| 159 |
+
-v /home/ubuntu/.cache/huggingface:/workspace/.cache/huggingface \
|
| 160 |
+
-w /workspace fastplms-esmfold2 \
|
| 161 |
+
python /app/cookbook/tutorials/binder_design_fastplms.py \
|
| 162 |
+
--backend local \
|
| 163 |
+
--target-name egfr \
|
| 164 |
+
--binder-sequence '################################################################################################################################' \
|
| 165 |
+
--not-antibody \
|
| 166 |
+
--steps 150 \
|
| 167 |
+
--batch-size 1 \
|
| 168 |
+
--seed 103 \
|
| 169 |
+
--output-dir /workspace/campaign_egfr_len128_b1_s150_seed103_consensus_cli
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
Verified result:
|
| 173 |
+
|
| 174 |
+
| Metric | Value |
|
| 175 |
+
| :--- | :--- |
|
| 176 |
+
| Binder length | `128` |
|
| 177 |
+
| Seed | `103` |
|
| 178 |
+
| Steps | `150` |
|
| 179 |
+
| Hero mean iPTM | `0.913870` |
|
| 180 |
+
| Hero min iPTM | `0.904600` |
|
| 181 |
+
| All four hero critics above 0.9 | `True` |
|
| 182 |
+
|
| 183 |
+
Binder sequence:
|
| 184 |
+
|
| 185 |
+
```text
|
| 186 |
+
SAVKHLLEIVKYLEEAIEKALEVDPVFLVPPAAEELLIAAKVIKELAKENPELIEVYELLMKAVKGLKKLVRSNDKEILREVIRLLRKAAKVIREILKNNPDLDPELRKALEELAKVLEEIAEVLEQQ
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
See the full guide in [`docs/binder_design.md`](https://github.com/Synthyra/FastPLMs/blob/main/docs/binder_design.md)
|
| 190 |
+
for Modal execution, official pI and selection scoring, per-critic metrics, and
|
| 191 |
+
the tested cheaper step-count boundary.
|
| 192 |
+
|
| 193 |
+
## Use MSAs
|
| 194 |
+
|
| 195 |
+
```python
|
| 196 |
+
types = model.input_types
|
| 197 |
+
|
| 198 |
+
msa = types.MSA.from_a3m("query.a3m", max_sequences=128)
|
| 199 |
+
input_with_msa = types.StructurePredictionInput(
|
| 200 |
+
sequences=[
|
| 201 |
+
types.ProteinInput(id="A", sequence=msa.query, msa=msa),
|
| 202 |
+
]
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
result = model.fold(input_with_msa, num_sampling_steps=50, seed=0)
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
## Raw Tensor Inference
|
| 209 |
+
|
| 210 |
+
```python
|
| 211 |
+
features, chain_infos = model.prepare_structure_input(complex_input, seed=0)
|
| 212 |
+
|
| 213 |
+
with torch.inference_mode():
|
| 214 |
+
output = model(
|
| 215 |
+
**features,
|
| 216 |
+
num_loops=3,
|
| 217 |
+
num_sampling_steps=50,
|
| 218 |
+
num_diffusion_samples=1,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
decoded = model.input_builder.decode(output, features, chain_infos)
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
Set `load_esmc=False` when loading if you want to provide precomputed `lm_hidden_states` manually or run folding-trunk tests without loading the 6B ESM++ backbone:
|
| 225 |
|
| 226 |
```python
|
|
|
|
|
|
|
|
|
|
| 227 |
model = AutoModel.from_pretrained(
|
| 228 |
"Synthyra/ESMFold2-Fast",
|
| 229 |
+
trust_remote_code=True,
|
| 230 |
+
load_esmc=False,
|
| 231 |
+
).cuda().eval()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
```
|
| 233 |
|
| 234 |
+
For FP8 LM inference, install `transformer_engine.pytorch` in a CUDA
|
| 235 |
+
environment with FP8-capable hardware and load the shared FastPLMs ESM++
|
| 236 |
+
backbone with:
|
| 237 |
|
| 238 |
```python
|
| 239 |
model = AutoModel.from_pretrained(
|
| 240 |
"Synthyra/ESMFold2-Fast",
|
| 241 |
trust_remote_code=True,
|
| 242 |
+
esmc_precision="fp8",
|
| 243 |
).cuda().eval()
|
| 244 |
```
|
| 245 |
+
|
| 246 |
+
FP8 is inference-only for the ESMFold2 LM backbone. TTT remains a bf16/fp32
|
| 247 |
+
path.
|
__init__.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
from .configuration_esmfold2 import ESMFold2Config
|
| 2 |
-
from .modeling_esmfold2_experimental import ESMFold2ExperimentalModel
|
| 3 |
-
from .modeling_esmfold2 import ESMFold2Model
|
| 4 |
-
|
| 5 |
-
__all__ = ["ESMFold2Config", "ESMFold2ExperimentalModel", "ESMFold2Model"]
|
| 6 |
|
|
|
|
|
|
| 1 |
+
from .configuration_esmfold2 import ESMFold2Config
|
| 2 |
+
from .modeling_esmfold2_experimental import ESMFold2ExperimentalModel
|
| 3 |
+
from .modeling_esmfold2 import ESMFold2Model
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
__all__ = ["ESMFold2Config", "ESMFold2ExperimentalModel", "ESMFold2Model"]
|
configuration_esmfold2.py
CHANGED
|
@@ -1,194 +1,206 @@
|
|
| 1 |
-
# Copyright 2026 Biohub. All rights reserved.
|
| 2 |
-
#
|
| 3 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
-
# you may not use this file except in compliance with the License.
|
| 5 |
-
# You may obtain a copy of the License at
|
| 6 |
-
#
|
| 7 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
-
#
|
| 9 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
-
# See the License for the specific language governing permissions and
|
| 13 |
-
# limitations under the License.
|
| 14 |
-
"""ESMFold2 model configuration."""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
from dataclasses import asdict, dataclass, field
|
| 19 |
-
|
| 20 |
-
from transformers.configuration_utils import PretrainedConfig
|
| 21 |
-
|
| 22 |
-
# ---------------------------------------------------------------------------
|
| 23 |
-
# Nested dataclass configs
|
| 24 |
-
# ---------------------------------------------------------------------------
|
| 25 |
-
|
| 26 |
-
_DEFAULT_ESMC_HF_REPO = "
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
""
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
@dataclass
|
| 42 |
-
class
|
| 43 |
-
"""
|
| 44 |
-
|
| 45 |
-
enabled: bool =
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
lm_dropout (`float`, defaults to 0.0):
|
| 193 |
Dropout probability on LM pair embeddings. When > 0, dropout is
|
| 194 |
applied with ``training=True`` (including at inference) to match
|
|
@@ -196,103 +208,121 @@ class ESMFold2Config(PretrainedConfig):
|
|
| 196 |
force_lm_dropout_during_inference (`bool`, defaults to False):
|
| 197 |
When True, apply ``lm_dropout`` even when ``model.eval()`` and
|
| 198 |
``lm_dropout`` > 0. Binder-design loads set this to True.
|
|
|
|
|
|
|
|
|
|
| 199 |
disable_msa_features (`bool`, defaults to False):
|
| 200 |
When True, zero out MSA-derived ``profile`` and ``deletion_mean``
|
| 201 |
before the inputs embedder (experimental medium/large checkpoints).
|
| 202 |
-
inputs (`InputsEmbedderConfig`):
|
| 203 |
-
Configuration for the inputs embedder module.
|
| 204 |
-
folding_trunk (`FoldingTrunkConfig`):
|
| 205 |
-
Configuration for the folding trunk.
|
| 206 |
-
structure_head (`DiffusionStructureHeadConfig`):
|
| 207 |
-
Configuration for the diffusion-based structure prediction head.
|
| 208 |
-
confidence_head (`ConfidenceHeadConfig`):
|
| 209 |
-
Configuration for the confidence prediction head.
|
| 210 |
-
|
| 211 |
-
Examples:
|
| 212 |
-
|
| 213 |
-
```python
|
| 214 |
-
>>> from transformers import ESMFold2Config, ESMFold2ExperimentalModel
|
| 215 |
-
|
| 216 |
-
>>> # Initializing an ESMFold2 configuration
|
| 217 |
-
>>> configuration = ESMFold2Config(type="experimental")
|
| 218 |
-
|
| 219 |
-
>>> # Initializing a model (with random weights) from the configuration
|
| 220 |
-
>>> model = ESMFold2ExperimentalModel(configuration)
|
| 221 |
-
|
| 222 |
-
>>> # Accessing the model configuration
|
| 223 |
-
>>> configuration = model.config
|
| 224 |
-
```
|
| 225 |
-
"""
|
| 226 |
-
|
| 227 |
-
model_type = "esmfold2"
|
| 228 |
-
has_no_defaults_at_init = True
|
| 229 |
-
|
| 230 |
-
def __init__(self, **kwargs):
|
| 231 |
-
super().__init__(**kwargs)
|
| 232 |
-
|
| 233 |
-
self.type: str = kwargs.get("type", "release")
|
| 234 |
-
if self.type not in ("release", "experimental"):
|
| 235 |
-
raise ValueError(
|
| 236 |
-
f"ESMFold2Config.type must be 'release' or 'experimental', "
|
| 237 |
-
f"got {self.type!r}"
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
# Top-level scalar fields
|
| 241 |
-
self.d_single: int = kwargs.get("d_single", 384)
|
| 242 |
-
self.d_pair: int = kwargs.get("d_pair", 256)
|
| 243 |
-
self.n_relative_residx_bins: int = kwargs.get("n_relative_residx_bins", 32)
|
| 244 |
-
self.n_relative_chain_bins: int = kwargs.get("n_relative_chain_bins", 2)
|
| 245 |
-
self.num_loops: int = kwargs.get("num_loops", 10)
|
| 246 |
-
self.num_diffusion_samples: int = kwargs.get("num_diffusion_samples", 8)
|
| 247 |
-
# If True, ``profile`` / ``deletion_mean`` are zeroed before the inputs
|
| 248 |
-
# embedder.
|
| 249 |
-
self.disable_msa_features: bool = kwargs.get("disable_msa_features", False)
|
| 250 |
-
self.lm_dropout: float = kwargs.get("lm_dropout", 0.0)
|
| 251 |
self.force_lm_dropout_during_inference: bool = kwargs.get(
|
| 252 |
"force_lm_dropout_during_inference", False
|
| 253 |
)
|
|
|
|
| 254 |
|
| 255 |
self.lm_d_model: int = kwargs.get("lm_d_model", 2560)
|
| 256 |
-
self.lm_num_layers: int = kwargs.get("lm_num_layers", 80)
|
| 257 |
-
#
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
self.
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
self.
|
| 283 |
-
kwargs.get("
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
output
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Biohub. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""ESMFold2 model configuration."""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from dataclasses import asdict, dataclass, field
|
| 19 |
+
|
| 20 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Nested dataclass configs
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
_DEFAULT_ESMC_HF_REPO = "Synthyra/ESMplusplus_6B"
|
| 27 |
+
_DEFAULT_ESMC_ATTN_BACKEND = "flex"
|
| 28 |
+
_ESMC_ID_ALIASES = {
|
| 29 |
+
"biohub/ESMC-300M": "Synthyra/ESMplusplus_small",
|
| 30 |
+
"biohub/ESMC-600M": "Synthyra/ESMplusplus_large",
|
| 31 |
+
"biohub/ESMC-6B": "Synthyra/ESMplusplus_6B",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def normalize_esmc_id(esmc_id: str) -> str:
|
| 36 |
+
if esmc_id in _ESMC_ID_ALIASES:
|
| 37 |
+
return _ESMC_ID_ALIASES[esmc_id]
|
| 38 |
+
return esmc_id
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class MSAEncoderConfig:
|
| 43 |
+
"""Config for the optional MSA encoder module (Large MSA models only)."""
|
| 44 |
+
|
| 45 |
+
enabled: bool = False
|
| 46 |
+
d_msa: int = 128
|
| 47 |
+
d_hidden: int = 32
|
| 48 |
+
n_layers: int = 4
|
| 49 |
+
n_heads_msa: int = 8
|
| 50 |
+
msa_head_width: int = 32
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class ParcaeConfig:
|
| 55 |
+
"""Release-only config for the parcae diffusion-loop scheduler."""
|
| 56 |
+
|
| 57 |
+
enabled: bool = True
|
| 58 |
+
poisson_mean: float = 3.0
|
| 59 |
+
min_steps: int = 1
|
| 60 |
+
max_steps: int | None = 6
|
| 61 |
+
coda_n_layers: int = 2
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class LMEncoderConfig:
|
| 66 |
+
"""Release-only config for the LM-side pair encoder."""
|
| 67 |
+
|
| 68 |
+
enabled: bool = True
|
| 69 |
+
n_layers: int = 4
|
| 70 |
+
lm_dropout: float = 0.25
|
| 71 |
+
per_loop_lm_dropout: bool = True
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@dataclass
|
| 75 |
+
class AtomAttentionConfig:
|
| 76 |
+
"""Config for SWA atom encoder/decoder with 3D RoPE."""
|
| 77 |
+
|
| 78 |
+
d_atom: int = 128
|
| 79 |
+
d_token: int = 768
|
| 80 |
+
n_blocks: int = 3
|
| 81 |
+
n_heads: int = 4
|
| 82 |
+
swa_window_size: int = 128
|
| 83 |
+
expansion_ratio: int = 2
|
| 84 |
+
# 3D RoPE config
|
| 85 |
+
spatial_rope_base_frequency: float = 20.0
|
| 86 |
+
n_spatial_rope_pairs_per_axis: int = 2
|
| 87 |
+
n_uid_rope_pairs: int = 10
|
| 88 |
+
uid_rope_base_frequency: float = 10000.0
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@dataclass
|
| 92 |
+
class FoldingTrunkConfig:
|
| 93 |
+
n_layers: int = 24
|
| 94 |
+
n_heads: int = 8
|
| 95 |
+
dropout: float = 0.0
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@dataclass
|
| 99 |
+
class InputsEmbedderConfig:
|
| 100 |
+
d_inputs: int = 451
|
| 101 |
+
atom_encoder: AtomAttentionConfig = field(default_factory=AtomAttentionConfig)
|
| 102 |
+
|
| 103 |
+
def __post_init__(self):
|
| 104 |
+
if isinstance(self.atom_encoder, dict):
|
| 105 |
+
self.atom_encoder = AtomAttentionConfig(**self.atom_encoder)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@dataclass
|
| 109 |
+
class DiffusionModuleConfig:
|
| 110 |
+
"""Config for the DiffusionModule."""
|
| 111 |
+
|
| 112 |
+
sigma_data: float = 16.0
|
| 113 |
+
c_atom: int = 128
|
| 114 |
+
c_token: int = 768
|
| 115 |
+
c_z: int = 256
|
| 116 |
+
c_s_inputs: int = 451
|
| 117 |
+
fourier_dim: int = 256
|
| 118 |
+
relpos_r_max: int = 32
|
| 119 |
+
relpos_s_max: int = 2
|
| 120 |
+
atom_num_blocks: int = 3
|
| 121 |
+
atom_num_heads: int = 4
|
| 122 |
+
token_num_blocks: int = 12
|
| 123 |
+
token_num_heads: int = 16
|
| 124 |
+
transition_multiplier: int = 2
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@dataclass
|
| 128 |
+
class DiffusionStructureHeadConfig:
|
| 129 |
+
"""Config for the diffusion-based structure prediction head."""
|
| 130 |
+
|
| 131 |
+
diffusion_module: DiffusionModuleConfig = field(
|
| 132 |
+
default_factory=DiffusionModuleConfig
|
| 133 |
+
)
|
| 134 |
+
distogram_bins: int = 128
|
| 135 |
+
|
| 136 |
+
# Training noise: sigma ~ sigma_data * exp(mu + sigma * N(0,1))
|
| 137 |
+
train_noise_log_mean: float = -1.2
|
| 138 |
+
train_noise_log_std: float = 1.5
|
| 139 |
+
|
| 140 |
+
# Sampling defaults (ODE)
|
| 141 |
+
gamma_0: float = 0.605
|
| 142 |
+
gamma_min: float = 1.107
|
| 143 |
+
noise_scale: float = 0.0
|
| 144 |
+
step_scale: float = 1.0
|
| 145 |
+
|
| 146 |
+
# Inference schedule defaults
|
| 147 |
+
inference_s_max: float = 160.0
|
| 148 |
+
inference_s_min: float = 4e-4
|
| 149 |
+
inference_p: float = 8.0
|
| 150 |
+
inference_num_steps: int = 68
|
| 151 |
+
|
| 152 |
+
def __post_init__(self):
|
| 153 |
+
if isinstance(self.diffusion_module, dict):
|
| 154 |
+
self.diffusion_module = DiffusionModuleConfig(**self.diffusion_module)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@dataclass
|
| 158 |
+
class ConfidenceHeadConfig:
|
| 159 |
+
enabled: bool = True
|
| 160 |
+
num_plddt_bins: int = 50
|
| 161 |
+
num_pde_bins: int = 64
|
| 162 |
+
num_pae_bins: int = 64
|
| 163 |
+
min_dist: float = 2.0
|
| 164 |
+
max_dist: float = 52.0
|
| 165 |
+
distogram_bins: int = 128
|
| 166 |
+
folding_trunk: FoldingTrunkConfig = field(
|
| 167 |
+
default_factory=lambda: FoldingTrunkConfig(n_layers=4)
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def __post_init__(self):
|
| 171 |
+
if isinstance(self.folding_trunk, dict):
|
| 172 |
+
self.folding_trunk = FoldingTrunkConfig(**self.folding_trunk)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ---------------------------------------------------------------------------
|
| 176 |
+
# Top-level config
|
| 177 |
+
# ---------------------------------------------------------------------------
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class ESMFold2Config(PretrainedConfig):
|
| 181 |
+
"""
|
| 182 |
+
Configuration for the ESMFold2 structure prediction model.
|
| 183 |
+
|
| 184 |
+
Uses SWA atom encoders with 3D RoPE, a diffusion transformer,
|
| 185 |
+
a folding trunk, and an ESMC 6B PLM backbone.
|
| 186 |
+
|
| 187 |
+
Configuration objects inherit from [`PretrainedConfig`] and can be used to control
|
| 188 |
+
the model outputs. Read the documentation from [`PretrainedConfig`] for more
|
| 189 |
+
information.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
d_single (`int`, defaults to 384):
|
| 193 |
+
Dimensionality of single (per-residue) representations.
|
| 194 |
+
d_pair (`int`, defaults to 256):
|
| 195 |
+
Dimensionality of pair (residue-residue) representations.
|
| 196 |
+
n_relative_residx_bins (`int`, defaults to 32):
|
| 197 |
+
Number of bins for relative residue index encoding.
|
| 198 |
+
n_relative_chain_bins (`int`, defaults to 2):
|
| 199 |
+
Number of bins for relative chain encoding.
|
| 200 |
+
num_loops (`int`, defaults to 10):
|
| 201 |
+
Number of trunk loops for iterative refinement.
|
| 202 |
+
num_diffusion_samples (`int`, defaults to 8):
|
| 203 |
+
Number of parallel structure predictions to generate.
|
| 204 |
lm_dropout (`float`, defaults to 0.0):
|
| 205 |
Dropout probability on LM pair embeddings. When > 0, dropout is
|
| 206 |
applied with ``training=True`` (including at inference) to match
|
|
|
|
| 208 |
force_lm_dropout_during_inference (`bool`, defaults to False):
|
| 209 |
When True, apply ``lm_dropout`` even when ``model.eval()`` and
|
| 210 |
``lm_dropout`` > 0. Binder-design loads set this to True.
|
| 211 |
+
lm_mask_pct (`float`, defaults to 0.0):
|
| 212 |
+
Fraction of LM residue tokens randomly replaced with the LM mask
|
| 213 |
+
token before running the PLM backbone.
|
| 214 |
disable_msa_features (`bool`, defaults to False):
|
| 215 |
When True, zero out MSA-derived ``profile`` and ``deletion_mean``
|
| 216 |
before the inputs embedder (experimental medium/large checkpoints).
|
| 217 |
+
inputs (`InputsEmbedderConfig`):
|
| 218 |
+
Configuration for the inputs embedder module.
|
| 219 |
+
folding_trunk (`FoldingTrunkConfig`):
|
| 220 |
+
Configuration for the folding trunk.
|
| 221 |
+
structure_head (`DiffusionStructureHeadConfig`):
|
| 222 |
+
Configuration for the diffusion-based structure prediction head.
|
| 223 |
+
confidence_head (`ConfidenceHeadConfig`):
|
| 224 |
+
Configuration for the confidence prediction head.
|
| 225 |
+
|
| 226 |
+
Examples:
|
| 227 |
+
|
| 228 |
+
```python
|
| 229 |
+
>>> from transformers import ESMFold2Config, ESMFold2ExperimentalModel
|
| 230 |
+
|
| 231 |
+
>>> # Initializing an ESMFold2 configuration
|
| 232 |
+
>>> configuration = ESMFold2Config(type="experimental")
|
| 233 |
+
|
| 234 |
+
>>> # Initializing a model (with random weights) from the configuration
|
| 235 |
+
>>> model = ESMFold2ExperimentalModel(configuration)
|
| 236 |
+
|
| 237 |
+
>>> # Accessing the model configuration
|
| 238 |
+
>>> configuration = model.config
|
| 239 |
+
```
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
model_type = "esmfold2"
|
| 243 |
+
has_no_defaults_at_init = True
|
| 244 |
+
|
| 245 |
+
def __init__(self, **kwargs):
|
| 246 |
+
super().__init__(**kwargs)
|
| 247 |
+
|
| 248 |
+
self.type: str = kwargs.get("type", "release")
|
| 249 |
+
if self.type not in ("release", "experimental"):
|
| 250 |
+
raise ValueError(
|
| 251 |
+
f"ESMFold2Config.type must be 'release' or 'experimental', "
|
| 252 |
+
f"got {self.type!r}"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# Top-level scalar fields
|
| 256 |
+
self.d_single: int = kwargs.get("d_single", 384)
|
| 257 |
+
self.d_pair: int = kwargs.get("d_pair", 256)
|
| 258 |
+
self.n_relative_residx_bins: int = kwargs.get("n_relative_residx_bins", 32)
|
| 259 |
+
self.n_relative_chain_bins: int = kwargs.get("n_relative_chain_bins", 2)
|
| 260 |
+
self.num_loops: int = kwargs.get("num_loops", 10)
|
| 261 |
+
self.num_diffusion_samples: int = kwargs.get("num_diffusion_samples", 8)
|
| 262 |
+
# If True, ``profile`` / ``deletion_mean`` are zeroed before the inputs
|
| 263 |
+
# embedder.
|
| 264 |
+
self.disable_msa_features: bool = kwargs.get("disable_msa_features", False)
|
| 265 |
+
self.lm_dropout: float = kwargs.get("lm_dropout", 0.0)
|
| 266 |
self.force_lm_dropout_during_inference: bool = kwargs.get(
|
| 267 |
"force_lm_dropout_during_inference", False
|
| 268 |
)
|
| 269 |
+
self.lm_mask_pct: float = kwargs.get("lm_mask_pct", 0.0)
|
| 270 |
|
| 271 |
self.lm_d_model: int = kwargs.get("lm_d_model", 2560)
|
| 272 |
+
self.lm_num_layers: int = kwargs.get("lm_num_layers", 80)
|
| 273 |
+
# Backward-compatible field name; values now point to FastPLMs ESM++.
|
| 274 |
+
raw_esmc_id = (
|
| 275 |
+
kwargs["esmc_id"] if "esmc_id" in kwargs else _DEFAULT_ESMC_HF_REPO
|
| 276 |
+
)
|
| 277 |
+
self.esmc_id: str = normalize_esmc_id(raw_esmc_id)
|
| 278 |
+
self.esmc_attn_backend: str = (
|
| 279 |
+
kwargs["esmc_attn_backend"]
|
| 280 |
+
if "esmc_attn_backend" in kwargs
|
| 281 |
+
else _DEFAULT_ESMC_ATTN_BACKEND
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
def _init_nested(cls, val):
|
| 285 |
+
if isinstance(val, cls):
|
| 286 |
+
return val
|
| 287 |
+
if isinstance(val, dict):
|
| 288 |
+
return cls(**val)
|
| 289 |
+
return cls()
|
| 290 |
+
|
| 291 |
+
self.inputs = _init_nested(InputsEmbedderConfig, kwargs.get("inputs"))
|
| 292 |
+
self.folding_trunk = _init_nested(
|
| 293 |
+
FoldingTrunkConfig, kwargs.get("folding_trunk")
|
| 294 |
+
)
|
| 295 |
+
self.structure_head = _init_nested(
|
| 296 |
+
DiffusionStructureHeadConfig, kwargs.get("structure_head")
|
| 297 |
+
)
|
| 298 |
+
self.confidence_head = _init_nested(
|
| 299 |
+
ConfidenceHeadConfig, kwargs.get("confidence_head")
|
| 300 |
+
)
|
| 301 |
+
self.msa_encoder = _init_nested(MSAEncoderConfig, kwargs.get("msa_encoder"))
|
| 302 |
+
# Release-only modules — ignored when ``type == "experimental"``.
|
| 303 |
+
self.parcae = _init_nested(ParcaeConfig, kwargs.get("parcae"))
|
| 304 |
+
self.lm_encoder = _init_nested(LMEncoderConfig, kwargs.get("lm_encoder"))
|
| 305 |
+
# If True, MSA encoder output replaces the pair stream; if False, it is added.
|
| 306 |
+
self.msa_encoder_overwrite: bool = bool(
|
| 307 |
+
kwargs.get("msa_encoder_overwrite", True)
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
def to_dict(self):
|
| 311 |
+
output = super().to_dict()
|
| 312 |
+
output["inputs"] = asdict(self.inputs)
|
| 313 |
+
output["folding_trunk"] = asdict(self.folding_trunk)
|
| 314 |
+
output["structure_head"] = asdict(self.structure_head)
|
| 315 |
+
output["confidence_head"] = asdict(self.confidence_head)
|
| 316 |
+
output["msa_encoder"] = asdict(self.msa_encoder)
|
| 317 |
+
output["parcae"] = asdict(self.parcae)
|
| 318 |
+
output["lm_encoder"] = asdict(self.lm_encoder)
|
| 319 |
+
return output
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
__all__ = [
|
| 323 |
+
"ESMFold2Config",
|
| 324 |
+
"MSAEncoderConfig",
|
| 325 |
+
"ParcaeConfig",
|
| 326 |
+
"LMEncoderConfig",
|
| 327 |
+
"normalize_esmc_id",
|
| 328 |
+
]
|
esmfold2_affine3d.py
CHANGED
|
@@ -1,560 +1,560 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import typing as T
|
| 4 |
-
from abc import ABC
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
from torch.nn import functional as F
|
| 9 |
-
from typing_extensions import Self
|
| 10 |
-
|
| 11 |
-
from .esmfold2_misc import fp32_autocast_context
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class Rotation(ABC):
|
| 15 |
-
@classmethod
|
| 16 |
-
def identity(cls, shape: tuple[int, ...], **tensor_kwargs) -> Self: ...
|
| 17 |
-
|
| 18 |
-
@classmethod
|
| 19 |
-
def random(cls, shape: tuple[int, ...], **tensor_kwargs) -> Self: ...
|
| 20 |
-
|
| 21 |
-
def __getitem__(self, idx: T.Any) -> Self: ...
|
| 22 |
-
|
| 23 |
-
@property
|
| 24 |
-
def tensor(self) -> torch.Tensor:
|
| 25 |
-
# We claim that this should be zero-cost abstraction that returns the raw tensor backing this
|
| 26 |
-
# object. The raw tensor should always have exactly 1 more dim than self.shape, which should be
|
| 27 |
-
# implemented using reshaping
|
| 28 |
-
...
|
| 29 |
-
|
| 30 |
-
@property
|
| 31 |
-
def shape(self) -> torch.Size:
|
| 32 |
-
# The "shape" of the rotation, as if it was a torch.tensor object
|
| 33 |
-
# This means that 1x4 quaternions are treated as size (1,) for example
|
| 34 |
-
...
|
| 35 |
-
|
| 36 |
-
def as_matrix(self) -> RotationMatrix: ...
|
| 37 |
-
|
| 38 |
-
def as_quat(self, normalize: bool = False) -> RotationQuat: ...
|
| 39 |
-
|
| 40 |
-
def compose(self, other: Self) -> Self:
|
| 41 |
-
# To be safe, we force users to explicitly convert between rotation types.
|
| 42 |
-
...
|
| 43 |
-
|
| 44 |
-
def convert_compose(self, other: Self) -> Self:
|
| 45 |
-
# This function will automatically convert between types of rotations
|
| 46 |
-
...
|
| 47 |
-
|
| 48 |
-
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 49 |
-
# rotates points by this rotation object
|
| 50 |
-
...
|
| 51 |
-
|
| 52 |
-
def invert(self) -> Self: ...
|
| 53 |
-
|
| 54 |
-
@property
|
| 55 |
-
def dtype(self) -> torch.dtype:
|
| 56 |
-
return self.tensor.dtype
|
| 57 |
-
|
| 58 |
-
@property
|
| 59 |
-
def device(self) -> torch.device:
|
| 60 |
-
return self.tensor.device
|
| 61 |
-
|
| 62 |
-
@property
|
| 63 |
-
def requires_grad(self) -> bool:
|
| 64 |
-
return self.tensor.requires_grad
|
| 65 |
-
|
| 66 |
-
@classmethod
|
| 67 |
-
def _from_tensor(cls, t: torch.Tensor) -> Self:
|
| 68 |
-
# This function exists to simplify the below functions, esp type signatures
|
| 69 |
-
# Its implementation is different from Affine3D.from_tensor and does not
|
| 70 |
-
# autodetect rotation types.
|
| 71 |
-
return cls(t) # type: ignore
|
| 72 |
-
|
| 73 |
-
def to(self, **kwargs) -> Self:
|
| 74 |
-
return self._from_tensor(self.tensor.to(**kwargs))
|
| 75 |
-
|
| 76 |
-
def detach(self, *args, **kwargs) -> Self:
|
| 77 |
-
return self._from_tensor(self.tensor.detach(**kwargs))
|
| 78 |
-
|
| 79 |
-
def tensor_apply(self, func) -> Self:
|
| 80 |
-
# Applys a function to the underlying tensor
|
| 81 |
-
return self._from_tensor(
|
| 82 |
-
torch.stack([func(x) for x in self.tensor.unbind(dim=-1)], dim=-1)
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
class RotationMatrix(Rotation):
|
| 87 |
-
def __init__(self, rots: torch.Tensor):
|
| 88 |
-
if rots.shape[-1] == 9:
|
| 89 |
-
rots = rots.unflatten(-1, (3, 3))
|
| 90 |
-
assert rots.shape[-1] == 3
|
| 91 |
-
assert rots.shape[-2] == 3
|
| 92 |
-
# Force full precision
|
| 93 |
-
rots = rots.to(torch.float32)
|
| 94 |
-
self._rots = rots
|
| 95 |
-
|
| 96 |
-
@classmethod
|
| 97 |
-
def identity(cls, shape, **tensor_kwargs):
|
| 98 |
-
rots = torch.eye(3, **tensor_kwargs)
|
| 99 |
-
rots = rots.view(*[1 for _ in range(len(shape))], 3, 3)
|
| 100 |
-
rots = rots.expand(*shape, -1, -1)
|
| 101 |
-
return cls(rots)
|
| 102 |
-
|
| 103 |
-
@classmethod
|
| 104 |
-
def random(cls, shape, **tensor_kwargs):
|
| 105 |
-
return RotationQuat.random(shape, **tensor_kwargs).as_matrix()
|
| 106 |
-
|
| 107 |
-
def __getitem__(self, idx: T.Any) -> RotationMatrix:
|
| 108 |
-
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 109 |
-
return RotationMatrix(self._rots[indices + (slice(None), slice(None))])
|
| 110 |
-
|
| 111 |
-
@property
|
| 112 |
-
def shape(self) -> torch.Size:
|
| 113 |
-
return self._rots.shape[:-2]
|
| 114 |
-
|
| 115 |
-
def as_matrix(self) -> RotationMatrix:
|
| 116 |
-
return self
|
| 117 |
-
|
| 118 |
-
def as_quat(self, normalize: bool = False) -> RotationQuat:
|
| 119 |
-
m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
|
| 120 |
-
self._rots.flatten(-2), dim=-1
|
| 121 |
-
)
|
| 122 |
-
q_abs = _sqrt_subgradient(
|
| 123 |
-
torch.stack(
|
| 124 |
-
[
|
| 125 |
-
1.0 + m00 + m11 + m22,
|
| 126 |
-
1.0 + m00 - m11 - m22,
|
| 127 |
-
1.0 - m00 + m11 - m22,
|
| 128 |
-
1.0 - m00 - m11 + m22,
|
| 129 |
-
],
|
| 130 |
-
dim=-1,
|
| 131 |
-
)
|
| 132 |
-
)
|
| 133 |
-
# we produce the desired quaternion multiplied by each of r, i, j, k
|
| 134 |
-
quat_by_rijk = torch.stack(
|
| 135 |
-
[
|
| 136 |
-
x
|
| 137 |
-
for lst in [
|
| 138 |
-
[q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01],
|
| 139 |
-
[m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20],
|
| 140 |
-
[m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21],
|
| 141 |
-
[m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2],
|
| 142 |
-
]
|
| 143 |
-
for x in lst
|
| 144 |
-
],
|
| 145 |
-
dim=-1,
|
| 146 |
-
).unflatten(-1, (4, 4))
|
| 147 |
-
|
| 148 |
-
# We floor here at 0.1 but the exact level is not important; if q_abs is small,
|
| 149 |
-
# the candidate won't be picked.
|
| 150 |
-
flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
|
| 151 |
-
quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
|
| 152 |
-
|
| 153 |
-
# if not for numerical problems, quat_candidates[i] should be same (up to a sign),
|
| 154 |
-
# forall i; we pick the best-conditioned one (with the largest denominator)
|
| 155 |
-
# We manually implement one_hot so torch.compile works
|
| 156 |
-
one_hot = torch.zeros_like(q_abs, dtype=torch.bool)
|
| 157 |
-
one_hot.scatter_(-1, q_abs.argmax(dim=-1, keepdim=True), True)
|
| 158 |
-
quat = quat_candidates[one_hot, :].reshape(q_abs.shape)
|
| 159 |
-
return RotationQuat(quat)
|
| 160 |
-
|
| 161 |
-
def compose(self, other: RotationMatrix) -> RotationMatrix:
|
| 162 |
-
with fp32_autocast_context(self._rots.device.type):
|
| 163 |
-
return RotationMatrix(self._rots @ other._rots)
|
| 164 |
-
|
| 165 |
-
def convert_compose(self, other: Rotation):
|
| 166 |
-
return self.compose(other.as_matrix())
|
| 167 |
-
|
| 168 |
-
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 169 |
-
with fp32_autocast_context(self.device.type):
|
| 170 |
-
if self._rots.shape[-3] == 1:
|
| 171 |
-
# This is a slight speedup over einsum for batched rotations
|
| 172 |
-
return p @ self._rots.transpose(-1, -2).squeeze(-3)
|
| 173 |
-
else:
|
| 174 |
-
# einsum way faster than bmm!
|
| 175 |
-
return torch.einsum("...ij,...j", self._rots, p)
|
| 176 |
-
|
| 177 |
-
def invert(self) -> RotationMatrix:
|
| 178 |
-
return RotationMatrix(self._rots.transpose(-1, -2))
|
| 179 |
-
|
| 180 |
-
@property
|
| 181 |
-
def tensor(self) -> torch.Tensor:
|
| 182 |
-
return self._rots.flatten(-2)
|
| 183 |
-
|
| 184 |
-
def to_3x3(self) -> torch.Tensor:
|
| 185 |
-
return self._rots
|
| 186 |
-
|
| 187 |
-
@staticmethod
|
| 188 |
-
def from_graham_schmidt(
|
| 189 |
-
x_axis: torch.Tensor, xy_plane: torch.Tensor, eps: float = 1e-12
|
| 190 |
-
) -> RotationMatrix:
|
| 191 |
-
# A low eps here is necessary for good stability!
|
| 192 |
-
return RotationMatrix(_graham_schmidt(x_axis, xy_plane, eps))
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
class RotationQuat(Rotation):
|
| 196 |
-
def __init__(self, quats: torch.Tensor, normalized=False):
|
| 197 |
-
assert quats.shape[-1] == 4
|
| 198 |
-
self._normalized = normalized
|
| 199 |
-
# Force float32 as well
|
| 200 |
-
if normalized:
|
| 201 |
-
self._quats = F.normalize(quats.to(torch.float32), dim=-1)
|
| 202 |
-
self._quats = self._quats.where(self._quats[..., :1] >= 0, -self._quats)
|
| 203 |
-
else:
|
| 204 |
-
self._quats = quats.to(torch.float32)
|
| 205 |
-
|
| 206 |
-
@classmethod
|
| 207 |
-
def identity(cls, shape, **tensor_kwargs):
|
| 208 |
-
q = torch.ones((*shape, 4), **tensor_kwargs)
|
| 209 |
-
mult = torch.tensor([1, 0, 0, 0], device=q.device)
|
| 210 |
-
return RotationQuat(q * mult)
|
| 211 |
-
|
| 212 |
-
@classmethod
|
| 213 |
-
def random(cls, shape, **tensor_kwargs):
|
| 214 |
-
quat = torch.randn((*shape, 4), **tensor_kwargs)
|
| 215 |
-
return RotationQuat(quat, normalized=True)
|
| 216 |
-
|
| 217 |
-
def __getitem__(self, idx: T.Any) -> RotationQuat:
|
| 218 |
-
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 219 |
-
return RotationQuat(self._quats[indices + (slice(None),)])
|
| 220 |
-
|
| 221 |
-
@property
|
| 222 |
-
def shape(self) -> torch.Size:
|
| 223 |
-
return self._quats.shape[:-1]
|
| 224 |
-
|
| 225 |
-
def compose(self, other: RotationQuat) -> RotationQuat:
|
| 226 |
-
with fp32_autocast_context(self._quats.device.type):
|
| 227 |
-
return RotationQuat(_quat_mult(self._quats, other._quats))
|
| 228 |
-
|
| 229 |
-
def convert_compose(self, other: Rotation):
|
| 230 |
-
return self.compose(other.as_quat())
|
| 231 |
-
|
| 232 |
-
def as_matrix(self) -> RotationMatrix:
|
| 233 |
-
q = self.normalized().tensor
|
| 234 |
-
r, i, j, k = torch.unbind(q, -1)
|
| 235 |
-
two_s = 2.0 / torch.linalg.norm(q, dim=-1)
|
| 236 |
-
|
| 237 |
-
o = torch.stack(
|
| 238 |
-
(
|
| 239 |
-
1 - two_s * (j * j + k * k),
|
| 240 |
-
two_s * (i * j - k * r),
|
| 241 |
-
two_s * (i * k + j * r),
|
| 242 |
-
two_s * (i * j + k * r),
|
| 243 |
-
1 - two_s * (i * i + k * k),
|
| 244 |
-
two_s * (j * k - i * r),
|
| 245 |
-
two_s * (i * k - j * r),
|
| 246 |
-
two_s * (j * k + i * r),
|
| 247 |
-
1 - two_s * (i * i + j * j),
|
| 248 |
-
),
|
| 249 |
-
-1,
|
| 250 |
-
)
|
| 251 |
-
return RotationMatrix(o.reshape(q.shape[:-1] + (3, 3)))
|
| 252 |
-
|
| 253 |
-
def as_quat(self, normalize: bool = False) -> RotationQuat:
|
| 254 |
-
return self
|
| 255 |
-
|
| 256 |
-
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 257 |
-
return _quat_rotation(self.normalized()._quats, p)
|
| 258 |
-
|
| 259 |
-
def invert(self) -> RotationQuat:
|
| 260 |
-
return RotationQuat(_quat_invert(self._quats))
|
| 261 |
-
|
| 262 |
-
@property
|
| 263 |
-
def tensor(self) -> torch.Tensor:
|
| 264 |
-
return self._quats
|
| 265 |
-
|
| 266 |
-
def normalized(self) -> RotationQuat:
|
| 267 |
-
return self if self._normalized else RotationQuat(self._quats, normalized=True)
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
@dataclass(frozen=True)
|
| 271 |
-
class Affine3D:
|
| 272 |
-
trans: torch.Tensor
|
| 273 |
-
rot: Rotation
|
| 274 |
-
|
| 275 |
-
def __post_init__(self):
|
| 276 |
-
assert self.trans.shape[:-1] == self.rot.shape
|
| 277 |
-
|
| 278 |
-
@staticmethod
|
| 279 |
-
def identity(
|
| 280 |
-
shape_or_affine: T.Union[tuple[int, ...], "Affine3D"],
|
| 281 |
-
rotation_type: T.Type[Rotation] = RotationMatrix,
|
| 282 |
-
**tensor_kwargs,
|
| 283 |
-
):
|
| 284 |
-
# Creates a new identity Affine3D object with a specified shape
|
| 285 |
-
# or the same shape as another Affine3D object.
|
| 286 |
-
if isinstance(shape_or_affine, Affine3D):
|
| 287 |
-
kwargs = {"dtype": shape_or_affine.dtype, "device": shape_or_affine.device}
|
| 288 |
-
kwargs.update(tensor_kwargs)
|
| 289 |
-
shape = shape_or_affine.shape
|
| 290 |
-
rotation_type = type(shape_or_affine.rot)
|
| 291 |
-
else:
|
| 292 |
-
kwargs = tensor_kwargs
|
| 293 |
-
shape = shape_or_affine
|
| 294 |
-
return Affine3D(
|
| 295 |
-
torch.zeros((*shape, 3), **kwargs), rotation_type.identity(shape, **kwargs)
|
| 296 |
-
)
|
| 297 |
-
|
| 298 |
-
@staticmethod
|
| 299 |
-
def random(
|
| 300 |
-
shape: tuple[int, ...],
|
| 301 |
-
std: float = 1,
|
| 302 |
-
rotation_type: T.Type[Rotation] = RotationMatrix,
|
| 303 |
-
**tensor_kwargs,
|
| 304 |
-
) -> "Affine3D":
|
| 305 |
-
return Affine3D(
|
| 306 |
-
trans=torch.randn((*shape, 3), **tensor_kwargs).mul(std),
|
| 307 |
-
rot=rotation_type.random(shape, **tensor_kwargs),
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
def __getitem__(self, idx: T.Any) -> "Affine3D":
|
| 311 |
-
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 312 |
-
return Affine3D(trans=self.trans[indices + (slice(None),)], rot=self.rot[idx])
|
| 313 |
-
|
| 314 |
-
@property
|
| 315 |
-
def shape(self) -> torch.Size:
|
| 316 |
-
return self.trans.shape[:-1]
|
| 317 |
-
|
| 318 |
-
@property
|
| 319 |
-
def dtype(self) -> torch.dtype:
|
| 320 |
-
return self.trans.dtype
|
| 321 |
-
|
| 322 |
-
@property
|
| 323 |
-
def device(self) -> torch.device:
|
| 324 |
-
return self.trans.device
|
| 325 |
-
|
| 326 |
-
@property
|
| 327 |
-
def requires_grad(self) -> bool:
|
| 328 |
-
return self.trans.requires_grad
|
| 329 |
-
|
| 330 |
-
def to(self, **kwargs) -> "Affine3D":
|
| 331 |
-
return Affine3D(self.trans.to(**kwargs), self.rot.to(**kwargs))
|
| 332 |
-
|
| 333 |
-
def detach(self, *args, **kwargs) -> "Affine3D":
|
| 334 |
-
return Affine3D(self.trans.detach(**kwargs), self.rot.detach(**kwargs))
|
| 335 |
-
|
| 336 |
-
def tensor_apply(self, func) -> "Affine3D":
|
| 337 |
-
# Applys a function to the underlying tensor
|
| 338 |
-
return self.from_tensor(
|
| 339 |
-
torch.stack([func(x) for x in self.tensor.unbind(dim=-1)], dim=-1)
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
def as_matrix(self):
|
| 343 |
-
return Affine3D(trans=self.trans, rot=self.rot.as_matrix())
|
| 344 |
-
|
| 345 |
-
def as_quat(self, normalize: bool = False):
|
| 346 |
-
return Affine3D(trans=self.trans, rot=self.rot.as_quat(normalize))
|
| 347 |
-
|
| 348 |
-
def compose(self, other: "Affine3D", autoconvert: bool = False):
|
| 349 |
-
rot = self.rot
|
| 350 |
-
new_rot = (rot.convert_compose if autoconvert else rot.compose)(other.rot)
|
| 351 |
-
new_trans = rot.apply(other.trans) + self.trans
|
| 352 |
-
return Affine3D(trans=new_trans, rot=new_rot)
|
| 353 |
-
|
| 354 |
-
def compose_rotation(self, other: Rotation, autoconvert: bool = False):
|
| 355 |
-
return Affine3D(
|
| 356 |
-
trans=self.trans,
|
| 357 |
-
rot=(self.rot.convert_compose if autoconvert else self.rot.compose)(other),
|
| 358 |
-
)
|
| 359 |
-
|
| 360 |
-
def scale(self, v: torch.Tensor | float):
|
| 361 |
-
return Affine3D(self.trans * v, self.rot)
|
| 362 |
-
|
| 363 |
-
def mask(self, mask: torch.Tensor, with_zero=False):
|
| 364 |
-
# Returns a transform where True positions in mask is identity
|
| 365 |
-
if with_zero:
|
| 366 |
-
tensor = self.tensor
|
| 367 |
-
return Affine3D.from_tensor(
|
| 368 |
-
torch.zeros_like(tensor).where(mask[..., None], tensor)
|
| 369 |
-
)
|
| 370 |
-
else:
|
| 371 |
-
identity = self.identity(
|
| 372 |
-
self.shape,
|
| 373 |
-
rotation_type=type(self.rot),
|
| 374 |
-
device=self.device,
|
| 375 |
-
dtype=self.dtype,
|
| 376 |
-
).tensor
|
| 377 |
-
return Affine3D.from_tensor(identity.where(mask[..., None], self.tensor))
|
| 378 |
-
|
| 379 |
-
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 380 |
-
return self.rot.apply(p) + self.trans
|
| 381 |
-
|
| 382 |
-
def invert(self):
|
| 383 |
-
inv_rot = self.rot.invert()
|
| 384 |
-
return Affine3D(trans=-inv_rot.apply(self.trans), rot=inv_rot)
|
| 385 |
-
|
| 386 |
-
@property
|
| 387 |
-
def tensor(self) -> torch.Tensor:
|
| 388 |
-
return torch.cat([self.rot.tensor, self.trans], dim=-1)
|
| 389 |
-
|
| 390 |
-
@staticmethod
|
| 391 |
-
def from_tensor(t: torch.Tensor) -> "Affine3D":
|
| 392 |
-
match t.shape[-1]:
|
| 393 |
-
case 4:
|
| 394 |
-
# Assume tensor 4x4 for backward compat with alphafold
|
| 395 |
-
trans = t[..., :3, 3]
|
| 396 |
-
rot = RotationMatrix(t[..., :3, :3])
|
| 397 |
-
case 6:
|
| 398 |
-
# Assume quaternion representation with real part = 1
|
| 399 |
-
trans = t[..., -3:]
|
| 400 |
-
rot = RotationQuat(F.pad(t[..., :3], (1, 0), value=1))
|
| 401 |
-
case 7:
|
| 402 |
-
trans = t[..., -3:]
|
| 403 |
-
rot = RotationQuat(t[..., :4])
|
| 404 |
-
case 12:
|
| 405 |
-
trans = t[..., -3:]
|
| 406 |
-
rot = RotationMatrix(t[..., :-3].unflatten(-1, (3, 3)))
|
| 407 |
-
case _:
|
| 408 |
-
raise RuntimeError(
|
| 409 |
-
f"Cannot detect rotation fromat from {t.shape[-1] -3}-d flat vector"
|
| 410 |
-
)
|
| 411 |
-
return Affine3D(trans, rot)
|
| 412 |
-
|
| 413 |
-
@staticmethod
|
| 414 |
-
def from_tensor_pair(t: torch.Tensor, r: torch.Tensor) -> "Affine3D":
|
| 415 |
-
return Affine3D(t, RotationMatrix(r))
|
| 416 |
-
|
| 417 |
-
@staticmethod
|
| 418 |
-
def from_graham_schmidt(
|
| 419 |
-
neg_x_axis: torch.Tensor,
|
| 420 |
-
origin: torch.Tensor,
|
| 421 |
-
xy_plane: torch.Tensor,
|
| 422 |
-
eps: float = 1e-10,
|
| 423 |
-
):
|
| 424 |
-
# The arguments of this function is for parity with AlphaFold
|
| 425 |
-
x_axis = origin - neg_x_axis
|
| 426 |
-
xy_plane = xy_plane - origin
|
| 427 |
-
return Affine3D(
|
| 428 |
-
trans=origin, rot=RotationMatrix.from_graham_schmidt(x_axis, xy_plane, eps)
|
| 429 |
-
)
|
| 430 |
-
|
| 431 |
-
@staticmethod
|
| 432 |
-
def cat(affines: list["Affine3D"], dim: int = 0):
|
| 433 |
-
if dim < 0:
|
| 434 |
-
dim = len(affines[0].shape) + dim
|
| 435 |
-
return Affine3D.from_tensor(torch.cat([x.tensor for x in affines], dim=dim))
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
def _quat_mult(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 439 |
-
"""
|
| 440 |
-
Multiply two quaternions.
|
| 441 |
-
Usual torch rules for broadcasting apply.
|
| 442 |
-
|
| 443 |
-
Args:
|
| 444 |
-
a: Quaternions as tensor of shape (..., 4), real part first.
|
| 445 |
-
b: Quaternions as tensor of shape (..., 4), real part first.
|
| 446 |
-
|
| 447 |
-
Returns:
|
| 448 |
-
The product of a and b, a tensor of quaternions shape (..., 4).
|
| 449 |
-
"""
|
| 450 |
-
aw, ax, ay, az = torch.unbind(a, -1)
|
| 451 |
-
bw, bx, by, bz = torch.unbind(b, -1)
|
| 452 |
-
ow = aw * bw - ax * bx - ay * by - az * bz
|
| 453 |
-
ox = aw * bx + ax * bw + ay * bz - az * by
|
| 454 |
-
oy = aw * by - ax * bz + ay * bw + az * bx
|
| 455 |
-
oz = aw * bz + ax * by - ay * bx + az * bw
|
| 456 |
-
return torch.stack((ow, ox, oy, oz), -1)
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
def _quat_rotation(q: torch.Tensor, p: torch.Tensor) -> torch.Tensor:
|
| 460 |
-
"""
|
| 461 |
-
Rotates p by quaternion q. Usual torch rules for broadcasting apply.
|
| 462 |
-
|
| 463 |
-
Args:
|
| 464 |
-
q: Quaternions as tensor of shape (..., 4), real part first.
|
| 465 |
-
p: Points as tensor of shape (..., 3)
|
| 466 |
-
|
| 467 |
-
Returns:
|
| 468 |
-
The rotated version of p, of shape (..., 3)
|
| 469 |
-
"""
|
| 470 |
-
aw, ax, ay, az = torch.unbind(q, -1)
|
| 471 |
-
bx, by, bz = torch.unbind(p, -1)
|
| 472 |
-
# fmt: off
|
| 473 |
-
ow = - ax * bx - ay * by - az * bz
|
| 474 |
-
ox = aw * bx + ay * bz - az * by
|
| 475 |
-
oy = aw * by - ax * bz + az * bx
|
| 476 |
-
oz = aw * bz + ax * by - ay * bx
|
| 477 |
-
# fmt: on
|
| 478 |
-
q_mul_pts = torch.stack((ow, ox, oy, oz), -1)
|
| 479 |
-
return _quat_mult(q_mul_pts, _quat_invert(q))[..., 1:]
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
def _quat_invert(q: torch.Tensor):
|
| 483 |
-
return q * torch.tensor([1, -1, -1, -1], device=q.device)
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
def _sqrt_subgradient(x: torch.Tensor) -> torch.Tensor:
|
| 487 |
-
# Returns torch.sqrt(torch.max(0, x)) but with a zero subgradient where x is 0.
|
| 488 |
-
ret = torch.zeros_like(x)
|
| 489 |
-
positive_mask = x > 0
|
| 490 |
-
ret[positive_mask] = torch.sqrt(x[positive_mask])
|
| 491 |
-
return ret
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
def _graham_schmidt(x_axis: torch.Tensor, xy_plane: torch.Tensor, eps: float = 1e-12):
|
| 495 |
-
# A low eps here is necessary for good stability!
|
| 496 |
-
with fp32_autocast_context(x_axis.device.type):
|
| 497 |
-
e1 = xy_plane
|
| 498 |
-
|
| 499 |
-
denom = torch.sqrt((x_axis**2).sum(dim=-1, keepdim=True) + eps)
|
| 500 |
-
x_axis = x_axis / denom
|
| 501 |
-
dot = (x_axis * e1).sum(dim=-1, keepdim=True)
|
| 502 |
-
e1 = e1 - x_axis * dot
|
| 503 |
-
denom = torch.sqrt((e1**2).sum(dim=-1, keepdim=True) + eps)
|
| 504 |
-
e1 = e1 / denom
|
| 505 |
-
e2 = torch.cross(x_axis, e1, dim=-1)
|
| 506 |
-
|
| 507 |
-
rots = torch.stack([x_axis, e1, e2], dim=-1)
|
| 508 |
-
|
| 509 |
-
return rots
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
def build_affine3d_from_coordinates(
|
| 513 |
-
coords: torch.Tensor, # (N, CA, C).
|
| 514 |
-
) -> tuple[Affine3D, torch.Tensor]:
|
| 515 |
-
_MAX_SUPPORTED_DISTANCE = 1e6
|
| 516 |
-
coord_mask = torch.all(
|
| 517 |
-
torch.all(torch.isfinite(coords) & (coords < _MAX_SUPPORTED_DISTANCE), dim=-1),
|
| 518 |
-
dim=-1,
|
| 519 |
-
)
|
| 520 |
-
|
| 521 |
-
def atom3_to_backbone_affine(bb_positions: torch.Tensor) -> Affine3D:
|
| 522 |
-
N, CA, C = bb_positions.unbind(dim=-2)
|
| 523 |
-
return Affine3D.from_graham_schmidt(C, CA, N)
|
| 524 |
-
|
| 525 |
-
coords = coords.clone().float()
|
| 526 |
-
coords[~coord_mask] = 0
|
| 527 |
-
|
| 528 |
-
# NOTE(thayes): If you have already normalized the coordinates, then
|
| 529 |
-
# the black hole affine translations will be zeros and the rotations will be
|
| 530 |
-
# the identity.
|
| 531 |
-
average_per_n_ca_c = coords.masked_fill(~coord_mask[..., None, None], 0).sum(1) / (
|
| 532 |
-
coord_mask.sum(-1)[..., None, None] + 1e-8
|
| 533 |
-
)
|
| 534 |
-
affine_from_average = atom3_to_backbone_affine(
|
| 535 |
-
average_per_n_ca_c.float()
|
| 536 |
-
).as_matrix()
|
| 537 |
-
|
| 538 |
-
B, S, _, _ = coords.shape
|
| 539 |
-
assert isinstance(B, int)
|
| 540 |
-
assert isinstance(S, int)
|
| 541 |
-
affine_rot_mats = affine_from_average.rot.tensor[..., None, :].expand(B, S, 9)
|
| 542 |
-
affine_trans = affine_from_average.trans[..., None, :].expand(B, S, 3)
|
| 543 |
-
|
| 544 |
-
# We use the identity rotation whereever we have no coordinates. This is
|
| 545 |
-
# important because otherwise the rotation matrices will be all zeros, which
|
| 546 |
-
# will cause collapse in the distance/direction attention mechanism.
|
| 547 |
-
identity_rot = RotationMatrix.identity(
|
| 548 |
-
(B, S), dtype=torch.float32, device=coords.device, requires_grad=False
|
| 549 |
-
)
|
| 550 |
-
affine_rot_mats = affine_rot_mats.where(
|
| 551 |
-
coord_mask.any(-1)[..., None, None], identity_rot.tensor
|
| 552 |
-
)
|
| 553 |
-
black_hole_affine = Affine3D(affine_trans, RotationMatrix(affine_rot_mats))
|
| 554 |
-
|
| 555 |
-
affine = atom3_to_backbone_affine(coords.float())
|
| 556 |
-
affine = Affine3D.from_tensor(
|
| 557 |
-
affine.tensor.where(coord_mask[..., None], black_hole_affine.tensor)
|
| 558 |
-
)
|
| 559 |
-
|
| 560 |
-
return affine, coord_mask
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing as T
|
| 4 |
+
from abc import ABC
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch.nn import functional as F
|
| 9 |
+
from typing_extensions import Self
|
| 10 |
+
|
| 11 |
+
from .esmfold2_misc import fp32_autocast_context
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Rotation(ABC):
|
| 15 |
+
@classmethod
|
| 16 |
+
def identity(cls, shape: tuple[int, ...], **tensor_kwargs) -> Self: ...
|
| 17 |
+
|
| 18 |
+
@classmethod
|
| 19 |
+
def random(cls, shape: tuple[int, ...], **tensor_kwargs) -> Self: ...
|
| 20 |
+
|
| 21 |
+
def __getitem__(self, idx: T.Any) -> Self: ...
|
| 22 |
+
|
| 23 |
+
@property
|
| 24 |
+
def tensor(self) -> torch.Tensor:
|
| 25 |
+
# We claim that this should be zero-cost abstraction that returns the raw tensor backing this
|
| 26 |
+
# object. The raw tensor should always have exactly 1 more dim than self.shape, which should be
|
| 27 |
+
# implemented using reshaping
|
| 28 |
+
...
|
| 29 |
+
|
| 30 |
+
@property
|
| 31 |
+
def shape(self) -> torch.Size:
|
| 32 |
+
# The "shape" of the rotation, as if it was a torch.tensor object
|
| 33 |
+
# This means that 1x4 quaternions are treated as size (1,) for example
|
| 34 |
+
...
|
| 35 |
+
|
| 36 |
+
def as_matrix(self) -> RotationMatrix: ...
|
| 37 |
+
|
| 38 |
+
def as_quat(self, normalize: bool = False) -> RotationQuat: ...
|
| 39 |
+
|
| 40 |
+
def compose(self, other: Self) -> Self:
|
| 41 |
+
# To be safe, we force users to explicitly convert between rotation types.
|
| 42 |
+
...
|
| 43 |
+
|
| 44 |
+
def convert_compose(self, other: Self) -> Self:
|
| 45 |
+
# This function will automatically convert between types of rotations
|
| 46 |
+
...
|
| 47 |
+
|
| 48 |
+
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 49 |
+
# rotates points by this rotation object
|
| 50 |
+
...
|
| 51 |
+
|
| 52 |
+
def invert(self) -> Self: ...
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def dtype(self) -> torch.dtype:
|
| 56 |
+
return self.tensor.dtype
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def device(self) -> torch.device:
|
| 60 |
+
return self.tensor.device
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def requires_grad(self) -> bool:
|
| 64 |
+
return self.tensor.requires_grad
|
| 65 |
+
|
| 66 |
+
@classmethod
|
| 67 |
+
def _from_tensor(cls, t: torch.Tensor) -> Self:
|
| 68 |
+
# This function exists to simplify the below functions, esp type signatures
|
| 69 |
+
# Its implementation is different from Affine3D.from_tensor and does not
|
| 70 |
+
# autodetect rotation types.
|
| 71 |
+
return cls(t) # type: ignore
|
| 72 |
+
|
| 73 |
+
def to(self, **kwargs) -> Self:
|
| 74 |
+
return self._from_tensor(self.tensor.to(**kwargs))
|
| 75 |
+
|
| 76 |
+
def detach(self, *args, **kwargs) -> Self:
|
| 77 |
+
return self._from_tensor(self.tensor.detach(**kwargs))
|
| 78 |
+
|
| 79 |
+
def tensor_apply(self, func) -> Self:
|
| 80 |
+
# Applys a function to the underlying tensor
|
| 81 |
+
return self._from_tensor(
|
| 82 |
+
torch.stack([func(x) for x in self.tensor.unbind(dim=-1)], dim=-1)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class RotationMatrix(Rotation):
|
| 87 |
+
def __init__(self, rots: torch.Tensor):
|
| 88 |
+
if rots.shape[-1] == 9:
|
| 89 |
+
rots = rots.unflatten(-1, (3, 3))
|
| 90 |
+
assert rots.shape[-1] == 3
|
| 91 |
+
assert rots.shape[-2] == 3
|
| 92 |
+
# Force full precision
|
| 93 |
+
rots = rots.to(torch.float32)
|
| 94 |
+
self._rots = rots
|
| 95 |
+
|
| 96 |
+
@classmethod
|
| 97 |
+
def identity(cls, shape, **tensor_kwargs):
|
| 98 |
+
rots = torch.eye(3, **tensor_kwargs)
|
| 99 |
+
rots = rots.view(*[1 for _ in range(len(shape))], 3, 3)
|
| 100 |
+
rots = rots.expand(*shape, -1, -1)
|
| 101 |
+
return cls(rots)
|
| 102 |
+
|
| 103 |
+
@classmethod
|
| 104 |
+
def random(cls, shape, **tensor_kwargs):
|
| 105 |
+
return RotationQuat.random(shape, **tensor_kwargs).as_matrix()
|
| 106 |
+
|
| 107 |
+
def __getitem__(self, idx: T.Any) -> RotationMatrix:
|
| 108 |
+
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 109 |
+
return RotationMatrix(self._rots[indices + (slice(None), slice(None))])
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def shape(self) -> torch.Size:
|
| 113 |
+
return self._rots.shape[:-2]
|
| 114 |
+
|
| 115 |
+
def as_matrix(self) -> RotationMatrix:
|
| 116 |
+
return self
|
| 117 |
+
|
| 118 |
+
def as_quat(self, normalize: bool = False) -> RotationQuat:
|
| 119 |
+
m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
|
| 120 |
+
self._rots.flatten(-2), dim=-1
|
| 121 |
+
)
|
| 122 |
+
q_abs = _sqrt_subgradient(
|
| 123 |
+
torch.stack(
|
| 124 |
+
[
|
| 125 |
+
1.0 + m00 + m11 + m22,
|
| 126 |
+
1.0 + m00 - m11 - m22,
|
| 127 |
+
1.0 - m00 + m11 - m22,
|
| 128 |
+
1.0 - m00 - m11 + m22,
|
| 129 |
+
],
|
| 130 |
+
dim=-1,
|
| 131 |
+
)
|
| 132 |
+
)
|
| 133 |
+
# we produce the desired quaternion multiplied by each of r, i, j, k
|
| 134 |
+
quat_by_rijk = torch.stack(
|
| 135 |
+
[
|
| 136 |
+
x
|
| 137 |
+
for lst in [
|
| 138 |
+
[q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01],
|
| 139 |
+
[m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20],
|
| 140 |
+
[m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21],
|
| 141 |
+
[m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2],
|
| 142 |
+
]
|
| 143 |
+
for x in lst
|
| 144 |
+
],
|
| 145 |
+
dim=-1,
|
| 146 |
+
).unflatten(-1, (4, 4))
|
| 147 |
+
|
| 148 |
+
# We floor here at 0.1 but the exact level is not important; if q_abs is small,
|
| 149 |
+
# the candidate won't be picked.
|
| 150 |
+
flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
|
| 151 |
+
quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
|
| 152 |
+
|
| 153 |
+
# if not for numerical problems, quat_candidates[i] should be same (up to a sign),
|
| 154 |
+
# forall i; we pick the best-conditioned one (with the largest denominator)
|
| 155 |
+
# We manually implement one_hot so torch.compile works
|
| 156 |
+
one_hot = torch.zeros_like(q_abs, dtype=torch.bool)
|
| 157 |
+
one_hot.scatter_(-1, q_abs.argmax(dim=-1, keepdim=True), True)
|
| 158 |
+
quat = quat_candidates[one_hot, :].reshape(q_abs.shape)
|
| 159 |
+
return RotationQuat(quat)
|
| 160 |
+
|
| 161 |
+
def compose(self, other: RotationMatrix) -> RotationMatrix:
|
| 162 |
+
with fp32_autocast_context(self._rots.device.type):
|
| 163 |
+
return RotationMatrix(self._rots @ other._rots)
|
| 164 |
+
|
| 165 |
+
def convert_compose(self, other: Rotation):
|
| 166 |
+
return self.compose(other.as_matrix())
|
| 167 |
+
|
| 168 |
+
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 169 |
+
with fp32_autocast_context(self.device.type):
|
| 170 |
+
if self._rots.shape[-3] == 1:
|
| 171 |
+
# This is a slight speedup over einsum for batched rotations
|
| 172 |
+
return p @ self._rots.transpose(-1, -2).squeeze(-3)
|
| 173 |
+
else:
|
| 174 |
+
# einsum way faster than bmm!
|
| 175 |
+
return torch.einsum("...ij,...j", self._rots, p)
|
| 176 |
+
|
| 177 |
+
def invert(self) -> RotationMatrix:
|
| 178 |
+
return RotationMatrix(self._rots.transpose(-1, -2))
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def tensor(self) -> torch.Tensor:
|
| 182 |
+
return self._rots.flatten(-2)
|
| 183 |
+
|
| 184 |
+
def to_3x3(self) -> torch.Tensor:
|
| 185 |
+
return self._rots
|
| 186 |
+
|
| 187 |
+
@staticmethod
|
| 188 |
+
def from_graham_schmidt(
|
| 189 |
+
x_axis: torch.Tensor, xy_plane: torch.Tensor, eps: float = 1e-12
|
| 190 |
+
) -> RotationMatrix:
|
| 191 |
+
# A low eps here is necessary for good stability!
|
| 192 |
+
return RotationMatrix(_graham_schmidt(x_axis, xy_plane, eps))
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class RotationQuat(Rotation):
|
| 196 |
+
def __init__(self, quats: torch.Tensor, normalized=False):
|
| 197 |
+
assert quats.shape[-1] == 4
|
| 198 |
+
self._normalized = normalized
|
| 199 |
+
# Force float32 as well
|
| 200 |
+
if normalized:
|
| 201 |
+
self._quats = F.normalize(quats.to(torch.float32), dim=-1)
|
| 202 |
+
self._quats = self._quats.where(self._quats[..., :1] >= 0, -self._quats)
|
| 203 |
+
else:
|
| 204 |
+
self._quats = quats.to(torch.float32)
|
| 205 |
+
|
| 206 |
+
@classmethod
|
| 207 |
+
def identity(cls, shape, **tensor_kwargs):
|
| 208 |
+
q = torch.ones((*shape, 4), **tensor_kwargs)
|
| 209 |
+
mult = torch.tensor([1, 0, 0, 0], device=q.device)
|
| 210 |
+
return RotationQuat(q * mult)
|
| 211 |
+
|
| 212 |
+
@classmethod
|
| 213 |
+
def random(cls, shape, **tensor_kwargs):
|
| 214 |
+
quat = torch.randn((*shape, 4), **tensor_kwargs)
|
| 215 |
+
return RotationQuat(quat, normalized=True)
|
| 216 |
+
|
| 217 |
+
def __getitem__(self, idx: T.Any) -> RotationQuat:
|
| 218 |
+
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 219 |
+
return RotationQuat(self._quats[indices + (slice(None),)])
|
| 220 |
+
|
| 221 |
+
@property
|
| 222 |
+
def shape(self) -> torch.Size:
|
| 223 |
+
return self._quats.shape[:-1]
|
| 224 |
+
|
| 225 |
+
def compose(self, other: RotationQuat) -> RotationQuat:
|
| 226 |
+
with fp32_autocast_context(self._quats.device.type):
|
| 227 |
+
return RotationQuat(_quat_mult(self._quats, other._quats))
|
| 228 |
+
|
| 229 |
+
def convert_compose(self, other: Rotation):
|
| 230 |
+
return self.compose(other.as_quat())
|
| 231 |
+
|
| 232 |
+
def as_matrix(self) -> RotationMatrix:
|
| 233 |
+
q = self.normalized().tensor
|
| 234 |
+
r, i, j, k = torch.unbind(q, -1)
|
| 235 |
+
two_s = 2.0 / torch.linalg.norm(q, dim=-1)
|
| 236 |
+
|
| 237 |
+
o = torch.stack(
|
| 238 |
+
(
|
| 239 |
+
1 - two_s * (j * j + k * k),
|
| 240 |
+
two_s * (i * j - k * r),
|
| 241 |
+
two_s * (i * k + j * r),
|
| 242 |
+
two_s * (i * j + k * r),
|
| 243 |
+
1 - two_s * (i * i + k * k),
|
| 244 |
+
two_s * (j * k - i * r),
|
| 245 |
+
two_s * (i * k - j * r),
|
| 246 |
+
two_s * (j * k + i * r),
|
| 247 |
+
1 - two_s * (i * i + j * j),
|
| 248 |
+
),
|
| 249 |
+
-1,
|
| 250 |
+
)
|
| 251 |
+
return RotationMatrix(o.reshape(q.shape[:-1] + (3, 3)))
|
| 252 |
+
|
| 253 |
+
def as_quat(self, normalize: bool = False) -> RotationQuat:
|
| 254 |
+
return self
|
| 255 |
+
|
| 256 |
+
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 257 |
+
return _quat_rotation(self.normalized()._quats, p)
|
| 258 |
+
|
| 259 |
+
def invert(self) -> RotationQuat:
|
| 260 |
+
return RotationQuat(_quat_invert(self._quats))
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def tensor(self) -> torch.Tensor:
|
| 264 |
+
return self._quats
|
| 265 |
+
|
| 266 |
+
def normalized(self) -> RotationQuat:
|
| 267 |
+
return self if self._normalized else RotationQuat(self._quats, normalized=True)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@dataclass(frozen=True)
|
| 271 |
+
class Affine3D:
|
| 272 |
+
trans: torch.Tensor
|
| 273 |
+
rot: Rotation
|
| 274 |
+
|
| 275 |
+
def __post_init__(self):
|
| 276 |
+
assert self.trans.shape[:-1] == self.rot.shape
|
| 277 |
+
|
| 278 |
+
@staticmethod
|
| 279 |
+
def identity(
|
| 280 |
+
shape_or_affine: T.Union[tuple[int, ...], "Affine3D"],
|
| 281 |
+
rotation_type: T.Type[Rotation] = RotationMatrix,
|
| 282 |
+
**tensor_kwargs,
|
| 283 |
+
):
|
| 284 |
+
# Creates a new identity Affine3D object with a specified shape
|
| 285 |
+
# or the same shape as another Affine3D object.
|
| 286 |
+
if isinstance(shape_or_affine, Affine3D):
|
| 287 |
+
kwargs = {"dtype": shape_or_affine.dtype, "device": shape_or_affine.device}
|
| 288 |
+
kwargs.update(tensor_kwargs)
|
| 289 |
+
shape = shape_or_affine.shape
|
| 290 |
+
rotation_type = type(shape_or_affine.rot)
|
| 291 |
+
else:
|
| 292 |
+
kwargs = tensor_kwargs
|
| 293 |
+
shape = shape_or_affine
|
| 294 |
+
return Affine3D(
|
| 295 |
+
torch.zeros((*shape, 3), **kwargs), rotation_type.identity(shape, **kwargs)
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
@staticmethod
|
| 299 |
+
def random(
|
| 300 |
+
shape: tuple[int, ...],
|
| 301 |
+
std: float = 1,
|
| 302 |
+
rotation_type: T.Type[Rotation] = RotationMatrix,
|
| 303 |
+
**tensor_kwargs,
|
| 304 |
+
) -> "Affine3D":
|
| 305 |
+
return Affine3D(
|
| 306 |
+
trans=torch.randn((*shape, 3), **tensor_kwargs).mul(std),
|
| 307 |
+
rot=rotation_type.random(shape, **tensor_kwargs),
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
def __getitem__(self, idx: T.Any) -> "Affine3D":
|
| 311 |
+
indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
|
| 312 |
+
return Affine3D(trans=self.trans[indices + (slice(None),)], rot=self.rot[idx])
|
| 313 |
+
|
| 314 |
+
@property
|
| 315 |
+
def shape(self) -> torch.Size:
|
| 316 |
+
return self.trans.shape[:-1]
|
| 317 |
+
|
| 318 |
+
@property
|
| 319 |
+
def dtype(self) -> torch.dtype:
|
| 320 |
+
return self.trans.dtype
|
| 321 |
+
|
| 322 |
+
@property
|
| 323 |
+
def device(self) -> torch.device:
|
| 324 |
+
return self.trans.device
|
| 325 |
+
|
| 326 |
+
@property
|
| 327 |
+
def requires_grad(self) -> bool:
|
| 328 |
+
return self.trans.requires_grad
|
| 329 |
+
|
| 330 |
+
def to(self, **kwargs) -> "Affine3D":
|
| 331 |
+
return Affine3D(self.trans.to(**kwargs), self.rot.to(**kwargs))
|
| 332 |
+
|
| 333 |
+
def detach(self, *args, **kwargs) -> "Affine3D":
|
| 334 |
+
return Affine3D(self.trans.detach(**kwargs), self.rot.detach(**kwargs))
|
| 335 |
+
|
| 336 |
+
def tensor_apply(self, func) -> "Affine3D":
|
| 337 |
+
# Applys a function to the underlying tensor
|
| 338 |
+
return self.from_tensor(
|
| 339 |
+
torch.stack([func(x) for x in self.tensor.unbind(dim=-1)], dim=-1)
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
def as_matrix(self):
|
| 343 |
+
return Affine3D(trans=self.trans, rot=self.rot.as_matrix())
|
| 344 |
+
|
| 345 |
+
def as_quat(self, normalize: bool = False):
|
| 346 |
+
return Affine3D(trans=self.trans, rot=self.rot.as_quat(normalize))
|
| 347 |
+
|
| 348 |
+
def compose(self, other: "Affine3D", autoconvert: bool = False):
|
| 349 |
+
rot = self.rot
|
| 350 |
+
new_rot = (rot.convert_compose if autoconvert else rot.compose)(other.rot)
|
| 351 |
+
new_trans = rot.apply(other.trans) + self.trans
|
| 352 |
+
return Affine3D(trans=new_trans, rot=new_rot)
|
| 353 |
+
|
| 354 |
+
def compose_rotation(self, other: Rotation, autoconvert: bool = False):
|
| 355 |
+
return Affine3D(
|
| 356 |
+
trans=self.trans,
|
| 357 |
+
rot=(self.rot.convert_compose if autoconvert else self.rot.compose)(other),
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
def scale(self, v: torch.Tensor | float):
|
| 361 |
+
return Affine3D(self.trans * v, self.rot)
|
| 362 |
+
|
| 363 |
+
def mask(self, mask: torch.Tensor, with_zero=False):
|
| 364 |
+
# Returns a transform where True positions in mask is identity
|
| 365 |
+
if with_zero:
|
| 366 |
+
tensor = self.tensor
|
| 367 |
+
return Affine3D.from_tensor(
|
| 368 |
+
torch.zeros_like(tensor).where(mask[..., None], tensor)
|
| 369 |
+
)
|
| 370 |
+
else:
|
| 371 |
+
identity = self.identity(
|
| 372 |
+
self.shape,
|
| 373 |
+
rotation_type=type(self.rot),
|
| 374 |
+
device=self.device,
|
| 375 |
+
dtype=self.dtype,
|
| 376 |
+
).tensor
|
| 377 |
+
return Affine3D.from_tensor(identity.where(mask[..., None], self.tensor))
|
| 378 |
+
|
| 379 |
+
def apply(self, p: torch.Tensor) -> torch.Tensor:
|
| 380 |
+
return self.rot.apply(p) + self.trans
|
| 381 |
+
|
| 382 |
+
def invert(self):
|
| 383 |
+
inv_rot = self.rot.invert()
|
| 384 |
+
return Affine3D(trans=-inv_rot.apply(self.trans), rot=inv_rot)
|
| 385 |
+
|
| 386 |
+
@property
|
| 387 |
+
def tensor(self) -> torch.Tensor:
|
| 388 |
+
return torch.cat([self.rot.tensor, self.trans], dim=-1)
|
| 389 |
+
|
| 390 |
+
@staticmethod
|
| 391 |
+
def from_tensor(t: torch.Tensor) -> "Affine3D":
|
| 392 |
+
match t.shape[-1]:
|
| 393 |
+
case 4:
|
| 394 |
+
# Assume tensor 4x4 for backward compat with alphafold
|
| 395 |
+
trans = t[..., :3, 3]
|
| 396 |
+
rot = RotationMatrix(t[..., :3, :3])
|
| 397 |
+
case 6:
|
| 398 |
+
# Assume quaternion representation with real part = 1
|
| 399 |
+
trans = t[..., -3:]
|
| 400 |
+
rot = RotationQuat(F.pad(t[..., :3], (1, 0), value=1))
|
| 401 |
+
case 7:
|
| 402 |
+
trans = t[..., -3:]
|
| 403 |
+
rot = RotationQuat(t[..., :4])
|
| 404 |
+
case 12:
|
| 405 |
+
trans = t[..., -3:]
|
| 406 |
+
rot = RotationMatrix(t[..., :-3].unflatten(-1, (3, 3)))
|
| 407 |
+
case _:
|
| 408 |
+
raise RuntimeError(
|
| 409 |
+
f"Cannot detect rotation fromat from {t.shape[-1] -3}-d flat vector"
|
| 410 |
+
)
|
| 411 |
+
return Affine3D(trans, rot)
|
| 412 |
+
|
| 413 |
+
@staticmethod
|
| 414 |
+
def from_tensor_pair(t: torch.Tensor, r: torch.Tensor) -> "Affine3D":
|
| 415 |
+
return Affine3D(t, RotationMatrix(r))
|
| 416 |
+
|
| 417 |
+
@staticmethod
|
| 418 |
+
def from_graham_schmidt(
|
| 419 |
+
neg_x_axis: torch.Tensor,
|
| 420 |
+
origin: torch.Tensor,
|
| 421 |
+
xy_plane: torch.Tensor,
|
| 422 |
+
eps: float = 1e-10,
|
| 423 |
+
):
|
| 424 |
+
# The arguments of this function is for parity with AlphaFold
|
| 425 |
+
x_axis = origin - neg_x_axis
|
| 426 |
+
xy_plane = xy_plane - origin
|
| 427 |
+
return Affine3D(
|
| 428 |
+
trans=origin, rot=RotationMatrix.from_graham_schmidt(x_axis, xy_plane, eps)
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
@staticmethod
|
| 432 |
+
def cat(affines: list["Affine3D"], dim: int = 0):
|
| 433 |
+
if dim < 0:
|
| 434 |
+
dim = len(affines[0].shape) + dim
|
| 435 |
+
return Affine3D.from_tensor(torch.cat([x.tensor for x in affines], dim=dim))
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def _quat_mult(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 439 |
+
"""
|
| 440 |
+
Multiply two quaternions.
|
| 441 |
+
Usual torch rules for broadcasting apply.
|
| 442 |
+
|
| 443 |
+
Args:
|
| 444 |
+
a: Quaternions as tensor of shape (..., 4), real part first.
|
| 445 |
+
b: Quaternions as tensor of shape (..., 4), real part first.
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
The product of a and b, a tensor of quaternions shape (..., 4).
|
| 449 |
+
"""
|
| 450 |
+
aw, ax, ay, az = torch.unbind(a, -1)
|
| 451 |
+
bw, bx, by, bz = torch.unbind(b, -1)
|
| 452 |
+
ow = aw * bw - ax * bx - ay * by - az * bz
|
| 453 |
+
ox = aw * bx + ax * bw + ay * bz - az * by
|
| 454 |
+
oy = aw * by - ax * bz + ay * bw + az * bx
|
| 455 |
+
oz = aw * bz + ax * by - ay * bx + az * bw
|
| 456 |
+
return torch.stack((ow, ox, oy, oz), -1)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def _quat_rotation(q: torch.Tensor, p: torch.Tensor) -> torch.Tensor:
|
| 460 |
+
"""
|
| 461 |
+
Rotates p by quaternion q. Usual torch rules for broadcasting apply.
|
| 462 |
+
|
| 463 |
+
Args:
|
| 464 |
+
q: Quaternions as tensor of shape (..., 4), real part first.
|
| 465 |
+
p: Points as tensor of shape (..., 3)
|
| 466 |
+
|
| 467 |
+
Returns:
|
| 468 |
+
The rotated version of p, of shape (..., 3)
|
| 469 |
+
"""
|
| 470 |
+
aw, ax, ay, az = torch.unbind(q, -1)
|
| 471 |
+
bx, by, bz = torch.unbind(p, -1)
|
| 472 |
+
# fmt: off
|
| 473 |
+
ow = - ax * bx - ay * by - az * bz
|
| 474 |
+
ox = aw * bx + ay * bz - az * by
|
| 475 |
+
oy = aw * by - ax * bz + az * bx
|
| 476 |
+
oz = aw * bz + ax * by - ay * bx
|
| 477 |
+
# fmt: on
|
| 478 |
+
q_mul_pts = torch.stack((ow, ox, oy, oz), -1)
|
| 479 |
+
return _quat_mult(q_mul_pts, _quat_invert(q))[..., 1:]
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def _quat_invert(q: torch.Tensor):
|
| 483 |
+
return q * torch.tensor([1, -1, -1, -1], device=q.device)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def _sqrt_subgradient(x: torch.Tensor) -> torch.Tensor:
|
| 487 |
+
# Returns torch.sqrt(torch.max(0, x)) but with a zero subgradient where x is 0.
|
| 488 |
+
ret = torch.zeros_like(x)
|
| 489 |
+
positive_mask = x > 0
|
| 490 |
+
ret[positive_mask] = torch.sqrt(x[positive_mask])
|
| 491 |
+
return ret
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def _graham_schmidt(x_axis: torch.Tensor, xy_plane: torch.Tensor, eps: float = 1e-12):
|
| 495 |
+
# A low eps here is necessary for good stability!
|
| 496 |
+
with fp32_autocast_context(x_axis.device.type):
|
| 497 |
+
e1 = xy_plane
|
| 498 |
+
|
| 499 |
+
denom = torch.sqrt((x_axis**2).sum(dim=-1, keepdim=True) + eps)
|
| 500 |
+
x_axis = x_axis / denom
|
| 501 |
+
dot = (x_axis * e1).sum(dim=-1, keepdim=True)
|
| 502 |
+
e1 = e1 - x_axis * dot
|
| 503 |
+
denom = torch.sqrt((e1**2).sum(dim=-1, keepdim=True) + eps)
|
| 504 |
+
e1 = e1 / denom
|
| 505 |
+
e2 = torch.cross(x_axis, e1, dim=-1)
|
| 506 |
+
|
| 507 |
+
rots = torch.stack([x_axis, e1, e2], dim=-1)
|
| 508 |
+
|
| 509 |
+
return rots
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def build_affine3d_from_coordinates(
|
| 513 |
+
coords: torch.Tensor, # (N, CA, C).
|
| 514 |
+
) -> tuple[Affine3D, torch.Tensor]:
|
| 515 |
+
_MAX_SUPPORTED_DISTANCE = 1e6
|
| 516 |
+
coord_mask = torch.all(
|
| 517 |
+
torch.all(torch.isfinite(coords) & (coords < _MAX_SUPPORTED_DISTANCE), dim=-1),
|
| 518 |
+
dim=-1,
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
def atom3_to_backbone_affine(bb_positions: torch.Tensor) -> Affine3D:
|
| 522 |
+
N, CA, C = bb_positions.unbind(dim=-2)
|
| 523 |
+
return Affine3D.from_graham_schmidt(C, CA, N)
|
| 524 |
+
|
| 525 |
+
coords = coords.clone().float()
|
| 526 |
+
coords[~coord_mask] = 0
|
| 527 |
+
|
| 528 |
+
# NOTE(thayes): If you have already normalized the coordinates, then
|
| 529 |
+
# the black hole affine translations will be zeros and the rotations will be
|
| 530 |
+
# the identity.
|
| 531 |
+
average_per_n_ca_c = coords.masked_fill(~coord_mask[..., None, None], 0).sum(1) / (
|
| 532 |
+
coord_mask.sum(-1)[..., None, None] + 1e-8
|
| 533 |
+
)
|
| 534 |
+
affine_from_average = atom3_to_backbone_affine(
|
| 535 |
+
average_per_n_ca_c.float()
|
| 536 |
+
).as_matrix()
|
| 537 |
+
|
| 538 |
+
B, S, _, _ = coords.shape
|
| 539 |
+
assert isinstance(B, int)
|
| 540 |
+
assert isinstance(S, int)
|
| 541 |
+
affine_rot_mats = affine_from_average.rot.tensor[..., None, :].expand(B, S, 9)
|
| 542 |
+
affine_trans = affine_from_average.trans[..., None, :].expand(B, S, 3)
|
| 543 |
+
|
| 544 |
+
# We use the identity rotation whereever we have no coordinates. This is
|
| 545 |
+
# important because otherwise the rotation matrices will be all zeros, which
|
| 546 |
+
# will cause collapse in the distance/direction attention mechanism.
|
| 547 |
+
identity_rot = RotationMatrix.identity(
|
| 548 |
+
(B, S), dtype=torch.float32, device=coords.device, requires_grad=False
|
| 549 |
+
)
|
| 550 |
+
affine_rot_mats = affine_rot_mats.where(
|
| 551 |
+
coord_mask.any(-1)[..., None, None], identity_rot.tensor
|
| 552 |
+
)
|
| 553 |
+
black_hole_affine = Affine3D(affine_trans, RotationMatrix(affine_rot_mats))
|
| 554 |
+
|
| 555 |
+
affine = atom3_to_backbone_affine(coords.float())
|
| 556 |
+
affine = Affine3D.from_tensor(
|
| 557 |
+
affine.tensor.where(coord_mask[..., None], black_hole_affine.tensor)
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
return affine, coord_mask
|
esmfold2_aligner.py
CHANGED
|
@@ -1,101 +1,101 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
from dataclasses import Field, replace
|
| 4 |
-
from typing import Any, ClassVar, Protocol, TypeVar
|
| 5 |
-
|
| 6 |
-
import numpy as np
|
| 7 |
-
import torch
|
| 8 |
-
|
| 9 |
-
from .esmfold2_protein_structure import compute_affine_and_rmsd
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class Alignable(Protocol):
|
| 13 |
-
# Trick to detect whether an object is a dataclass
|
| 14 |
-
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
| 15 |
-
|
| 16 |
-
@property
|
| 17 |
-
def atom37_positions(self) -> np.ndarray: # type: ignore
|
| 18 |
-
pass
|
| 19 |
-
|
| 20 |
-
@property
|
| 21 |
-
def atom37_mask(self) -> np.ndarray: # type: ignore
|
| 22 |
-
pass
|
| 23 |
-
|
| 24 |
-
def __len__(self) -> int: ...
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
T = TypeVar("T", bound=Alignable)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
class Aligner:
|
| 31 |
-
def __init__(
|
| 32 |
-
self,
|
| 33 |
-
mobile: Alignable,
|
| 34 |
-
target: Alignable,
|
| 35 |
-
only_use_backbone: bool = False,
|
| 36 |
-
use_reflection: bool = False,
|
| 37 |
-
):
|
| 38 |
-
"""
|
| 39 |
-
Aligns a mobile protein chain against a target protein chain.
|
| 40 |
-
|
| 41 |
-
Args:
|
| 42 |
-
mobile (ProteinChain): Protein chain to be aligned.
|
| 43 |
-
target (ProteinChain): Protein chain target.
|
| 44 |
-
only_use_backbone (bool): Whether to only use backbone atoms.
|
| 45 |
-
use_reflection (bool): Whether to align to target reflection.
|
| 46 |
-
"""
|
| 47 |
-
# Check proteins must have same number of residues
|
| 48 |
-
assert len(mobile) == len(target)
|
| 49 |
-
|
| 50 |
-
# Determine overlapping atoms
|
| 51 |
-
joint_atom37_mask = mobile.atom37_mask.astype(bool) & target.atom37_mask.astype(
|
| 52 |
-
bool
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
# Backbone atoms are first sites in atom37 representation
|
| 56 |
-
if only_use_backbone:
|
| 57 |
-
joint_atom37_mask[:, 3:] = False
|
| 58 |
-
|
| 59 |
-
# Extract matching atom positions and convert to batched tensors
|
| 60 |
-
mobile_atom_tensor = (
|
| 61 |
-
torch.from_numpy(mobile.atom37_positions).type(torch.double).unsqueeze(0)
|
| 62 |
-
)
|
| 63 |
-
target_atom_tensor = (
|
| 64 |
-
torch.from_numpy(target.atom37_positions).type(torch.double).unsqueeze(0)
|
| 65 |
-
)
|
| 66 |
-
joint_atom37_mask = (
|
| 67 |
-
torch.from_numpy(joint_atom37_mask).type(torch.bool).unsqueeze(0)
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# If using reflection flip target
|
| 71 |
-
if use_reflection:
|
| 72 |
-
target_atom_tensor = -target_atom_tensor
|
| 73 |
-
|
| 74 |
-
# Compute alignment and rmsd
|
| 75 |
-
affine3D, rmsd = compute_affine_and_rmsd(
|
| 76 |
-
mobile_atom_tensor, target_atom_tensor, atom_exists_mask=joint_atom37_mask
|
| 77 |
-
)
|
| 78 |
-
self._affine3D = affine3D
|
| 79 |
-
self._rmsd = rmsd.item()
|
| 80 |
-
|
| 81 |
-
@property
|
| 82 |
-
def rmsd(self):
|
| 83 |
-
return self._rmsd
|
| 84 |
-
|
| 85 |
-
def apply(self, mobile: T) -> T:
|
| 86 |
-
"""Apply alignment to a protein chain"""
|
| 87 |
-
# Extract atom positions and convert to batched tensors
|
| 88 |
-
mobile_atom_tensor = (
|
| 89 |
-
torch.from_numpy(mobile.atom37_positions[mobile.atom37_mask])
|
| 90 |
-
.type(torch.float32)
|
| 91 |
-
.unsqueeze(0)
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
# Transform atom arrays
|
| 95 |
-
aligned_atom_tensor = self._affine3D.apply(mobile_atom_tensor).squeeze(0)
|
| 96 |
-
|
| 97 |
-
# Rebuild atom37 positions
|
| 98 |
-
aligned_atom37_positions = np.full_like(mobile.atom37_positions, np.nan)
|
| 99 |
-
aligned_atom37_positions[mobile.atom37_mask] = aligned_atom_tensor
|
| 100 |
-
|
| 101 |
-
return replace(mobile, atom37_positions=aligned_atom37_positions)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import Field, replace
|
| 4 |
+
from typing import Any, ClassVar, Protocol, TypeVar
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from .esmfold2_protein_structure import compute_affine_and_rmsd
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Alignable(Protocol):
|
| 13 |
+
# Trick to detect whether an object is a dataclass
|
| 14 |
+
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
| 15 |
+
|
| 16 |
+
@property
|
| 17 |
+
def atom37_positions(self) -> np.ndarray: # type: ignore
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
@property
|
| 21 |
+
def atom37_mask(self) -> np.ndarray: # type: ignore
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
def __len__(self) -> int: ...
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
T = TypeVar("T", bound=Alignable)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Aligner:
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
mobile: Alignable,
|
| 34 |
+
target: Alignable,
|
| 35 |
+
only_use_backbone: bool = False,
|
| 36 |
+
use_reflection: bool = False,
|
| 37 |
+
):
|
| 38 |
+
"""
|
| 39 |
+
Aligns a mobile protein chain against a target protein chain.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
mobile (ProteinChain): Protein chain to be aligned.
|
| 43 |
+
target (ProteinChain): Protein chain target.
|
| 44 |
+
only_use_backbone (bool): Whether to only use backbone atoms.
|
| 45 |
+
use_reflection (bool): Whether to align to target reflection.
|
| 46 |
+
"""
|
| 47 |
+
# Check proteins must have same number of residues
|
| 48 |
+
assert len(mobile) == len(target)
|
| 49 |
+
|
| 50 |
+
# Determine overlapping atoms
|
| 51 |
+
joint_atom37_mask = mobile.atom37_mask.astype(bool) & target.atom37_mask.astype(
|
| 52 |
+
bool
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Backbone atoms are first sites in atom37 representation
|
| 56 |
+
if only_use_backbone:
|
| 57 |
+
joint_atom37_mask[:, 3:] = False
|
| 58 |
+
|
| 59 |
+
# Extract matching atom positions and convert to batched tensors
|
| 60 |
+
mobile_atom_tensor = (
|
| 61 |
+
torch.from_numpy(mobile.atom37_positions).type(torch.double).unsqueeze(0)
|
| 62 |
+
)
|
| 63 |
+
target_atom_tensor = (
|
| 64 |
+
torch.from_numpy(target.atom37_positions).type(torch.double).unsqueeze(0)
|
| 65 |
+
)
|
| 66 |
+
joint_atom37_mask = (
|
| 67 |
+
torch.from_numpy(joint_atom37_mask).type(torch.bool).unsqueeze(0)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# If using reflection flip target
|
| 71 |
+
if use_reflection:
|
| 72 |
+
target_atom_tensor = -target_atom_tensor
|
| 73 |
+
|
| 74 |
+
# Compute alignment and rmsd
|
| 75 |
+
affine3D, rmsd = compute_affine_and_rmsd(
|
| 76 |
+
mobile_atom_tensor, target_atom_tensor, atom_exists_mask=joint_atom37_mask
|
| 77 |
+
)
|
| 78 |
+
self._affine3D = affine3D
|
| 79 |
+
self._rmsd = rmsd.item()
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def rmsd(self):
|
| 83 |
+
return self._rmsd
|
| 84 |
+
|
| 85 |
+
def apply(self, mobile: T) -> T:
|
| 86 |
+
"""Apply alignment to a protein chain"""
|
| 87 |
+
# Extract atom positions and convert to batched tensors
|
| 88 |
+
mobile_atom_tensor = (
|
| 89 |
+
torch.from_numpy(mobile.atom37_positions[mobile.atom37_mask])
|
| 90 |
+
.type(torch.float32)
|
| 91 |
+
.unsqueeze(0)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Transform atom arrays
|
| 95 |
+
aligned_atom_tensor = self._affine3D.apply(mobile_atom_tensor).squeeze(0)
|
| 96 |
+
|
| 97 |
+
# Rebuild atom37 positions
|
| 98 |
+
aligned_atom37_positions = np.full_like(mobile.atom37_positions, np.nan)
|
| 99 |
+
aligned_atom37_positions[mobile.atom37_mask] = aligned_atom_tensor
|
| 100 |
+
|
| 101 |
+
return replace(mobile, atom37_positions=aligned_atom37_positions)
|
esmfold2_atom_indexer.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
-
import numpy as np
|
| 2 |
-
|
| 3 |
-
from .esmfold2_protein_structure import index_by_atom_name
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class AtomIndexer:
|
| 7 |
-
def __init__(self, structure, property: str, dim: int):
|
| 8 |
-
self.structure = structure
|
| 9 |
-
self.property = property
|
| 10 |
-
self.dim = dim
|
| 11 |
-
|
| 12 |
-
def __getitem__(self, atom_names: str | list[str]) -> np.ndarray:
|
| 13 |
-
return index_by_atom_name(
|
| 14 |
-
getattr(self.structure, self.property), atom_names, self.dim
|
| 15 |
-
)
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from .esmfold2_protein_structure import index_by_atom_name
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class AtomIndexer:
|
| 7 |
+
def __init__(self, structure, property: str, dim: int):
|
| 8 |
+
self.structure = structure
|
| 9 |
+
self.property = property
|
| 10 |
+
self.dim = dim
|
| 11 |
+
|
| 12 |
+
def __getitem__(self, atom_names: str | list[str]) -> np.ndarray:
|
| 13 |
+
return index_by_atom_name(
|
| 14 |
+
getattr(self.structure, self.property), atom_names, self.dim
|
| 15 |
+
)
|
esmfold2_conformers.py
CHANGED
|
@@ -1,291 +1,291 @@
|
|
| 1 |
-
"""CCD conformer loading utilities.
|
| 2 |
-
|
| 3 |
-
Loads idealized conformer coordinates from a CCD pickle file containing RDKit molecules.
|
| 4 |
-
Conformer priority follows AF3 Section 2.8: Computed > Ideal > first available.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import os
|
| 10 |
-
import pickle
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
import numpy as np
|
| 14 |
-
from huggingface_hub import hf_hub_download
|
| 15 |
-
|
| 16 |
-
from .esmfold2_constants import RES_TYPE_TO_CCD
|
| 17 |
-
|
| 18 |
-
if os.environ.get("ESMCFOLD_CCD_PATH"):
|
| 19 |
-
CCD_PICKLE_PATH = Path(os.environ["ESMCFOLD_CCD_PATH"])
|
| 20 |
-
else:
|
| 21 |
-
CCD_PICKLE_PATH = None
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# Lazily loaded CCD dictionary
|
| 25 |
-
_CCD_MOLECULES: dict | None = None
|
| 26 |
-
|
| 27 |
-
# Caches
|
| 28 |
-
_CCD_CONFORMERS: dict[str, dict[str, np.ndarray]] = {}
|
| 29 |
-
_CCD_ATOM_CACHE: dict[str, list[tuple[str, str, int]]] = {}
|
| 30 |
-
_CCD_BONDS_CACHE: dict[str, list[tuple[str, str]]] = {}
|
| 31 |
-
_CCD_LEAVING_ATOMS_CACHE: dict[str, set[str]] = {}
|
| 32 |
-
_IDEALIZED_POS_CACHE: dict[tuple[int, str], np.ndarray | None] = {}
|
| 33 |
-
_LIGAND_IDEALIZED_POS_CACHE: dict[tuple[str, str], np.ndarray | None] = {}
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def load_ccd(cache_dir: Path | str | None = None) -> dict:
|
| 37 |
-
"""Load CCD molecules from pickle file, downloading if needed.
|
| 38 |
-
|
| 39 |
-
Args:
|
| 40 |
-
cache_dir: Directory to cache the downloaded CCD pickle.
|
| 41 |
-
If None, uses CCD_PICKLE_PATH env var or downloads to ~/.cache/esmcfold/.
|
| 42 |
-
"""
|
| 43 |
-
global _CCD_MOLECULES
|
| 44 |
-
if _CCD_MOLECULES is not None:
|
| 45 |
-
return _CCD_MOLECULES
|
| 46 |
-
|
| 47 |
-
# Determine pickle path
|
| 48 |
-
if CCD_PICKLE_PATH is not None and CCD_PICKLE_PATH.exists():
|
| 49 |
-
pkl_path = CCD_PICKLE_PATH
|
| 50 |
-
elif cache_dir is not None:
|
| 51 |
-
cache_dir = Path(cache_dir)
|
| 52 |
-
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 53 |
-
pkl_path = cache_dir / "ccd.pkl"
|
| 54 |
-
else:
|
| 55 |
-
try:
|
| 56 |
-
pkl_path = Path(
|
| 57 |
-
hf_hub_download(repo_id="biohub/ESMFold2", filename="ccd.pkl")
|
| 58 |
-
)
|
| 59 |
-
except Exception as e:
|
| 60 |
-
raise FileNotFoundError(
|
| 61 |
-
f"Failed to download CCD pickle file from Hugging Face repository: {e}"
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
if not pkl_path.exists():
|
| 65 |
-
raise FileNotFoundError(
|
| 66 |
-
f"CCD pickle file not found: {pkl_path}. Please set the ESMCFOLD_CCD_PATH environment variable to the path of a valid CCD pickle file or download the file from the Hugging Face repository."
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
print(f"Loading CCD dictionary from {pkl_path}")
|
| 70 |
-
with open(pkl_path, "rb") as f:
|
| 71 |
-
_CCD_MOLECULES = pickle.load(f)
|
| 72 |
-
|
| 73 |
-
if _CCD_MOLECULES is None:
|
| 74 |
-
_CCD_MOLECULES = {}
|
| 75 |
-
|
| 76 |
-
return _CCD_MOLECULES
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def _get_ccd_molecules() -> dict:
|
| 80 |
-
"""Get CCD molecules, loading lazily on first call."""
|
| 81 |
-
global _CCD_MOLECULES
|
| 82 |
-
if _CCD_MOLECULES is None:
|
| 83 |
-
return load_ccd()
|
| 84 |
-
return _CCD_MOLECULES
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def _get_ccd_mol_with_significant_h(comp_id: str):
|
| 88 |
-
"""Get CCD molecule with only chemically significant hydrogens.
|
| 89 |
-
|
| 90 |
-
Returns (mol, conformer) tuple or (None, None) if not available.
|
| 91 |
-
"""
|
| 92 |
-
ccd = _get_ccd_molecules()
|
| 93 |
-
if comp_id not in ccd:
|
| 94 |
-
return None, None
|
| 95 |
-
|
| 96 |
-
mol = ccd[comp_id]
|
| 97 |
-
if mol.GetNumConformers() == 0:
|
| 98 |
-
return None, None
|
| 99 |
-
|
| 100 |
-
# Find the "Computed" conformer (RDKit ETKDGv3), fall back to "Ideal"
|
| 101 |
-
conf_idx = 0
|
| 102 |
-
for i, c in enumerate(mol.GetConformers()):
|
| 103 |
-
props = c.GetPropsAsDict()
|
| 104 |
-
if props.get("name") == "Computed":
|
| 105 |
-
conf_idx = i
|
| 106 |
-
break
|
| 107 |
-
else:
|
| 108 |
-
for i, c in enumerate(mol.GetConformers()):
|
| 109 |
-
props = c.GetPropsAsDict()
|
| 110 |
-
if props.get("name") == "Ideal":
|
| 111 |
-
conf_idx = i
|
| 112 |
-
break
|
| 113 |
-
|
| 114 |
-
from rdkit import Chem
|
| 115 |
-
|
| 116 |
-
mol_no_h = Chem.RemoveHs(mol, sanitize=False)
|
| 117 |
-
|
| 118 |
-
if mol_no_h.GetNumConformers() == 0:
|
| 119 |
-
return None, None
|
| 120 |
-
|
| 121 |
-
return mol_no_h, mol_no_h.GetConformer(
|
| 122 |
-
min(conf_idx, mol_no_h.GetNumConformers() - 1)
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def get_ccd_conformer(comp_id: str) -> dict[str, np.ndarray] | None:
|
| 127 |
-
"""Get idealized conformer as dict of atom_name -> position [3].
|
| 128 |
-
|
| 129 |
-
Conformer priority: Computed > Ideal > first available.
|
| 130 |
-
"""
|
| 131 |
-
if comp_id in _CCD_CONFORMERS:
|
| 132 |
-
cached = _CCD_CONFORMERS[comp_id]
|
| 133 |
-
return cached if cached else None
|
| 134 |
-
|
| 135 |
-
mol, conf = _get_ccd_mol_with_significant_h(comp_id)
|
| 136 |
-
if mol is None or conf is None:
|
| 137 |
-
_CCD_CONFORMERS[comp_id] = {}
|
| 138 |
-
return None
|
| 139 |
-
|
| 140 |
-
conformer: dict[str, np.ndarray] = {}
|
| 141 |
-
for atom in mol.GetAtoms():
|
| 142 |
-
props = atom.GetPropsAsDict()
|
| 143 |
-
atom_name = props.get("name")
|
| 144 |
-
if not isinstance(atom_name, str) or not atom_name:
|
| 145 |
-
continue
|
| 146 |
-
idx = atom.GetIdx()
|
| 147 |
-
pos = conf.GetAtomPosition(idx)
|
| 148 |
-
conformer[atom_name] = np.array([pos.x, pos.y, pos.z], dtype=np.float32)
|
| 149 |
-
|
| 150 |
-
_CCD_CONFORMERS[comp_id] = conformer
|
| 151 |
-
return conformer if conformer else None
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def get_idealized_atom_pos(res_type: int, atom_name: str) -> np.ndarray | None:
|
| 155 |
-
"""Get idealized position for a standard residue atom.
|
| 156 |
-
|
| 157 |
-
Uses res_type index to look up CCD component, then returns position.
|
| 158 |
-
Returns None if not found.
|
| 159 |
-
"""
|
| 160 |
-
cache_key = (res_type, atom_name)
|
| 161 |
-
if cache_key in _IDEALIZED_POS_CACHE:
|
| 162 |
-
return _IDEALIZED_POS_CACHE[cache_key]
|
| 163 |
-
|
| 164 |
-
comp_id = RES_TYPE_TO_CCD.get(res_type)
|
| 165 |
-
if comp_id:
|
| 166 |
-
ccd_conformer = get_ccd_conformer(comp_id)
|
| 167 |
-
if ccd_conformer and atom_name in ccd_conformer:
|
| 168 |
-
pos = ccd_conformer[atom_name]
|
| 169 |
-
_IDEALIZED_POS_CACHE[cache_key] = pos
|
| 170 |
-
return pos
|
| 171 |
-
|
| 172 |
-
_IDEALIZED_POS_CACHE[cache_key] = None
|
| 173 |
-
return None
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def get_ligand_idealized_atom_pos(res_name: str, atom_name: str) -> np.ndarray | None:
|
| 177 |
-
"""Get idealized position for a ligand/modified residue atom.
|
| 178 |
-
|
| 179 |
-
Returns None if not found.
|
| 180 |
-
"""
|
| 181 |
-
cache_key = (res_name, atom_name)
|
| 182 |
-
if cache_key in _LIGAND_IDEALIZED_POS_CACHE:
|
| 183 |
-
return _LIGAND_IDEALIZED_POS_CACHE[cache_key]
|
| 184 |
-
|
| 185 |
-
ccd_conformer = get_ccd_conformer(res_name)
|
| 186 |
-
if ccd_conformer and atom_name in ccd_conformer:
|
| 187 |
-
pos = ccd_conformer[atom_name]
|
| 188 |
-
_LIGAND_IDEALIZED_POS_CACHE[cache_key] = pos
|
| 189 |
-
return pos
|
| 190 |
-
|
| 191 |
-
_LIGAND_IDEALIZED_POS_CACHE[cache_key] = None
|
| 192 |
-
return None
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def get_ligand_ccd_atoms_with_charges(
|
| 196 |
-
comp_id: str,
|
| 197 |
-
) -> list[tuple[str, str, int]] | None:
|
| 198 |
-
"""Get list of (atom_name, element, charge) for a CCD component.
|
| 199 |
-
|
| 200 |
-
Uses RDKit RemoveHs(sanitize=False) to keep chemically significant hydrogens.
|
| 201 |
-
Returns None if CCD data not available.
|
| 202 |
-
"""
|
| 203 |
-
if comp_id in _CCD_ATOM_CACHE:
|
| 204 |
-
cached = _CCD_ATOM_CACHE[comp_id]
|
| 205 |
-
return cached if cached else None
|
| 206 |
-
|
| 207 |
-
mol, _ = _get_ccd_mol_with_significant_h(comp_id)
|
| 208 |
-
if mol is None:
|
| 209 |
-
_CCD_ATOM_CACHE[comp_id] = []
|
| 210 |
-
return None
|
| 211 |
-
|
| 212 |
-
atoms: list[tuple[str, str, int]] = []
|
| 213 |
-
for atom in mol.GetAtoms():
|
| 214 |
-
props = atom.GetPropsAsDict()
|
| 215 |
-
atom_name = props.get("name")
|
| 216 |
-
if not isinstance(atom_name, str) or not atom_name:
|
| 217 |
-
continue
|
| 218 |
-
element = atom.GetSymbol()
|
| 219 |
-
charge = atom.GetFormalCharge()
|
| 220 |
-
atoms.append((atom_name, element, charge))
|
| 221 |
-
|
| 222 |
-
_CCD_ATOM_CACHE[comp_id] = atoms
|
| 223 |
-
return atoms if atoms else None
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def get_ligand_ccd_bonds(comp_id: str) -> list[tuple[str, str]] | None:
|
| 227 |
-
"""Get list of (atom1_name, atom2_name) bonds for a CCD component.
|
| 228 |
-
|
| 229 |
-
Returns None if CCD data not available.
|
| 230 |
-
"""
|
| 231 |
-
if comp_id in _CCD_BONDS_CACHE:
|
| 232 |
-
cached = _CCD_BONDS_CACHE[comp_id]
|
| 233 |
-
return cached if cached else None
|
| 234 |
-
|
| 235 |
-
mol, _ = _get_ccd_mol_with_significant_h(comp_id)
|
| 236 |
-
if mol is None:
|
| 237 |
-
_CCD_BONDS_CACHE[comp_id] = []
|
| 238 |
-
return None
|
| 239 |
-
|
| 240 |
-
# Get included atom names
|
| 241 |
-
included_atoms = set()
|
| 242 |
-
for atom in mol.GetAtoms():
|
| 243 |
-
props = atom.GetPropsAsDict()
|
| 244 |
-
atom_name = props.get("name")
|
| 245 |
-
if isinstance(atom_name, str) and atom_name:
|
| 246 |
-
included_atoms.add(atom_name)
|
| 247 |
-
|
| 248 |
-
bonds: list[tuple[str, str]] = []
|
| 249 |
-
for bond in mol.GetBonds():
|
| 250 |
-
a1 = bond.GetBeginAtom()
|
| 251 |
-
a2 = bond.GetEndAtom()
|
| 252 |
-
n1 = a1.GetPropsAsDict().get("name")
|
| 253 |
-
n2 = a2.GetPropsAsDict().get("name")
|
| 254 |
-
if (
|
| 255 |
-
isinstance(n1, str)
|
| 256 |
-
and isinstance(n2, str)
|
| 257 |
-
and n1
|
| 258 |
-
and n2
|
| 259 |
-
and n1 in included_atoms
|
| 260 |
-
and n2 in included_atoms
|
| 261 |
-
):
|
| 262 |
-
bonds.append((n1, n2))
|
| 263 |
-
|
| 264 |
-
_CCD_BONDS_CACHE[comp_id] = bonds
|
| 265 |
-
return bonds if bonds else None
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def get_ccd_leaving_atoms(comp_id: str) -> set[str]:
|
| 269 |
-
"""Get set of atom names marked as leaving atoms in CCD.
|
| 270 |
-
|
| 271 |
-
Leaving atoms are removed during polymerization (e.g., OP3 in nucleotides).
|
| 272 |
-
"""
|
| 273 |
-
if comp_id in _CCD_LEAVING_ATOMS_CACHE:
|
| 274 |
-
return _CCD_LEAVING_ATOMS_CACHE[comp_id]
|
| 275 |
-
|
| 276 |
-
ccd = _get_ccd_molecules()
|
| 277 |
-
if comp_id not in ccd:
|
| 278 |
-
_CCD_LEAVING_ATOMS_CACHE[comp_id] = set()
|
| 279 |
-
return set()
|
| 280 |
-
|
| 281 |
-
mol = ccd[comp_id]
|
| 282 |
-
leaving_atoms = set()
|
| 283 |
-
for atom in mol.GetAtoms():
|
| 284 |
-
if atom.HasProp("leaving_atom"):
|
| 285 |
-
if atom.GetProp("leaving_atom") == "1":
|
| 286 |
-
name = atom.GetProp("name") if atom.HasProp("name") else ""
|
| 287 |
-
if name:
|
| 288 |
-
leaving_atoms.add(name)
|
| 289 |
-
|
| 290 |
-
_CCD_LEAVING_ATOMS_CACHE[comp_id] = leaving_atoms
|
| 291 |
-
return leaving_atoms
|
|
|
|
| 1 |
+
"""CCD conformer loading utilities.
|
| 2 |
+
|
| 3 |
+
Loads idealized conformer coordinates from a CCD pickle file containing RDKit molecules.
|
| 4 |
+
Conformer priority follows AF3 Section 2.8: Computed > Ideal > first available.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import pickle
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from huggingface_hub import hf_hub_download
|
| 15 |
+
|
| 16 |
+
from .esmfold2_constants import RES_TYPE_TO_CCD
|
| 17 |
+
|
| 18 |
+
if os.environ.get("ESMCFOLD_CCD_PATH"):
|
| 19 |
+
CCD_PICKLE_PATH = Path(os.environ["ESMCFOLD_CCD_PATH"])
|
| 20 |
+
else:
|
| 21 |
+
CCD_PICKLE_PATH = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Lazily loaded CCD dictionary
|
| 25 |
+
_CCD_MOLECULES: dict | None = None
|
| 26 |
+
|
| 27 |
+
# Caches
|
| 28 |
+
_CCD_CONFORMERS: dict[str, dict[str, np.ndarray]] = {}
|
| 29 |
+
_CCD_ATOM_CACHE: dict[str, list[tuple[str, str, int]]] = {}
|
| 30 |
+
_CCD_BONDS_CACHE: dict[str, list[tuple[str, str]]] = {}
|
| 31 |
+
_CCD_LEAVING_ATOMS_CACHE: dict[str, set[str]] = {}
|
| 32 |
+
_IDEALIZED_POS_CACHE: dict[tuple[int, str], np.ndarray | None] = {}
|
| 33 |
+
_LIGAND_IDEALIZED_POS_CACHE: dict[tuple[str, str], np.ndarray | None] = {}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_ccd(cache_dir: Path | str | None = None) -> dict:
|
| 37 |
+
"""Load CCD molecules from pickle file, downloading if needed.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
cache_dir: Directory to cache the downloaded CCD pickle.
|
| 41 |
+
If None, uses CCD_PICKLE_PATH env var or downloads to ~/.cache/esmcfold/.
|
| 42 |
+
"""
|
| 43 |
+
global _CCD_MOLECULES
|
| 44 |
+
if _CCD_MOLECULES is not None:
|
| 45 |
+
return _CCD_MOLECULES
|
| 46 |
+
|
| 47 |
+
# Determine pickle path
|
| 48 |
+
if CCD_PICKLE_PATH is not None and CCD_PICKLE_PATH.exists():
|
| 49 |
+
pkl_path = CCD_PICKLE_PATH
|
| 50 |
+
elif cache_dir is not None:
|
| 51 |
+
cache_dir = Path(cache_dir)
|
| 52 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 53 |
+
pkl_path = cache_dir / "ccd.pkl"
|
| 54 |
+
else:
|
| 55 |
+
try:
|
| 56 |
+
pkl_path = Path(
|
| 57 |
+
hf_hub_download(repo_id="biohub/ESMFold2", filename="ccd.pkl")
|
| 58 |
+
)
|
| 59 |
+
except Exception as e:
|
| 60 |
+
raise FileNotFoundError(
|
| 61 |
+
f"Failed to download CCD pickle file from Hugging Face repository: {e}"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
if not pkl_path.exists():
|
| 65 |
+
raise FileNotFoundError(
|
| 66 |
+
f"CCD pickle file not found: {pkl_path}. Please set the ESMCFOLD_CCD_PATH environment variable to the path of a valid CCD pickle file or download the file from the Hugging Face repository."
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
print(f"Loading CCD dictionary from {pkl_path}")
|
| 70 |
+
with open(pkl_path, "rb") as f:
|
| 71 |
+
_CCD_MOLECULES = pickle.load(f)
|
| 72 |
+
|
| 73 |
+
if _CCD_MOLECULES is None:
|
| 74 |
+
_CCD_MOLECULES = {}
|
| 75 |
+
|
| 76 |
+
return _CCD_MOLECULES
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _get_ccd_molecules() -> dict:
|
| 80 |
+
"""Get CCD molecules, loading lazily on first call."""
|
| 81 |
+
global _CCD_MOLECULES
|
| 82 |
+
if _CCD_MOLECULES is None:
|
| 83 |
+
return load_ccd()
|
| 84 |
+
return _CCD_MOLECULES
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _get_ccd_mol_with_significant_h(comp_id: str):
|
| 88 |
+
"""Get CCD molecule with only chemically significant hydrogens.
|
| 89 |
+
|
| 90 |
+
Returns (mol, conformer) tuple or (None, None) if not available.
|
| 91 |
+
"""
|
| 92 |
+
ccd = _get_ccd_molecules()
|
| 93 |
+
if comp_id not in ccd:
|
| 94 |
+
return None, None
|
| 95 |
+
|
| 96 |
+
mol = ccd[comp_id]
|
| 97 |
+
if mol.GetNumConformers() == 0:
|
| 98 |
+
return None, None
|
| 99 |
+
|
| 100 |
+
# Find the "Computed" conformer (RDKit ETKDGv3), fall back to "Ideal"
|
| 101 |
+
conf_idx = 0
|
| 102 |
+
for i, c in enumerate(mol.GetConformers()):
|
| 103 |
+
props = c.GetPropsAsDict()
|
| 104 |
+
if props.get("name") == "Computed":
|
| 105 |
+
conf_idx = i
|
| 106 |
+
break
|
| 107 |
+
else:
|
| 108 |
+
for i, c in enumerate(mol.GetConformers()):
|
| 109 |
+
props = c.GetPropsAsDict()
|
| 110 |
+
if props.get("name") == "Ideal":
|
| 111 |
+
conf_idx = i
|
| 112 |
+
break
|
| 113 |
+
|
| 114 |
+
from rdkit import Chem
|
| 115 |
+
|
| 116 |
+
mol_no_h = Chem.RemoveHs(mol, sanitize=False)
|
| 117 |
+
|
| 118 |
+
if mol_no_h.GetNumConformers() == 0:
|
| 119 |
+
return None, None
|
| 120 |
+
|
| 121 |
+
return mol_no_h, mol_no_h.GetConformer(
|
| 122 |
+
min(conf_idx, mol_no_h.GetNumConformers() - 1)
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def get_ccd_conformer(comp_id: str) -> dict[str, np.ndarray] | None:
|
| 127 |
+
"""Get idealized conformer as dict of atom_name -> position [3].
|
| 128 |
+
|
| 129 |
+
Conformer priority: Computed > Ideal > first available.
|
| 130 |
+
"""
|
| 131 |
+
if comp_id in _CCD_CONFORMERS:
|
| 132 |
+
cached = _CCD_CONFORMERS[comp_id]
|
| 133 |
+
return cached if cached else None
|
| 134 |
+
|
| 135 |
+
mol, conf = _get_ccd_mol_with_significant_h(comp_id)
|
| 136 |
+
if mol is None or conf is None:
|
| 137 |
+
_CCD_CONFORMERS[comp_id] = {}
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
conformer: dict[str, np.ndarray] = {}
|
| 141 |
+
for atom in mol.GetAtoms():
|
| 142 |
+
props = atom.GetPropsAsDict()
|
| 143 |
+
atom_name = props.get("name")
|
| 144 |
+
if not isinstance(atom_name, str) or not atom_name:
|
| 145 |
+
continue
|
| 146 |
+
idx = atom.GetIdx()
|
| 147 |
+
pos = conf.GetAtomPosition(idx)
|
| 148 |
+
conformer[atom_name] = np.array([pos.x, pos.y, pos.z], dtype=np.float32)
|
| 149 |
+
|
| 150 |
+
_CCD_CONFORMERS[comp_id] = conformer
|
| 151 |
+
return conformer if conformer else None
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def get_idealized_atom_pos(res_type: int, atom_name: str) -> np.ndarray | None:
|
| 155 |
+
"""Get idealized position for a standard residue atom.
|
| 156 |
+
|
| 157 |
+
Uses res_type index to look up CCD component, then returns position.
|
| 158 |
+
Returns None if not found.
|
| 159 |
+
"""
|
| 160 |
+
cache_key = (res_type, atom_name)
|
| 161 |
+
if cache_key in _IDEALIZED_POS_CACHE:
|
| 162 |
+
return _IDEALIZED_POS_CACHE[cache_key]
|
| 163 |
+
|
| 164 |
+
comp_id = RES_TYPE_TO_CCD.get(res_type)
|
| 165 |
+
if comp_id:
|
| 166 |
+
ccd_conformer = get_ccd_conformer(comp_id)
|
| 167 |
+
if ccd_conformer and atom_name in ccd_conformer:
|
| 168 |
+
pos = ccd_conformer[atom_name]
|
| 169 |
+
_IDEALIZED_POS_CACHE[cache_key] = pos
|
| 170 |
+
return pos
|
| 171 |
+
|
| 172 |
+
_IDEALIZED_POS_CACHE[cache_key] = None
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def get_ligand_idealized_atom_pos(res_name: str, atom_name: str) -> np.ndarray | None:
|
| 177 |
+
"""Get idealized position for a ligand/modified residue atom.
|
| 178 |
+
|
| 179 |
+
Returns None if not found.
|
| 180 |
+
"""
|
| 181 |
+
cache_key = (res_name, atom_name)
|
| 182 |
+
if cache_key in _LIGAND_IDEALIZED_POS_CACHE:
|
| 183 |
+
return _LIGAND_IDEALIZED_POS_CACHE[cache_key]
|
| 184 |
+
|
| 185 |
+
ccd_conformer = get_ccd_conformer(res_name)
|
| 186 |
+
if ccd_conformer and atom_name in ccd_conformer:
|
| 187 |
+
pos = ccd_conformer[atom_name]
|
| 188 |
+
_LIGAND_IDEALIZED_POS_CACHE[cache_key] = pos
|
| 189 |
+
return pos
|
| 190 |
+
|
| 191 |
+
_LIGAND_IDEALIZED_POS_CACHE[cache_key] = None
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def get_ligand_ccd_atoms_with_charges(
|
| 196 |
+
comp_id: str,
|
| 197 |
+
) -> list[tuple[str, str, int]] | None:
|
| 198 |
+
"""Get list of (atom_name, element, charge) for a CCD component.
|
| 199 |
+
|
| 200 |
+
Uses RDKit RemoveHs(sanitize=False) to keep chemically significant hydrogens.
|
| 201 |
+
Returns None if CCD data not available.
|
| 202 |
+
"""
|
| 203 |
+
if comp_id in _CCD_ATOM_CACHE:
|
| 204 |
+
cached = _CCD_ATOM_CACHE[comp_id]
|
| 205 |
+
return cached if cached else None
|
| 206 |
+
|
| 207 |
+
mol, _ = _get_ccd_mol_with_significant_h(comp_id)
|
| 208 |
+
if mol is None:
|
| 209 |
+
_CCD_ATOM_CACHE[comp_id] = []
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
atoms: list[tuple[str, str, int]] = []
|
| 213 |
+
for atom in mol.GetAtoms():
|
| 214 |
+
props = atom.GetPropsAsDict()
|
| 215 |
+
atom_name = props.get("name")
|
| 216 |
+
if not isinstance(atom_name, str) or not atom_name:
|
| 217 |
+
continue
|
| 218 |
+
element = atom.GetSymbol()
|
| 219 |
+
charge = atom.GetFormalCharge()
|
| 220 |
+
atoms.append((atom_name, element, charge))
|
| 221 |
+
|
| 222 |
+
_CCD_ATOM_CACHE[comp_id] = atoms
|
| 223 |
+
return atoms if atoms else None
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def get_ligand_ccd_bonds(comp_id: str) -> list[tuple[str, str]] | None:
|
| 227 |
+
"""Get list of (atom1_name, atom2_name) bonds for a CCD component.
|
| 228 |
+
|
| 229 |
+
Returns None if CCD data not available.
|
| 230 |
+
"""
|
| 231 |
+
if comp_id in _CCD_BONDS_CACHE:
|
| 232 |
+
cached = _CCD_BONDS_CACHE[comp_id]
|
| 233 |
+
return cached if cached else None
|
| 234 |
+
|
| 235 |
+
mol, _ = _get_ccd_mol_with_significant_h(comp_id)
|
| 236 |
+
if mol is None:
|
| 237 |
+
_CCD_BONDS_CACHE[comp_id] = []
|
| 238 |
+
return None
|
| 239 |
+
|
| 240 |
+
# Get included atom names
|
| 241 |
+
included_atoms = set()
|
| 242 |
+
for atom in mol.GetAtoms():
|
| 243 |
+
props = atom.GetPropsAsDict()
|
| 244 |
+
atom_name = props.get("name")
|
| 245 |
+
if isinstance(atom_name, str) and atom_name:
|
| 246 |
+
included_atoms.add(atom_name)
|
| 247 |
+
|
| 248 |
+
bonds: list[tuple[str, str]] = []
|
| 249 |
+
for bond in mol.GetBonds():
|
| 250 |
+
a1 = bond.GetBeginAtom()
|
| 251 |
+
a2 = bond.GetEndAtom()
|
| 252 |
+
n1 = a1.GetPropsAsDict().get("name")
|
| 253 |
+
n2 = a2.GetPropsAsDict().get("name")
|
| 254 |
+
if (
|
| 255 |
+
isinstance(n1, str)
|
| 256 |
+
and isinstance(n2, str)
|
| 257 |
+
and n1
|
| 258 |
+
and n2
|
| 259 |
+
and n1 in included_atoms
|
| 260 |
+
and n2 in included_atoms
|
| 261 |
+
):
|
| 262 |
+
bonds.append((n1, n2))
|
| 263 |
+
|
| 264 |
+
_CCD_BONDS_CACHE[comp_id] = bonds
|
| 265 |
+
return bonds if bonds else None
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def get_ccd_leaving_atoms(comp_id: str) -> set[str]:
|
| 269 |
+
"""Get set of atom names marked as leaving atoms in CCD.
|
| 270 |
+
|
| 271 |
+
Leaving atoms are removed during polymerization (e.g., OP3 in nucleotides).
|
| 272 |
+
"""
|
| 273 |
+
if comp_id in _CCD_LEAVING_ATOMS_CACHE:
|
| 274 |
+
return _CCD_LEAVING_ATOMS_CACHE[comp_id]
|
| 275 |
+
|
| 276 |
+
ccd = _get_ccd_molecules()
|
| 277 |
+
if comp_id not in ccd:
|
| 278 |
+
_CCD_LEAVING_ATOMS_CACHE[comp_id] = set()
|
| 279 |
+
return set()
|
| 280 |
+
|
| 281 |
+
mol = ccd[comp_id]
|
| 282 |
+
leaving_atoms = set()
|
| 283 |
+
for atom in mol.GetAtoms():
|
| 284 |
+
if atom.HasProp("leaving_atom"):
|
| 285 |
+
if atom.GetProp("leaving_atom") == "1":
|
| 286 |
+
name = atom.GetProp("name") if atom.HasProp("name") else ""
|
| 287 |
+
if name:
|
| 288 |
+
leaving_atoms.add(name)
|
| 289 |
+
|
| 290 |
+
_CCD_LEAVING_ATOMS_CACHE[comp_id] = leaving_atoms
|
| 291 |
+
return leaving_atoms
|
esmfold2_constants.py
CHANGED
|
@@ -1,562 +1,562 @@
|
|
| 1 |
-
"""Constants for the ESMFold2 input pipeline.
|
| 2 |
-
|
| 3 |
-
Includes molecule types, residue types, vocabularies, atom lists, and element data.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
# =============================================================================
|
| 7 |
-
# Molecule types
|
| 8 |
-
# =============================================================================
|
| 9 |
-
|
| 10 |
-
MOL_TYPE_PROTEIN = 0
|
| 11 |
-
MOL_TYPE_DNA = 1
|
| 12 |
-
MOL_TYPE_RNA = 2
|
| 13 |
-
MOL_TYPE_NONPOLYMER = 3
|
| 14 |
-
|
| 15 |
-
# =============================================================================
|
| 16 |
-
# Residue type indices
|
| 17 |
-
# =============================================================================
|
| 18 |
-
|
| 19 |
-
# Standard amino acids (indices 2-21), MSE mapped to MET
|
| 20 |
-
PROTEIN_RESIDUE_TO_RES_TYPE = {
|
| 21 |
-
"ALA": 2,
|
| 22 |
-
"ARG": 3,
|
| 23 |
-
"ASN": 4,
|
| 24 |
-
"ASP": 5,
|
| 25 |
-
"CYS": 6,
|
| 26 |
-
"GLN": 7,
|
| 27 |
-
"GLU": 8,
|
| 28 |
-
"GLY": 9,
|
| 29 |
-
"HIS": 10,
|
| 30 |
-
"ILE": 11,
|
| 31 |
-
"LEU": 12,
|
| 32 |
-
"LYS": 13,
|
| 33 |
-
"MET": 14,
|
| 34 |
-
"PHE": 15,
|
| 35 |
-
"PRO": 16,
|
| 36 |
-
"SER": 17,
|
| 37 |
-
"THR": 18,
|
| 38 |
-
"TRP": 19,
|
| 39 |
-
"TYR": 20,
|
| 40 |
-
"VAL": 21,
|
| 41 |
-
"MSE": 14, # Selenomethionine -> MET
|
| 42 |
-
}
|
| 43 |
-
PROTEIN_UNK_RES_TYPE = 22
|
| 44 |
-
|
| 45 |
-
# RNA nucleotides (indices 23-26, unknown=27)
|
| 46 |
-
RNA_RESIDUE_TO_RES_TYPE = {"A": 23, "G": 24, "C": 25, "U": 26}
|
| 47 |
-
RNA_UNK_RES_TYPE = 27
|
| 48 |
-
|
| 49 |
-
# DNA nucleotides (indices 28-31, unknown=32)
|
| 50 |
-
DNA_RESIDUE_TO_RES_TYPE = {"DA": 28, "DG": 29, "DC": 30, "DT": 31}
|
| 51 |
-
DNA_UNK_RES_TYPE = 32
|
| 52 |
-
|
| 53 |
-
GAP_RES_TYPE = 32
|
| 54 |
-
|
| 55 |
-
# =============================================================================
|
| 56 |
-
# Vocabularies
|
| 57 |
-
# =============================================================================
|
| 58 |
-
|
| 59 |
-
# 3-letter to 1-letter codes for proteins
|
| 60 |
-
PROTEIN_3TO1 = {
|
| 61 |
-
"ALA": "A",
|
| 62 |
-
"ARG": "R",
|
| 63 |
-
"ASN": "N",
|
| 64 |
-
"ASP": "D",
|
| 65 |
-
"CYS": "C",
|
| 66 |
-
"GLN": "Q",
|
| 67 |
-
"GLU": "E",
|
| 68 |
-
"GLY": "G",
|
| 69 |
-
"HIS": "H",
|
| 70 |
-
"ILE": "I",
|
| 71 |
-
"LEU": "L",
|
| 72 |
-
"LYS": "K",
|
| 73 |
-
"MET": "M",
|
| 74 |
-
"PHE": "F",
|
| 75 |
-
"PRO": "P",
|
| 76 |
-
"SER": "S",
|
| 77 |
-
"THR": "T",
|
| 78 |
-
"TRP": "W",
|
| 79 |
-
"TYR": "Y",
|
| 80 |
-
"VAL": "V",
|
| 81 |
-
"MSE": "M",
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
# 1-letter to 3-letter codes
|
| 85 |
-
PROTEIN_1TO3 = {v: k for k, v in PROTEIN_3TO1.items() if k != "MSE"}
|
| 86 |
-
PROTEIN_1TO3["X"] = "UNK"
|
| 87 |
-
|
| 88 |
-
# DNA 1-letter to CCD code
|
| 89 |
-
DNA_1TO3 = {"A": "DA", "T": "DT", "C": "DC", "G": "DG"}
|
| 90 |
-
|
| 91 |
-
# RNA 1-letter to CCD code
|
| 92 |
-
RNA_1TO3 = {"A": "A", "U": "U", "C": "C", "G": "G"}
|
| 93 |
-
|
| 94 |
-
# ESM-2 input_ids vocabulary for proteins
|
| 95 |
-
ESM_PROTEIN_VOCAB = {
|
| 96 |
-
"L": 4,
|
| 97 |
-
"A": 5,
|
| 98 |
-
"G": 6,
|
| 99 |
-
"V": 7,
|
| 100 |
-
"S": 8,
|
| 101 |
-
"E": 9,
|
| 102 |
-
"R": 10,
|
| 103 |
-
"T": 11,
|
| 104 |
-
"I": 12,
|
| 105 |
-
"D": 13,
|
| 106 |
-
"P": 14,
|
| 107 |
-
"K": 15,
|
| 108 |
-
"Q": 16,
|
| 109 |
-
"N": 17,
|
| 110 |
-
"F": 18,
|
| 111 |
-
"Y": 19,
|
| 112 |
-
"M": 20,
|
| 113 |
-
"H": 21,
|
| 114 |
-
"W": 22,
|
| 115 |
-
"C": 23,
|
| 116 |
-
"X": 3, # Unknown
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
# For DNA/RNA/ligands
|
| 120 |
-
DNA_RNA_LIGAND_INPUT_ID = 24
|
| 121 |
-
|
| 122 |
-
# MSA tokens
|
| 123 |
-
MSA_PAD_TOKEN_ID = 0
|
| 124 |
-
MSA_GAP_TOKEN_ID = 1 # Gap/insertion token for MSA
|
| 125 |
-
|
| 126 |
-
# res_type int -> CCD component ID (for conformer lookup)
|
| 127 |
-
RES_TYPE_TO_CCD = {
|
| 128 |
-
# Proteins (2-22)
|
| 129 |
-
2: "ALA",
|
| 130 |
-
3: "ARG",
|
| 131 |
-
4: "ASN",
|
| 132 |
-
5: "ASP",
|
| 133 |
-
6: "CYS",
|
| 134 |
-
7: "GLN",
|
| 135 |
-
8: "GLU",
|
| 136 |
-
9: "GLY",
|
| 137 |
-
10: "HIS",
|
| 138 |
-
11: "ILE",
|
| 139 |
-
12: "LEU",
|
| 140 |
-
13: "LYS",
|
| 141 |
-
14: "MET",
|
| 142 |
-
15: "PHE",
|
| 143 |
-
16: "PRO",
|
| 144 |
-
17: "SER",
|
| 145 |
-
18: "THR",
|
| 146 |
-
19: "TRP",
|
| 147 |
-
20: "TYR",
|
| 148 |
-
21: "VAL",
|
| 149 |
-
22: "UNK",
|
| 150 |
-
# RNA (23-27)
|
| 151 |
-
23: "A",
|
| 152 |
-
24: "G",
|
| 153 |
-
25: "C",
|
| 154 |
-
26: "U",
|
| 155 |
-
27: "N",
|
| 156 |
-
# DNA (28-32)
|
| 157 |
-
28: "DA",
|
| 158 |
-
29: "DG",
|
| 159 |
-
30: "DC",
|
| 160 |
-
31: "DT",
|
| 161 |
-
32: "DN",
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
# =============================================================================
|
| 165 |
-
# Charged atoms at physiological pH
|
| 166 |
-
# =============================================================================
|
| 167 |
-
|
| 168 |
-
CHARGED_ATOMS: dict[tuple[str, str], int] = {
|
| 169 |
-
("LYS", "NZ"): 1,
|
| 170 |
-
("ARG", "NH2"): 1,
|
| 171 |
-
("HIS", "ND1"): 1,
|
| 172 |
-
("PO4", "O2"): -1,
|
| 173 |
-
("PO4", "O3"): -1,
|
| 174 |
-
("PO4", "O4"): -1,
|
| 175 |
-
("SO4", "O3"): -1,
|
| 176 |
-
("SO4", "O4"): -1,
|
| 177 |
-
("MG", "MG"): 2,
|
| 178 |
-
("ZN", "ZN"): 2,
|
| 179 |
-
("CA", "CA"): 2,
|
| 180 |
-
("FE2", "FE"): 2,
|
| 181 |
-
("MN", "MN"): 2,
|
| 182 |
-
("CO", "CO"): 2,
|
| 183 |
-
("NCO", "CO"): 3,
|
| 184 |
-
("CU", "CU"): 2,
|
| 185 |
-
("NI", "NI"): 2,
|
| 186 |
-
("K", "K"): 1,
|
| 187 |
-
("NA", "NA"): 1,
|
| 188 |
-
("CD", "CD"): 2,
|
| 189 |
-
("CL", "CL"): -1,
|
| 190 |
-
("ACT", "OXT"): -1,
|
| 191 |
-
("NAD", "O2N"): -1,
|
| 192 |
-
("NAD", "N1N"): 1,
|
| 193 |
-
("NAP", "O2N"): -1,
|
| 194 |
-
("NAP", "N1N"): 1,
|
| 195 |
-
("IMD", "N3"): 1,
|
| 196 |
-
("SAM", "SD"): 1,
|
| 197 |
-
("FE", "FE"): 3,
|
| 198 |
-
("A1BH3", "N3"): 1,
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
# =============================================================================
|
| 202 |
-
# Element atomic numbers (Z=1 to 92)
|
| 203 |
-
# =============================================================================
|
| 204 |
-
|
| 205 |
-
ELEMENT_TO_ATOMIC_NUM = {
|
| 206 |
-
"H": 1,
|
| 207 |
-
"LI": 3,
|
| 208 |
-
"BE": 4,
|
| 209 |
-
"B": 5,
|
| 210 |
-
"C": 6,
|
| 211 |
-
"N": 7,
|
| 212 |
-
"O": 8,
|
| 213 |
-
"F": 9,
|
| 214 |
-
"NE": 10,
|
| 215 |
-
"NA": 11,
|
| 216 |
-
"MG": 12,
|
| 217 |
-
"AL": 13,
|
| 218 |
-
"SI": 14,
|
| 219 |
-
"P": 15,
|
| 220 |
-
"S": 16,
|
| 221 |
-
"CL": 17,
|
| 222 |
-
"AR": 18,
|
| 223 |
-
"K": 19,
|
| 224 |
-
"CA": 20,
|
| 225 |
-
"SC": 21,
|
| 226 |
-
"TI": 22,
|
| 227 |
-
"V": 23,
|
| 228 |
-
"CR": 24,
|
| 229 |
-
"MN": 25,
|
| 230 |
-
"FE": 26,
|
| 231 |
-
"CO": 27,
|
| 232 |
-
"NI": 28,
|
| 233 |
-
"CU": 29,
|
| 234 |
-
"ZN": 30,
|
| 235 |
-
"GA": 31,
|
| 236 |
-
"GE": 32,
|
| 237 |
-
"AS": 33,
|
| 238 |
-
"SE": 34,
|
| 239 |
-
"BR": 35,
|
| 240 |
-
"KR": 36,
|
| 241 |
-
"RB": 37,
|
| 242 |
-
"SR": 38,
|
| 243 |
-
"Y": 39,
|
| 244 |
-
"ZR": 40,
|
| 245 |
-
"NB": 41,
|
| 246 |
-
"MO": 42,
|
| 247 |
-
"TC": 43,
|
| 248 |
-
"RU": 44,
|
| 249 |
-
"RH": 45,
|
| 250 |
-
"PD": 46,
|
| 251 |
-
"AG": 47,
|
| 252 |
-
"CD": 48,
|
| 253 |
-
"IN": 49,
|
| 254 |
-
"SN": 50,
|
| 255 |
-
"SB": 51,
|
| 256 |
-
"TE": 52,
|
| 257 |
-
"I": 53,
|
| 258 |
-
"XE": 54,
|
| 259 |
-
"CS": 55,
|
| 260 |
-
"BA": 56,
|
| 261 |
-
"LA": 57,
|
| 262 |
-
"CE": 58,
|
| 263 |
-
"PR": 59,
|
| 264 |
-
"ND": 60,
|
| 265 |
-
"PM": 61,
|
| 266 |
-
"SM": 62,
|
| 267 |
-
"EU": 63,
|
| 268 |
-
"GD": 64,
|
| 269 |
-
"TB": 65,
|
| 270 |
-
"DY": 66,
|
| 271 |
-
"HO": 67,
|
| 272 |
-
"ER": 68,
|
| 273 |
-
"TM": 69,
|
| 274 |
-
"YB": 70,
|
| 275 |
-
"LU": 71,
|
| 276 |
-
"HF": 72,
|
| 277 |
-
"TA": 73,
|
| 278 |
-
"W": 74,
|
| 279 |
-
"RE": 75,
|
| 280 |
-
"OS": 76,
|
| 281 |
-
"IR": 77,
|
| 282 |
-
"PT": 78,
|
| 283 |
-
"AU": 79,
|
| 284 |
-
"HG": 80,
|
| 285 |
-
"TL": 81,
|
| 286 |
-
"PB": 82,
|
| 287 |
-
"BI": 83,
|
| 288 |
-
"PO": 84,
|
| 289 |
-
"AT": 85,
|
| 290 |
-
"RN": 86,
|
| 291 |
-
"FR": 87,
|
| 292 |
-
"RA": 88,
|
| 293 |
-
"AC": 89,
|
| 294 |
-
"TH": 90,
|
| 295 |
-
"PA": 91,
|
| 296 |
-
"U": 92,
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
# Inverse mapping: atomic number → element symbol
|
| 300 |
-
ELEMENT_NUMBER_TO_SYMBOL = {v: k for k, v in ELEMENT_TO_ATOMIC_NUM.items()}
|
| 301 |
-
|
| 302 |
-
# =============================================================================
|
| 303 |
-
# Standard heavy atoms per residue type
|
| 304 |
-
# =============================================================================
|
| 305 |
-
|
| 306 |
-
PROTEIN_HEAVY_ATOMS = {
|
| 307 |
-
"ALA": ["N", "CA", "C", "O", "CB"],
|
| 308 |
-
"ARG": ["N", "CA", "C", "O", "CB", "CG", "CD", "NE", "CZ", "NH1", "NH2"],
|
| 309 |
-
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2"],
|
| 310 |
-
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2"],
|
| 311 |
-
"CYS": ["N", "CA", "C", "O", "CB", "SG"],
|
| 312 |
-
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2"],
|
| 313 |
-
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2"],
|
| 314 |
-
"GLY": ["N", "CA", "C", "O"],
|
| 315 |
-
"HIS": ["N", "CA", "C", "O", "CB", "CG", "ND1", "CD2", "CE1", "NE2"],
|
| 316 |
-
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1"],
|
| 317 |
-
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2"],
|
| 318 |
-
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ"],
|
| 319 |
-
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 320 |
-
"PHE": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ"],
|
| 321 |
-
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD"],
|
| 322 |
-
"SER": ["N", "CA", "C", "O", "CB", "OG"],
|
| 323 |
-
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2"],
|
| 324 |
-
"TRP": [
|
| 325 |
-
"N",
|
| 326 |
-
"CA",
|
| 327 |
-
"C",
|
| 328 |
-
"O",
|
| 329 |
-
"CB",
|
| 330 |
-
"CG",
|
| 331 |
-
"CD1",
|
| 332 |
-
"CD2",
|
| 333 |
-
"NE1",
|
| 334 |
-
"CE2",
|
| 335 |
-
"CE3",
|
| 336 |
-
"CZ2",
|
| 337 |
-
"CZ3",
|
| 338 |
-
"CH2",
|
| 339 |
-
],
|
| 340 |
-
"TYR": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "OH"],
|
| 341 |
-
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2"],
|
| 342 |
-
"MSE": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 343 |
-
"UNK": ["N", "CA", "C", "O"],
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
DNA_HEAVY_ATOMS = {
|
| 347 |
-
"DA": [
|
| 348 |
-
"P",
|
| 349 |
-
"OP1",
|
| 350 |
-
"OP2",
|
| 351 |
-
"O5'",
|
| 352 |
-
"C5'",
|
| 353 |
-
"C4'",
|
| 354 |
-
"O4'",
|
| 355 |
-
"C3'",
|
| 356 |
-
"O3'",
|
| 357 |
-
"C2'",
|
| 358 |
-
"C1'",
|
| 359 |
-
"N9",
|
| 360 |
-
"C8",
|
| 361 |
-
"N7",
|
| 362 |
-
"C5",
|
| 363 |
-
"C6",
|
| 364 |
-
"N6",
|
| 365 |
-
"N1",
|
| 366 |
-
"C2",
|
| 367 |
-
"N3",
|
| 368 |
-
"C4",
|
| 369 |
-
],
|
| 370 |
-
"DG": [
|
| 371 |
-
"P",
|
| 372 |
-
"OP1",
|
| 373 |
-
"OP2",
|
| 374 |
-
"O5'",
|
| 375 |
-
"C5'",
|
| 376 |
-
"C4'",
|
| 377 |
-
"O4'",
|
| 378 |
-
"C3'",
|
| 379 |
-
"O3'",
|
| 380 |
-
"C2'",
|
| 381 |
-
"C1'",
|
| 382 |
-
"N9",
|
| 383 |
-
"C8",
|
| 384 |
-
"N7",
|
| 385 |
-
"C5",
|
| 386 |
-
"C6",
|
| 387 |
-
"O6",
|
| 388 |
-
"N1",
|
| 389 |
-
"C2",
|
| 390 |
-
"N2",
|
| 391 |
-
"N3",
|
| 392 |
-
"C4",
|
| 393 |
-
],
|
| 394 |
-
"DC": [
|
| 395 |
-
"P",
|
| 396 |
-
"OP1",
|
| 397 |
-
"OP2",
|
| 398 |
-
"O5'",
|
| 399 |
-
"C5'",
|
| 400 |
-
"C4'",
|
| 401 |
-
"O4'",
|
| 402 |
-
"C3'",
|
| 403 |
-
"O3'",
|
| 404 |
-
"C2'",
|
| 405 |
-
"C1'",
|
| 406 |
-
"N1",
|
| 407 |
-
"C2",
|
| 408 |
-
"O2",
|
| 409 |
-
"N3",
|
| 410 |
-
"C4",
|
| 411 |
-
"N4",
|
| 412 |
-
"C5",
|
| 413 |
-
"C6",
|
| 414 |
-
],
|
| 415 |
-
"DT": [
|
| 416 |
-
"P",
|
| 417 |
-
"OP1",
|
| 418 |
-
"OP2",
|
| 419 |
-
"O5'",
|
| 420 |
-
"C5'",
|
| 421 |
-
"C4'",
|
| 422 |
-
"O4'",
|
| 423 |
-
"C3'",
|
| 424 |
-
"O3'",
|
| 425 |
-
"C2'",
|
| 426 |
-
"C1'",
|
| 427 |
-
"N1",
|
| 428 |
-
"C2",
|
| 429 |
-
"O2",
|
| 430 |
-
"N3",
|
| 431 |
-
"C4",
|
| 432 |
-
"O4",
|
| 433 |
-
"C5",
|
| 434 |
-
"C7",
|
| 435 |
-
"C6",
|
| 436 |
-
],
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
RNA_HEAVY_ATOMS = {
|
| 440 |
-
"A": [
|
| 441 |
-
"P",
|
| 442 |
-
"OP1",
|
| 443 |
-
"OP2",
|
| 444 |
-
"O5'",
|
| 445 |
-
"C5'",
|
| 446 |
-
"C4'",
|
| 447 |
-
"O4'",
|
| 448 |
-
"C3'",
|
| 449 |
-
"O3'",
|
| 450 |
-
"C2'",
|
| 451 |
-
"O2'",
|
| 452 |
-
"C1'",
|
| 453 |
-
"N9",
|
| 454 |
-
"C8",
|
| 455 |
-
"N7",
|
| 456 |
-
"C5",
|
| 457 |
-
"C6",
|
| 458 |
-
"N6",
|
| 459 |
-
"N1",
|
| 460 |
-
"C2",
|
| 461 |
-
"N3",
|
| 462 |
-
"C4",
|
| 463 |
-
],
|
| 464 |
-
"G": [
|
| 465 |
-
"P",
|
| 466 |
-
"OP1",
|
| 467 |
-
"OP2",
|
| 468 |
-
"O5'",
|
| 469 |
-
"C5'",
|
| 470 |
-
"C4'",
|
| 471 |
-
"O4'",
|
| 472 |
-
"C3'",
|
| 473 |
-
"O3'",
|
| 474 |
-
"C2'",
|
| 475 |
-
"O2'",
|
| 476 |
-
"C1'",
|
| 477 |
-
"N9",
|
| 478 |
-
"C8",
|
| 479 |
-
"N7",
|
| 480 |
-
"C5",
|
| 481 |
-
"C6",
|
| 482 |
-
"O6",
|
| 483 |
-
"N1",
|
| 484 |
-
"C2",
|
| 485 |
-
"N2",
|
| 486 |
-
"N3",
|
| 487 |
-
"C4",
|
| 488 |
-
],
|
| 489 |
-
"C": [
|
| 490 |
-
"P",
|
| 491 |
-
"OP1",
|
| 492 |
-
"OP2",
|
| 493 |
-
"O5'",
|
| 494 |
-
"C5'",
|
| 495 |
-
"C4'",
|
| 496 |
-
"O4'",
|
| 497 |
-
"C3'",
|
| 498 |
-
"O3'",
|
| 499 |
-
"C2'",
|
| 500 |
-
"O2'",
|
| 501 |
-
"C1'",
|
| 502 |
-
"N1",
|
| 503 |
-
"C2",
|
| 504 |
-
"O2",
|
| 505 |
-
"N3",
|
| 506 |
-
"C4",
|
| 507 |
-
"N4",
|
| 508 |
-
"C5",
|
| 509 |
-
"C6",
|
| 510 |
-
],
|
| 511 |
-
"U": [
|
| 512 |
-
"P",
|
| 513 |
-
"OP1",
|
| 514 |
-
"OP2",
|
| 515 |
-
"O5'",
|
| 516 |
-
"C5'",
|
| 517 |
-
"C4'",
|
| 518 |
-
"O4'",
|
| 519 |
-
"C3'",
|
| 520 |
-
"O3'",
|
| 521 |
-
"C2'",
|
| 522 |
-
"O2'",
|
| 523 |
-
"C1'",
|
| 524 |
-
"N1",
|
| 525 |
-
"C2",
|
| 526 |
-
"O2",
|
| 527 |
-
"N3",
|
| 528 |
-
"C4",
|
| 529 |
-
"O4",
|
| 530 |
-
"C5",
|
| 531 |
-
"C6",
|
| 532 |
-
],
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
# Unknown nucleotide backbone atoms
|
| 536 |
-
DNA_BACKBONE_ATOMS = [
|
| 537 |
-
"P",
|
| 538 |
-
"OP1",
|
| 539 |
-
"OP2",
|
| 540 |
-
"O5'",
|
| 541 |
-
"C5'",
|
| 542 |
-
"C4'",
|
| 543 |
-
"O4'",
|
| 544 |
-
"C3'",
|
| 545 |
-
"O3'",
|
| 546 |
-
"C2'",
|
| 547 |
-
"C1'",
|
| 548 |
-
]
|
| 549 |
-
RNA_BACKBONE_ATOMS = [
|
| 550 |
-
"P",
|
| 551 |
-
"OP1",
|
| 552 |
-
"OP2",
|
| 553 |
-
"O5'",
|
| 554 |
-
"C5'",
|
| 555 |
-
"C4'",
|
| 556 |
-
"O4'",
|
| 557 |
-
"C3'",
|
| 558 |
-
"O3'",
|
| 559 |
-
"C2'",
|
| 560 |
-
"O2'",
|
| 561 |
-
"C1'",
|
| 562 |
-
]
|
|
|
|
| 1 |
+
"""Constants for the ESMFold2 input pipeline.
|
| 2 |
+
|
| 3 |
+
Includes molecule types, residue types, vocabularies, atom lists, and element data.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# =============================================================================
|
| 7 |
+
# Molecule types
|
| 8 |
+
# =============================================================================
|
| 9 |
+
|
| 10 |
+
MOL_TYPE_PROTEIN = 0
|
| 11 |
+
MOL_TYPE_DNA = 1
|
| 12 |
+
MOL_TYPE_RNA = 2
|
| 13 |
+
MOL_TYPE_NONPOLYMER = 3
|
| 14 |
+
|
| 15 |
+
# =============================================================================
|
| 16 |
+
# Residue type indices
|
| 17 |
+
# =============================================================================
|
| 18 |
+
|
| 19 |
+
# Standard amino acids (indices 2-21), MSE mapped to MET
|
| 20 |
+
PROTEIN_RESIDUE_TO_RES_TYPE = {
|
| 21 |
+
"ALA": 2,
|
| 22 |
+
"ARG": 3,
|
| 23 |
+
"ASN": 4,
|
| 24 |
+
"ASP": 5,
|
| 25 |
+
"CYS": 6,
|
| 26 |
+
"GLN": 7,
|
| 27 |
+
"GLU": 8,
|
| 28 |
+
"GLY": 9,
|
| 29 |
+
"HIS": 10,
|
| 30 |
+
"ILE": 11,
|
| 31 |
+
"LEU": 12,
|
| 32 |
+
"LYS": 13,
|
| 33 |
+
"MET": 14,
|
| 34 |
+
"PHE": 15,
|
| 35 |
+
"PRO": 16,
|
| 36 |
+
"SER": 17,
|
| 37 |
+
"THR": 18,
|
| 38 |
+
"TRP": 19,
|
| 39 |
+
"TYR": 20,
|
| 40 |
+
"VAL": 21,
|
| 41 |
+
"MSE": 14, # Selenomethionine -> MET
|
| 42 |
+
}
|
| 43 |
+
PROTEIN_UNK_RES_TYPE = 22
|
| 44 |
+
|
| 45 |
+
# RNA nucleotides (indices 23-26, unknown=27)
|
| 46 |
+
RNA_RESIDUE_TO_RES_TYPE = {"A": 23, "G": 24, "C": 25, "U": 26}
|
| 47 |
+
RNA_UNK_RES_TYPE = 27
|
| 48 |
+
|
| 49 |
+
# DNA nucleotides (indices 28-31, unknown=32)
|
| 50 |
+
DNA_RESIDUE_TO_RES_TYPE = {"DA": 28, "DG": 29, "DC": 30, "DT": 31}
|
| 51 |
+
DNA_UNK_RES_TYPE = 32
|
| 52 |
+
|
| 53 |
+
GAP_RES_TYPE = 32
|
| 54 |
+
|
| 55 |
+
# =============================================================================
|
| 56 |
+
# Vocabularies
|
| 57 |
+
# =============================================================================
|
| 58 |
+
|
| 59 |
+
# 3-letter to 1-letter codes for proteins
|
| 60 |
+
PROTEIN_3TO1 = {
|
| 61 |
+
"ALA": "A",
|
| 62 |
+
"ARG": "R",
|
| 63 |
+
"ASN": "N",
|
| 64 |
+
"ASP": "D",
|
| 65 |
+
"CYS": "C",
|
| 66 |
+
"GLN": "Q",
|
| 67 |
+
"GLU": "E",
|
| 68 |
+
"GLY": "G",
|
| 69 |
+
"HIS": "H",
|
| 70 |
+
"ILE": "I",
|
| 71 |
+
"LEU": "L",
|
| 72 |
+
"LYS": "K",
|
| 73 |
+
"MET": "M",
|
| 74 |
+
"PHE": "F",
|
| 75 |
+
"PRO": "P",
|
| 76 |
+
"SER": "S",
|
| 77 |
+
"THR": "T",
|
| 78 |
+
"TRP": "W",
|
| 79 |
+
"TYR": "Y",
|
| 80 |
+
"VAL": "V",
|
| 81 |
+
"MSE": "M",
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# 1-letter to 3-letter codes
|
| 85 |
+
PROTEIN_1TO3 = {v: k for k, v in PROTEIN_3TO1.items() if k != "MSE"}
|
| 86 |
+
PROTEIN_1TO3["X"] = "UNK"
|
| 87 |
+
|
| 88 |
+
# DNA 1-letter to CCD code
|
| 89 |
+
DNA_1TO3 = {"A": "DA", "T": "DT", "C": "DC", "G": "DG"}
|
| 90 |
+
|
| 91 |
+
# RNA 1-letter to CCD code
|
| 92 |
+
RNA_1TO3 = {"A": "A", "U": "U", "C": "C", "G": "G"}
|
| 93 |
+
|
| 94 |
+
# ESM-2 input_ids vocabulary for proteins
|
| 95 |
+
ESM_PROTEIN_VOCAB = {
|
| 96 |
+
"L": 4,
|
| 97 |
+
"A": 5,
|
| 98 |
+
"G": 6,
|
| 99 |
+
"V": 7,
|
| 100 |
+
"S": 8,
|
| 101 |
+
"E": 9,
|
| 102 |
+
"R": 10,
|
| 103 |
+
"T": 11,
|
| 104 |
+
"I": 12,
|
| 105 |
+
"D": 13,
|
| 106 |
+
"P": 14,
|
| 107 |
+
"K": 15,
|
| 108 |
+
"Q": 16,
|
| 109 |
+
"N": 17,
|
| 110 |
+
"F": 18,
|
| 111 |
+
"Y": 19,
|
| 112 |
+
"M": 20,
|
| 113 |
+
"H": 21,
|
| 114 |
+
"W": 22,
|
| 115 |
+
"C": 23,
|
| 116 |
+
"X": 3, # Unknown
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
# For DNA/RNA/ligands
|
| 120 |
+
DNA_RNA_LIGAND_INPUT_ID = 24
|
| 121 |
+
|
| 122 |
+
# MSA tokens
|
| 123 |
+
MSA_PAD_TOKEN_ID = 0
|
| 124 |
+
MSA_GAP_TOKEN_ID = 1 # Gap/insertion token for MSA
|
| 125 |
+
|
| 126 |
+
# res_type int -> CCD component ID (for conformer lookup)
|
| 127 |
+
RES_TYPE_TO_CCD = {
|
| 128 |
+
# Proteins (2-22)
|
| 129 |
+
2: "ALA",
|
| 130 |
+
3: "ARG",
|
| 131 |
+
4: "ASN",
|
| 132 |
+
5: "ASP",
|
| 133 |
+
6: "CYS",
|
| 134 |
+
7: "GLN",
|
| 135 |
+
8: "GLU",
|
| 136 |
+
9: "GLY",
|
| 137 |
+
10: "HIS",
|
| 138 |
+
11: "ILE",
|
| 139 |
+
12: "LEU",
|
| 140 |
+
13: "LYS",
|
| 141 |
+
14: "MET",
|
| 142 |
+
15: "PHE",
|
| 143 |
+
16: "PRO",
|
| 144 |
+
17: "SER",
|
| 145 |
+
18: "THR",
|
| 146 |
+
19: "TRP",
|
| 147 |
+
20: "TYR",
|
| 148 |
+
21: "VAL",
|
| 149 |
+
22: "UNK",
|
| 150 |
+
# RNA (23-27)
|
| 151 |
+
23: "A",
|
| 152 |
+
24: "G",
|
| 153 |
+
25: "C",
|
| 154 |
+
26: "U",
|
| 155 |
+
27: "N",
|
| 156 |
+
# DNA (28-32)
|
| 157 |
+
28: "DA",
|
| 158 |
+
29: "DG",
|
| 159 |
+
30: "DC",
|
| 160 |
+
31: "DT",
|
| 161 |
+
32: "DN",
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
# =============================================================================
|
| 165 |
+
# Charged atoms at physiological pH
|
| 166 |
+
# =============================================================================
|
| 167 |
+
|
| 168 |
+
CHARGED_ATOMS: dict[tuple[str, str], int] = {
|
| 169 |
+
("LYS", "NZ"): 1,
|
| 170 |
+
("ARG", "NH2"): 1,
|
| 171 |
+
("HIS", "ND1"): 1,
|
| 172 |
+
("PO4", "O2"): -1,
|
| 173 |
+
("PO4", "O3"): -1,
|
| 174 |
+
("PO4", "O4"): -1,
|
| 175 |
+
("SO4", "O3"): -1,
|
| 176 |
+
("SO4", "O4"): -1,
|
| 177 |
+
("MG", "MG"): 2,
|
| 178 |
+
("ZN", "ZN"): 2,
|
| 179 |
+
("CA", "CA"): 2,
|
| 180 |
+
("FE2", "FE"): 2,
|
| 181 |
+
("MN", "MN"): 2,
|
| 182 |
+
("CO", "CO"): 2,
|
| 183 |
+
("NCO", "CO"): 3,
|
| 184 |
+
("CU", "CU"): 2,
|
| 185 |
+
("NI", "NI"): 2,
|
| 186 |
+
("K", "K"): 1,
|
| 187 |
+
("NA", "NA"): 1,
|
| 188 |
+
("CD", "CD"): 2,
|
| 189 |
+
("CL", "CL"): -1,
|
| 190 |
+
("ACT", "OXT"): -1,
|
| 191 |
+
("NAD", "O2N"): -1,
|
| 192 |
+
("NAD", "N1N"): 1,
|
| 193 |
+
("NAP", "O2N"): -1,
|
| 194 |
+
("NAP", "N1N"): 1,
|
| 195 |
+
("IMD", "N3"): 1,
|
| 196 |
+
("SAM", "SD"): 1,
|
| 197 |
+
("FE", "FE"): 3,
|
| 198 |
+
("A1BH3", "N3"): 1,
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
# =============================================================================
|
| 202 |
+
# Element atomic numbers (Z=1 to 92)
|
| 203 |
+
# =============================================================================
|
| 204 |
+
|
| 205 |
+
ELEMENT_TO_ATOMIC_NUM = {
|
| 206 |
+
"H": 1,
|
| 207 |
+
"LI": 3,
|
| 208 |
+
"BE": 4,
|
| 209 |
+
"B": 5,
|
| 210 |
+
"C": 6,
|
| 211 |
+
"N": 7,
|
| 212 |
+
"O": 8,
|
| 213 |
+
"F": 9,
|
| 214 |
+
"NE": 10,
|
| 215 |
+
"NA": 11,
|
| 216 |
+
"MG": 12,
|
| 217 |
+
"AL": 13,
|
| 218 |
+
"SI": 14,
|
| 219 |
+
"P": 15,
|
| 220 |
+
"S": 16,
|
| 221 |
+
"CL": 17,
|
| 222 |
+
"AR": 18,
|
| 223 |
+
"K": 19,
|
| 224 |
+
"CA": 20,
|
| 225 |
+
"SC": 21,
|
| 226 |
+
"TI": 22,
|
| 227 |
+
"V": 23,
|
| 228 |
+
"CR": 24,
|
| 229 |
+
"MN": 25,
|
| 230 |
+
"FE": 26,
|
| 231 |
+
"CO": 27,
|
| 232 |
+
"NI": 28,
|
| 233 |
+
"CU": 29,
|
| 234 |
+
"ZN": 30,
|
| 235 |
+
"GA": 31,
|
| 236 |
+
"GE": 32,
|
| 237 |
+
"AS": 33,
|
| 238 |
+
"SE": 34,
|
| 239 |
+
"BR": 35,
|
| 240 |
+
"KR": 36,
|
| 241 |
+
"RB": 37,
|
| 242 |
+
"SR": 38,
|
| 243 |
+
"Y": 39,
|
| 244 |
+
"ZR": 40,
|
| 245 |
+
"NB": 41,
|
| 246 |
+
"MO": 42,
|
| 247 |
+
"TC": 43,
|
| 248 |
+
"RU": 44,
|
| 249 |
+
"RH": 45,
|
| 250 |
+
"PD": 46,
|
| 251 |
+
"AG": 47,
|
| 252 |
+
"CD": 48,
|
| 253 |
+
"IN": 49,
|
| 254 |
+
"SN": 50,
|
| 255 |
+
"SB": 51,
|
| 256 |
+
"TE": 52,
|
| 257 |
+
"I": 53,
|
| 258 |
+
"XE": 54,
|
| 259 |
+
"CS": 55,
|
| 260 |
+
"BA": 56,
|
| 261 |
+
"LA": 57,
|
| 262 |
+
"CE": 58,
|
| 263 |
+
"PR": 59,
|
| 264 |
+
"ND": 60,
|
| 265 |
+
"PM": 61,
|
| 266 |
+
"SM": 62,
|
| 267 |
+
"EU": 63,
|
| 268 |
+
"GD": 64,
|
| 269 |
+
"TB": 65,
|
| 270 |
+
"DY": 66,
|
| 271 |
+
"HO": 67,
|
| 272 |
+
"ER": 68,
|
| 273 |
+
"TM": 69,
|
| 274 |
+
"YB": 70,
|
| 275 |
+
"LU": 71,
|
| 276 |
+
"HF": 72,
|
| 277 |
+
"TA": 73,
|
| 278 |
+
"W": 74,
|
| 279 |
+
"RE": 75,
|
| 280 |
+
"OS": 76,
|
| 281 |
+
"IR": 77,
|
| 282 |
+
"PT": 78,
|
| 283 |
+
"AU": 79,
|
| 284 |
+
"HG": 80,
|
| 285 |
+
"TL": 81,
|
| 286 |
+
"PB": 82,
|
| 287 |
+
"BI": 83,
|
| 288 |
+
"PO": 84,
|
| 289 |
+
"AT": 85,
|
| 290 |
+
"RN": 86,
|
| 291 |
+
"FR": 87,
|
| 292 |
+
"RA": 88,
|
| 293 |
+
"AC": 89,
|
| 294 |
+
"TH": 90,
|
| 295 |
+
"PA": 91,
|
| 296 |
+
"U": 92,
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
# Inverse mapping: atomic number → element symbol
|
| 300 |
+
ELEMENT_NUMBER_TO_SYMBOL = {v: k for k, v in ELEMENT_TO_ATOMIC_NUM.items()}
|
| 301 |
+
|
| 302 |
+
# =============================================================================
|
| 303 |
+
# Standard heavy atoms per residue type
|
| 304 |
+
# =============================================================================
|
| 305 |
+
|
| 306 |
+
PROTEIN_HEAVY_ATOMS = {
|
| 307 |
+
"ALA": ["N", "CA", "C", "O", "CB"],
|
| 308 |
+
"ARG": ["N", "CA", "C", "O", "CB", "CG", "CD", "NE", "CZ", "NH1", "NH2"],
|
| 309 |
+
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2"],
|
| 310 |
+
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2"],
|
| 311 |
+
"CYS": ["N", "CA", "C", "O", "CB", "SG"],
|
| 312 |
+
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2"],
|
| 313 |
+
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2"],
|
| 314 |
+
"GLY": ["N", "CA", "C", "O"],
|
| 315 |
+
"HIS": ["N", "CA", "C", "O", "CB", "CG", "ND1", "CD2", "CE1", "NE2"],
|
| 316 |
+
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1"],
|
| 317 |
+
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2"],
|
| 318 |
+
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ"],
|
| 319 |
+
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 320 |
+
"PHE": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ"],
|
| 321 |
+
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD"],
|
| 322 |
+
"SER": ["N", "CA", "C", "O", "CB", "OG"],
|
| 323 |
+
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2"],
|
| 324 |
+
"TRP": [
|
| 325 |
+
"N",
|
| 326 |
+
"CA",
|
| 327 |
+
"C",
|
| 328 |
+
"O",
|
| 329 |
+
"CB",
|
| 330 |
+
"CG",
|
| 331 |
+
"CD1",
|
| 332 |
+
"CD2",
|
| 333 |
+
"NE1",
|
| 334 |
+
"CE2",
|
| 335 |
+
"CE3",
|
| 336 |
+
"CZ2",
|
| 337 |
+
"CZ3",
|
| 338 |
+
"CH2",
|
| 339 |
+
],
|
| 340 |
+
"TYR": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "OH"],
|
| 341 |
+
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2"],
|
| 342 |
+
"MSE": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 343 |
+
"UNK": ["N", "CA", "C", "O"],
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
DNA_HEAVY_ATOMS = {
|
| 347 |
+
"DA": [
|
| 348 |
+
"P",
|
| 349 |
+
"OP1",
|
| 350 |
+
"OP2",
|
| 351 |
+
"O5'",
|
| 352 |
+
"C5'",
|
| 353 |
+
"C4'",
|
| 354 |
+
"O4'",
|
| 355 |
+
"C3'",
|
| 356 |
+
"O3'",
|
| 357 |
+
"C2'",
|
| 358 |
+
"C1'",
|
| 359 |
+
"N9",
|
| 360 |
+
"C8",
|
| 361 |
+
"N7",
|
| 362 |
+
"C5",
|
| 363 |
+
"C6",
|
| 364 |
+
"N6",
|
| 365 |
+
"N1",
|
| 366 |
+
"C2",
|
| 367 |
+
"N3",
|
| 368 |
+
"C4",
|
| 369 |
+
],
|
| 370 |
+
"DG": [
|
| 371 |
+
"P",
|
| 372 |
+
"OP1",
|
| 373 |
+
"OP2",
|
| 374 |
+
"O5'",
|
| 375 |
+
"C5'",
|
| 376 |
+
"C4'",
|
| 377 |
+
"O4'",
|
| 378 |
+
"C3'",
|
| 379 |
+
"O3'",
|
| 380 |
+
"C2'",
|
| 381 |
+
"C1'",
|
| 382 |
+
"N9",
|
| 383 |
+
"C8",
|
| 384 |
+
"N7",
|
| 385 |
+
"C5",
|
| 386 |
+
"C6",
|
| 387 |
+
"O6",
|
| 388 |
+
"N1",
|
| 389 |
+
"C2",
|
| 390 |
+
"N2",
|
| 391 |
+
"N3",
|
| 392 |
+
"C4",
|
| 393 |
+
],
|
| 394 |
+
"DC": [
|
| 395 |
+
"P",
|
| 396 |
+
"OP1",
|
| 397 |
+
"OP2",
|
| 398 |
+
"O5'",
|
| 399 |
+
"C5'",
|
| 400 |
+
"C4'",
|
| 401 |
+
"O4'",
|
| 402 |
+
"C3'",
|
| 403 |
+
"O3'",
|
| 404 |
+
"C2'",
|
| 405 |
+
"C1'",
|
| 406 |
+
"N1",
|
| 407 |
+
"C2",
|
| 408 |
+
"O2",
|
| 409 |
+
"N3",
|
| 410 |
+
"C4",
|
| 411 |
+
"N4",
|
| 412 |
+
"C5",
|
| 413 |
+
"C6",
|
| 414 |
+
],
|
| 415 |
+
"DT": [
|
| 416 |
+
"P",
|
| 417 |
+
"OP1",
|
| 418 |
+
"OP2",
|
| 419 |
+
"O5'",
|
| 420 |
+
"C5'",
|
| 421 |
+
"C4'",
|
| 422 |
+
"O4'",
|
| 423 |
+
"C3'",
|
| 424 |
+
"O3'",
|
| 425 |
+
"C2'",
|
| 426 |
+
"C1'",
|
| 427 |
+
"N1",
|
| 428 |
+
"C2",
|
| 429 |
+
"O2",
|
| 430 |
+
"N3",
|
| 431 |
+
"C4",
|
| 432 |
+
"O4",
|
| 433 |
+
"C5",
|
| 434 |
+
"C7",
|
| 435 |
+
"C6",
|
| 436 |
+
],
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
RNA_HEAVY_ATOMS = {
|
| 440 |
+
"A": [
|
| 441 |
+
"P",
|
| 442 |
+
"OP1",
|
| 443 |
+
"OP2",
|
| 444 |
+
"O5'",
|
| 445 |
+
"C5'",
|
| 446 |
+
"C4'",
|
| 447 |
+
"O4'",
|
| 448 |
+
"C3'",
|
| 449 |
+
"O3'",
|
| 450 |
+
"C2'",
|
| 451 |
+
"O2'",
|
| 452 |
+
"C1'",
|
| 453 |
+
"N9",
|
| 454 |
+
"C8",
|
| 455 |
+
"N7",
|
| 456 |
+
"C5",
|
| 457 |
+
"C6",
|
| 458 |
+
"N6",
|
| 459 |
+
"N1",
|
| 460 |
+
"C2",
|
| 461 |
+
"N3",
|
| 462 |
+
"C4",
|
| 463 |
+
],
|
| 464 |
+
"G": [
|
| 465 |
+
"P",
|
| 466 |
+
"OP1",
|
| 467 |
+
"OP2",
|
| 468 |
+
"O5'",
|
| 469 |
+
"C5'",
|
| 470 |
+
"C4'",
|
| 471 |
+
"O4'",
|
| 472 |
+
"C3'",
|
| 473 |
+
"O3'",
|
| 474 |
+
"C2'",
|
| 475 |
+
"O2'",
|
| 476 |
+
"C1'",
|
| 477 |
+
"N9",
|
| 478 |
+
"C8",
|
| 479 |
+
"N7",
|
| 480 |
+
"C5",
|
| 481 |
+
"C6",
|
| 482 |
+
"O6",
|
| 483 |
+
"N1",
|
| 484 |
+
"C2",
|
| 485 |
+
"N2",
|
| 486 |
+
"N3",
|
| 487 |
+
"C4",
|
| 488 |
+
],
|
| 489 |
+
"C": [
|
| 490 |
+
"P",
|
| 491 |
+
"OP1",
|
| 492 |
+
"OP2",
|
| 493 |
+
"O5'",
|
| 494 |
+
"C5'",
|
| 495 |
+
"C4'",
|
| 496 |
+
"O4'",
|
| 497 |
+
"C3'",
|
| 498 |
+
"O3'",
|
| 499 |
+
"C2'",
|
| 500 |
+
"O2'",
|
| 501 |
+
"C1'",
|
| 502 |
+
"N1",
|
| 503 |
+
"C2",
|
| 504 |
+
"O2",
|
| 505 |
+
"N3",
|
| 506 |
+
"C4",
|
| 507 |
+
"N4",
|
| 508 |
+
"C5",
|
| 509 |
+
"C6",
|
| 510 |
+
],
|
| 511 |
+
"U": [
|
| 512 |
+
"P",
|
| 513 |
+
"OP1",
|
| 514 |
+
"OP2",
|
| 515 |
+
"O5'",
|
| 516 |
+
"C5'",
|
| 517 |
+
"C4'",
|
| 518 |
+
"O4'",
|
| 519 |
+
"C3'",
|
| 520 |
+
"O3'",
|
| 521 |
+
"C2'",
|
| 522 |
+
"O2'",
|
| 523 |
+
"C1'",
|
| 524 |
+
"N1",
|
| 525 |
+
"C2",
|
| 526 |
+
"O2",
|
| 527 |
+
"N3",
|
| 528 |
+
"C4",
|
| 529 |
+
"O4",
|
| 530 |
+
"C5",
|
| 531 |
+
"C6",
|
| 532 |
+
],
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
# Unknown nucleotide backbone atoms
|
| 536 |
+
DNA_BACKBONE_ATOMS = [
|
| 537 |
+
"P",
|
| 538 |
+
"OP1",
|
| 539 |
+
"OP2",
|
| 540 |
+
"O5'",
|
| 541 |
+
"C5'",
|
| 542 |
+
"C4'",
|
| 543 |
+
"O4'",
|
| 544 |
+
"C3'",
|
| 545 |
+
"O3'",
|
| 546 |
+
"C2'",
|
| 547 |
+
"C1'",
|
| 548 |
+
]
|
| 549 |
+
RNA_BACKBONE_ATOMS = [
|
| 550 |
+
"P",
|
| 551 |
+
"OP1",
|
| 552 |
+
"OP2",
|
| 553 |
+
"O5'",
|
| 554 |
+
"C5'",
|
| 555 |
+
"C4'",
|
| 556 |
+
"O4'",
|
| 557 |
+
"C3'",
|
| 558 |
+
"O3'",
|
| 559 |
+
"C2'",
|
| 560 |
+
"O2'",
|
| 561 |
+
"C1'",
|
| 562 |
+
]
|
esmfold2_constants_esm3.py
CHANGED
|
@@ -1,137 +1,137 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from functools import cache
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
|
| 5 |
-
from huggingface_hub import snapshot_download
|
| 6 |
-
|
| 7 |
-
SEQUENCE_BOS_TOKEN = 0
|
| 8 |
-
SEQUENCE_PAD_TOKEN = 1
|
| 9 |
-
SEQUENCE_EOS_TOKEN = 2
|
| 10 |
-
SEQUENCE_CHAINBREAK_TOKEN = 31
|
| 11 |
-
SEQUENCE_MASK_TOKEN = 32
|
| 12 |
-
|
| 13 |
-
VQVAE_CODEBOOK_SIZE = 4096
|
| 14 |
-
VQVAE_SPECIAL_TOKENS = {
|
| 15 |
-
"MASK": VQVAE_CODEBOOK_SIZE,
|
| 16 |
-
"EOS": VQVAE_CODEBOOK_SIZE + 1,
|
| 17 |
-
"BOS": VQVAE_CODEBOOK_SIZE + 2,
|
| 18 |
-
"PAD": VQVAE_CODEBOOK_SIZE + 3,
|
| 19 |
-
"CHAINBREAK": VQVAE_CODEBOOK_SIZE + 4,
|
| 20 |
-
}
|
| 21 |
-
VQVAE_DIRECTION_LOSS_BINS = 16
|
| 22 |
-
VQVAE_PAE_BINS = 64
|
| 23 |
-
VQVAE_MAX_PAE_BIN = 31.0
|
| 24 |
-
VQVAE_PLDDT_BINS = 50
|
| 25 |
-
|
| 26 |
-
STRUCTURE_MASK_TOKEN = VQVAE_SPECIAL_TOKENS["MASK"]
|
| 27 |
-
STRUCTURE_BOS_TOKEN = VQVAE_SPECIAL_TOKENS["BOS"]
|
| 28 |
-
STRUCTURE_EOS_TOKEN = VQVAE_SPECIAL_TOKENS["EOS"]
|
| 29 |
-
STRUCTURE_PAD_TOKEN = VQVAE_SPECIAL_TOKENS["PAD"]
|
| 30 |
-
STRUCTURE_CHAINBREAK_TOKEN = VQVAE_SPECIAL_TOKENS["CHAINBREAK"]
|
| 31 |
-
STRUCTURE_UNDEFINED_TOKEN = 955
|
| 32 |
-
|
| 33 |
-
SASA_PAD_TOKEN = 0
|
| 34 |
-
|
| 35 |
-
SS8_PAD_TOKEN = 0
|
| 36 |
-
|
| 37 |
-
INTERPRO_PAD_TOKEN = 0
|
| 38 |
-
|
| 39 |
-
RESIDUE_PAD_TOKEN = 0
|
| 40 |
-
|
| 41 |
-
CHAIN_BREAK_STR = "|"
|
| 42 |
-
|
| 43 |
-
SEQUENCE_BOS_STR = "<cls>"
|
| 44 |
-
SEQUENCE_EOS_STR = "<eos>"
|
| 45 |
-
|
| 46 |
-
MASK_STR_SHORT = "_"
|
| 47 |
-
SEQUENCE_MASK_STR = "<mask>"
|
| 48 |
-
SASA_MASK_STR = "<unk>"
|
| 49 |
-
SS8_MASK_STR = "<unk>"
|
| 50 |
-
|
| 51 |
-
# fmt: off
|
| 52 |
-
SEQUENCE_VOCAB = [
|
| 53 |
-
"<cls>", "<pad>", "<eos>", "<unk>",
|
| 54 |
-
"L", "A", "G", "V", "S", "E", "R", "T", "I", "D", "P", "K",
|
| 55 |
-
"Q", "N", "F", "Y", "M", "H", "W", "C", "X", "B", "U", "Z",
|
| 56 |
-
"O", ".", "-", "|",
|
| 57 |
-
"<mask>",
|
| 58 |
-
]
|
| 59 |
-
# fmt: on
|
| 60 |
-
|
| 61 |
-
SEQUENCE_STANDARD_AA_MIN_TOKEN = 4 # L
|
| 62 |
-
SEQUENCE_STANDARD_AA_MAX_TOKEN = 24 # X (exclusive)
|
| 63 |
-
|
| 64 |
-
SSE_8CLASS_VOCAB = "GHITEBSC"
|
| 65 |
-
SSE_3CLASS_VOCAB = "HEC"
|
| 66 |
-
SSE_8CLASS_TO_3CLASS_MAP = {
|
| 67 |
-
"G": "H",
|
| 68 |
-
"H": "H",
|
| 69 |
-
"I": "H",
|
| 70 |
-
"T": "C",
|
| 71 |
-
"E": "E",
|
| 72 |
-
"B": "E",
|
| 73 |
-
"S": "C",
|
| 74 |
-
"C": "C",
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
SASA_DISCRETIZATION_BOUNDARIES = [
|
| 78 |
-
0.8,
|
| 79 |
-
4.0,
|
| 80 |
-
9.6,
|
| 81 |
-
16.4,
|
| 82 |
-
24.5,
|
| 83 |
-
32.9,
|
| 84 |
-
42.0,
|
| 85 |
-
51.5,
|
| 86 |
-
61.2,
|
| 87 |
-
70.9,
|
| 88 |
-
81.6,
|
| 89 |
-
93.3,
|
| 90 |
-
107.2,
|
| 91 |
-
125.4,
|
| 92 |
-
151.4,
|
| 93 |
-
]
|
| 94 |
-
|
| 95 |
-
MAX_RESIDUE_ANNOTATIONS = 16
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
TFIDF_VECTOR_SIZE = 58641
|
| 99 |
-
|
| 100 |
-
FUNCTION_TOKENS_DEPTH = 8
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
@staticmethod
|
| 104 |
-
@cache
|
| 105 |
-
def data_root(model: str):
|
| 106 |
-
if "INFRA_PROVIDER" in os.environ:
|
| 107 |
-
return Path("")
|
| 108 |
-
# Try to download from huggingface if it doesn't exist
|
| 109 |
-
if model.startswith("esm3"):
|
| 110 |
-
path = Path(snapshot_download(repo_id="biohub/esm3-sm-open-v1"))
|
| 111 |
-
elif model.startswith("esmc-300"):
|
| 112 |
-
path = Path(snapshot_download(repo_id="biohub/esmc-300m-2024-12"))
|
| 113 |
-
elif model.startswith("esmc-600"):
|
| 114 |
-
path = Path(snapshot_download(repo_id="biohub/esmc-600m-2024-12"))
|
| 115 |
-
elif model.startswith("esmc-6b"):
|
| 116 |
-
path = Path(snapshot_download(repo_id="biohub/esmc-6b-2024-12"))
|
| 117 |
-
else:
|
| 118 |
-
raise ValueError(f"{model=} is an invalid model name.")
|
| 119 |
-
return path
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
IN_REPO_DATA_FOLDER = Path(__file__).parents[2] / "data"
|
| 123 |
-
|
| 124 |
-
INTERPRO_ENTRY = IN_REPO_DATA_FOLDER / "entry_list_safety_29026.list"
|
| 125 |
-
INTERPRO_HIERARCHY = IN_REPO_DATA_FOLDER / "ParentChildTreeFile.txt"
|
| 126 |
-
INTERPRO2GO = IN_REPO_DATA_FOLDER / "ParentChildTreeFile.txt"
|
| 127 |
-
INTERPRO_2ID = "data/tag_dict_4_safety_filtered.json"
|
| 128 |
-
|
| 129 |
-
LSH_TABLE_PATHS = {"8bit": "data/hyperplanes_8bit_58641.npz"}
|
| 130 |
-
|
| 131 |
-
KEYWORDS_VOCABULARY = (
|
| 132 |
-
IN_REPO_DATA_FOLDER / "keyword_vocabulary_safety_filtered_58641.txt"
|
| 133 |
-
)
|
| 134 |
-
KEYWORDS_IDF = IN_REPO_DATA_FOLDER / "keyword_idf_safety_filtered_58641.npy"
|
| 135 |
-
|
| 136 |
-
RESID_CSV = "data/uniref90_and_mgnify90_residue_annotations_gt_1k_proteins.csv"
|
| 137 |
-
INTERPRO2KEYWORDS = IN_REPO_DATA_FOLDER / "interpro_29026_to_keywords_58641.csv"
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from functools import cache
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from huggingface_hub import snapshot_download
|
| 6 |
+
|
| 7 |
+
SEQUENCE_BOS_TOKEN = 0
|
| 8 |
+
SEQUENCE_PAD_TOKEN = 1
|
| 9 |
+
SEQUENCE_EOS_TOKEN = 2
|
| 10 |
+
SEQUENCE_CHAINBREAK_TOKEN = 31
|
| 11 |
+
SEQUENCE_MASK_TOKEN = 32
|
| 12 |
+
|
| 13 |
+
VQVAE_CODEBOOK_SIZE = 4096
|
| 14 |
+
VQVAE_SPECIAL_TOKENS = {
|
| 15 |
+
"MASK": VQVAE_CODEBOOK_SIZE,
|
| 16 |
+
"EOS": VQVAE_CODEBOOK_SIZE + 1,
|
| 17 |
+
"BOS": VQVAE_CODEBOOK_SIZE + 2,
|
| 18 |
+
"PAD": VQVAE_CODEBOOK_SIZE + 3,
|
| 19 |
+
"CHAINBREAK": VQVAE_CODEBOOK_SIZE + 4,
|
| 20 |
+
}
|
| 21 |
+
VQVAE_DIRECTION_LOSS_BINS = 16
|
| 22 |
+
VQVAE_PAE_BINS = 64
|
| 23 |
+
VQVAE_MAX_PAE_BIN = 31.0
|
| 24 |
+
VQVAE_PLDDT_BINS = 50
|
| 25 |
+
|
| 26 |
+
STRUCTURE_MASK_TOKEN = VQVAE_SPECIAL_TOKENS["MASK"]
|
| 27 |
+
STRUCTURE_BOS_TOKEN = VQVAE_SPECIAL_TOKENS["BOS"]
|
| 28 |
+
STRUCTURE_EOS_TOKEN = VQVAE_SPECIAL_TOKENS["EOS"]
|
| 29 |
+
STRUCTURE_PAD_TOKEN = VQVAE_SPECIAL_TOKENS["PAD"]
|
| 30 |
+
STRUCTURE_CHAINBREAK_TOKEN = VQVAE_SPECIAL_TOKENS["CHAINBREAK"]
|
| 31 |
+
STRUCTURE_UNDEFINED_TOKEN = 955
|
| 32 |
+
|
| 33 |
+
SASA_PAD_TOKEN = 0
|
| 34 |
+
|
| 35 |
+
SS8_PAD_TOKEN = 0
|
| 36 |
+
|
| 37 |
+
INTERPRO_PAD_TOKEN = 0
|
| 38 |
+
|
| 39 |
+
RESIDUE_PAD_TOKEN = 0
|
| 40 |
+
|
| 41 |
+
CHAIN_BREAK_STR = "|"
|
| 42 |
+
|
| 43 |
+
SEQUENCE_BOS_STR = "<cls>"
|
| 44 |
+
SEQUENCE_EOS_STR = "<eos>"
|
| 45 |
+
|
| 46 |
+
MASK_STR_SHORT = "_"
|
| 47 |
+
SEQUENCE_MASK_STR = "<mask>"
|
| 48 |
+
SASA_MASK_STR = "<unk>"
|
| 49 |
+
SS8_MASK_STR = "<unk>"
|
| 50 |
+
|
| 51 |
+
# fmt: off
|
| 52 |
+
SEQUENCE_VOCAB = [
|
| 53 |
+
"<cls>", "<pad>", "<eos>", "<unk>",
|
| 54 |
+
"L", "A", "G", "V", "S", "E", "R", "T", "I", "D", "P", "K",
|
| 55 |
+
"Q", "N", "F", "Y", "M", "H", "W", "C", "X", "B", "U", "Z",
|
| 56 |
+
"O", ".", "-", "|",
|
| 57 |
+
"<mask>",
|
| 58 |
+
]
|
| 59 |
+
# fmt: on
|
| 60 |
+
|
| 61 |
+
SEQUENCE_STANDARD_AA_MIN_TOKEN = 4 # L
|
| 62 |
+
SEQUENCE_STANDARD_AA_MAX_TOKEN = 24 # X (exclusive)
|
| 63 |
+
|
| 64 |
+
SSE_8CLASS_VOCAB = "GHITEBSC"
|
| 65 |
+
SSE_3CLASS_VOCAB = "HEC"
|
| 66 |
+
SSE_8CLASS_TO_3CLASS_MAP = {
|
| 67 |
+
"G": "H",
|
| 68 |
+
"H": "H",
|
| 69 |
+
"I": "H",
|
| 70 |
+
"T": "C",
|
| 71 |
+
"E": "E",
|
| 72 |
+
"B": "E",
|
| 73 |
+
"S": "C",
|
| 74 |
+
"C": "C",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
SASA_DISCRETIZATION_BOUNDARIES = [
|
| 78 |
+
0.8,
|
| 79 |
+
4.0,
|
| 80 |
+
9.6,
|
| 81 |
+
16.4,
|
| 82 |
+
24.5,
|
| 83 |
+
32.9,
|
| 84 |
+
42.0,
|
| 85 |
+
51.5,
|
| 86 |
+
61.2,
|
| 87 |
+
70.9,
|
| 88 |
+
81.6,
|
| 89 |
+
93.3,
|
| 90 |
+
107.2,
|
| 91 |
+
125.4,
|
| 92 |
+
151.4,
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
MAX_RESIDUE_ANNOTATIONS = 16
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
TFIDF_VECTOR_SIZE = 58641
|
| 99 |
+
|
| 100 |
+
FUNCTION_TOKENS_DEPTH = 8
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
@cache
|
| 105 |
+
def data_root(model: str):
|
| 106 |
+
if "INFRA_PROVIDER" in os.environ:
|
| 107 |
+
return Path("")
|
| 108 |
+
# Try to download from huggingface if it doesn't exist
|
| 109 |
+
if model.startswith("esm3"):
|
| 110 |
+
path = Path(snapshot_download(repo_id="biohub/esm3-sm-open-v1"))
|
| 111 |
+
elif model.startswith("esmc-300"):
|
| 112 |
+
path = Path(snapshot_download(repo_id="biohub/esmc-300m-2024-12"))
|
| 113 |
+
elif model.startswith("esmc-600"):
|
| 114 |
+
path = Path(snapshot_download(repo_id="biohub/esmc-600m-2024-12"))
|
| 115 |
+
elif model.startswith("esmc-6b"):
|
| 116 |
+
path = Path(snapshot_download(repo_id="biohub/esmc-6b-2024-12"))
|
| 117 |
+
else:
|
| 118 |
+
raise ValueError(f"{model=} is an invalid model name.")
|
| 119 |
+
return path
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
IN_REPO_DATA_FOLDER = Path(__file__).parents[2] / "data"
|
| 123 |
+
|
| 124 |
+
INTERPRO_ENTRY = IN_REPO_DATA_FOLDER / "entry_list_safety_29026.list"
|
| 125 |
+
INTERPRO_HIERARCHY = IN_REPO_DATA_FOLDER / "ParentChildTreeFile.txt"
|
| 126 |
+
INTERPRO2GO = IN_REPO_DATA_FOLDER / "ParentChildTreeFile.txt"
|
| 127 |
+
INTERPRO_2ID = "data/tag_dict_4_safety_filtered.json"
|
| 128 |
+
|
| 129 |
+
LSH_TABLE_PATHS = {"8bit": "data/hyperplanes_8bit_58641.npz"}
|
| 130 |
+
|
| 131 |
+
KEYWORDS_VOCABULARY = (
|
| 132 |
+
IN_REPO_DATA_FOLDER / "keyword_vocabulary_safety_filtered_58641.txt"
|
| 133 |
+
)
|
| 134 |
+
KEYWORDS_IDF = IN_REPO_DATA_FOLDER / "keyword_idf_safety_filtered_58641.npy"
|
| 135 |
+
|
| 136 |
+
RESID_CSV = "data/uniref90_and_mgnify90_residue_annotations_gt_1k_proteins.csv"
|
| 137 |
+
INTERPRO2KEYWORDS = IN_REPO_DATA_FOLDER / "interpro_29026_to_keywords_58641.csv"
|
esmfold2_input_builder.py
CHANGED
|
@@ -1,254 +1,254 @@
|
|
| 1 |
-
from dataclasses import dataclass
|
| 2 |
-
from typing import Any, Sequence, TypeAlias, Union
|
| 3 |
-
|
| 4 |
-
import numpy as np
|
| 5 |
-
|
| 6 |
-
from .esmfold2_msa import MSA
|
| 7 |
-
|
| 8 |
-
# fmt: off
|
| 9 |
-
MSAInput: TypeAlias = Union[
|
| 10 |
-
MSA,
|
| 11 |
-
None,
|
| 12 |
-
]
|
| 13 |
-
# fmt: on
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@dataclass
|
| 17 |
-
class Modification:
|
| 18 |
-
position: int # zero-indexed
|
| 19 |
-
ccd: str
|
| 20 |
-
smiles: str | None = None # TODO(mlee): add smiles support
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
@dataclass
|
| 24 |
-
class ProteinInput:
|
| 25 |
-
id: str | list[str]
|
| 26 |
-
sequence: str
|
| 27 |
-
modifications: list[Modification] | None = None
|
| 28 |
-
msa: MSAInput = None
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
@dataclass
|
| 32 |
-
class RNAInput:
|
| 33 |
-
id: str | list[str]
|
| 34 |
-
sequence: str
|
| 35 |
-
modifications: list[Modification] | None = None
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
@dataclass
|
| 39 |
-
class DNAInput:
|
| 40 |
-
id: str | list[str]
|
| 41 |
-
sequence: str
|
| 42 |
-
modifications: list[Modification] | None = None
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
@dataclass
|
| 46 |
-
class LigandInput:
|
| 47 |
-
id: str | list[str]
|
| 48 |
-
smiles: str | None = None
|
| 49 |
-
ccd: list[str] | None = None
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
@dataclass
|
| 53 |
-
class DistogramConditioning:
|
| 54 |
-
chain_id: str
|
| 55 |
-
distogram: np.ndarray
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
@dataclass
|
| 59 |
-
class PocketConditioning:
|
| 60 |
-
binder_chain_id: str
|
| 61 |
-
contacts: list[tuple[str, int]]
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
@dataclass
|
| 65 |
-
class CovalentBond:
|
| 66 |
-
chain_id1: str
|
| 67 |
-
res_idx1: int
|
| 68 |
-
atom_idx1: int
|
| 69 |
-
chain_id2: str
|
| 70 |
-
res_idx2: int
|
| 71 |
-
atom_idx2: int
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
@dataclass
|
| 75 |
-
class StructurePredictionInput:
|
| 76 |
-
sequences: Sequence[ProteinInput | RNAInput | DNAInput | LigandInput]
|
| 77 |
-
pocket: PocketConditioning | None = None
|
| 78 |
-
distogram_conditioning: list[DistogramConditioning] | None = None
|
| 79 |
-
covalent_bonds: list[CovalentBond] | None = None
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def serialize_structure_prediction_input(all_atom_input: StructurePredictionInput):
|
| 83 |
-
def create_chain_data(seq_input, chain_type: str) -> dict[str, Any]:
|
| 84 |
-
chain_data: dict[str, Any] = {
|
| 85 |
-
"sequence": seq_input.sequence,
|
| 86 |
-
"id": seq_input.id,
|
| 87 |
-
"type": chain_type,
|
| 88 |
-
}
|
| 89 |
-
if hasattr(seq_input, "modifications") and seq_input.modifications:
|
| 90 |
-
mods = [
|
| 91 |
-
{"position": mod.position, "ccd": mod.ccd}
|
| 92 |
-
for mod in seq_input.modifications
|
| 93 |
-
]
|
| 94 |
-
chain_data["modifications"] = mods
|
| 95 |
-
if not hasattr(seq_input, "msa"):
|
| 96 |
-
pass
|
| 97 |
-
elif seq_input.msa is None:
|
| 98 |
-
chain_data["msa"] = None
|
| 99 |
-
elif isinstance(seq_input.msa, MSA):
|
| 100 |
-
chain_data["msa"] = {"sequences": seq_input.msa.sequences}
|
| 101 |
-
else:
|
| 102 |
-
error_msg = f"MSA must be None or MSA. Got {seq_input.msa} instead."
|
| 103 |
-
raise AttributeError(error_msg)
|
| 104 |
-
return chain_data
|
| 105 |
-
|
| 106 |
-
sequences = []
|
| 107 |
-
for seq_input in all_atom_input.sequences:
|
| 108 |
-
if isinstance(seq_input, ProteinInput):
|
| 109 |
-
sequences.append(create_chain_data(seq_input, "protein"))
|
| 110 |
-
elif isinstance(seq_input, RNAInput):
|
| 111 |
-
sequences.append(create_chain_data(seq_input, "rna"))
|
| 112 |
-
elif isinstance(seq_input, DNAInput):
|
| 113 |
-
sequences.append(create_chain_data(seq_input, "dna"))
|
| 114 |
-
elif isinstance(seq_input, LigandInput):
|
| 115 |
-
sequences.append(
|
| 116 |
-
{
|
| 117 |
-
"smiles": seq_input.smiles,
|
| 118 |
-
"id": seq_input.id,
|
| 119 |
-
"ccd": seq_input.ccd,
|
| 120 |
-
"type": "ligand",
|
| 121 |
-
}
|
| 122 |
-
)
|
| 123 |
-
else:
|
| 124 |
-
raise ValueError(f"Unsupported sequence input type: {type(seq_input)}")
|
| 125 |
-
|
| 126 |
-
result: dict[str, Any] = {"sequences": sequences}
|
| 127 |
-
|
| 128 |
-
if all_atom_input.covalent_bonds is not None:
|
| 129 |
-
result["covalent_bonds"] = [
|
| 130 |
-
{
|
| 131 |
-
"chain_id1": bond.chain_id1,
|
| 132 |
-
"res_idx1": bond.res_idx1,
|
| 133 |
-
"atom_idx1": bond.atom_idx1,
|
| 134 |
-
"chain_id2": bond.chain_id2,
|
| 135 |
-
"res_idx2": bond.res_idx2,
|
| 136 |
-
"atom_idx2": bond.atom_idx2,
|
| 137 |
-
}
|
| 138 |
-
for bond in all_atom_input.covalent_bonds
|
| 139 |
-
]
|
| 140 |
-
|
| 141 |
-
if all_atom_input.pocket is not None:
|
| 142 |
-
result["pocket"] = {
|
| 143 |
-
"binder_chain_id": all_atom_input.pocket.binder_chain_id,
|
| 144 |
-
"contacts": all_atom_input.pocket.contacts,
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
if all_atom_input.distogram_conditioning is not None:
|
| 148 |
-
result["distogram_conditioning"] = [
|
| 149 |
-
{"chain_id": disto.chain_id, "distogram": disto.distogram.tolist()}
|
| 150 |
-
for disto in all_atom_input.distogram_conditioning
|
| 151 |
-
]
|
| 152 |
-
|
| 153 |
-
return result
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
def deserialize_structure_prediction_input(
|
| 157 |
-
data: dict[str, Any],
|
| 158 |
-
) -> StructurePredictionInput:
|
| 159 |
-
"""Inverse of :func:`serialize_structure_prediction_input`.
|
| 160 |
-
|
| 161 |
-
Reconstructs a :class:`StructurePredictionInput` from the JSON-safe dict
|
| 162 |
-
produced by ``serialize_structure_prediction_input``. Values round-trip;
|
| 163 |
-
``DistogramConditioning.distogram`` dtype follows from JSON (``int64``
|
| 164 |
-
for integer entries, ``float64`` for floats) — cast back to the original
|
| 165 |
-
dtype if downstream code requires a specific one.
|
| 166 |
-
"""
|
| 167 |
-
|
| 168 |
-
def _mods(chain: dict[str, Any]) -> list[Modification] | None:
|
| 169 |
-
raw = chain.get("modifications")
|
| 170 |
-
if not raw:
|
| 171 |
-
return None
|
| 172 |
-
return [Modification(position=m["position"], ccd=m["ccd"]) for m in raw]
|
| 173 |
-
|
| 174 |
-
def _msa(chain: dict[str, Any]) -> MSAInput:
|
| 175 |
-
if "msa" not in chain or chain["msa"] is None:
|
| 176 |
-
return None
|
| 177 |
-
msa_blk = chain["msa"]
|
| 178 |
-
if isinstance(msa_blk, str):
|
| 179 |
-
raise ValueError(f"Unexpected MSA string value: {msa_blk!r}")
|
| 180 |
-
return MSA.from_sequences(msa_blk["sequences"])
|
| 181 |
-
|
| 182 |
-
sequences: list[ProteinInput | RNAInput | DNAInput | LigandInput] = []
|
| 183 |
-
for chain in data["sequences"]:
|
| 184 |
-
t = chain["type"]
|
| 185 |
-
if t == "protein":
|
| 186 |
-
sequences.append(
|
| 187 |
-
ProteinInput(
|
| 188 |
-
id=chain["id"],
|
| 189 |
-
sequence=chain["sequence"],
|
| 190 |
-
modifications=_mods(chain),
|
| 191 |
-
msa=_msa(chain),
|
| 192 |
-
)
|
| 193 |
-
)
|
| 194 |
-
elif t == "rna":
|
| 195 |
-
sequences.append(
|
| 196 |
-
RNAInput(
|
| 197 |
-
id=chain["id"],
|
| 198 |
-
sequence=chain["sequence"],
|
| 199 |
-
modifications=_mods(chain),
|
| 200 |
-
)
|
| 201 |
-
)
|
| 202 |
-
elif t == "dna":
|
| 203 |
-
sequences.append(
|
| 204 |
-
DNAInput(
|
| 205 |
-
id=chain["id"],
|
| 206 |
-
sequence=chain["sequence"],
|
| 207 |
-
modifications=_mods(chain),
|
| 208 |
-
)
|
| 209 |
-
)
|
| 210 |
-
elif t == "ligand":
|
| 211 |
-
sequences.append(
|
| 212 |
-
LigandInput(
|
| 213 |
-
id=chain["id"], smiles=chain.get("smiles"), ccd=chain.get("ccd")
|
| 214 |
-
)
|
| 215 |
-
)
|
| 216 |
-
else:
|
| 217 |
-
raise ValueError(f"Unsupported sequence type: {t!r}")
|
| 218 |
-
|
| 219 |
-
pocket: PocketConditioning | None = None
|
| 220 |
-
if (pocket_blk := data.get("pocket")) is not None:
|
| 221 |
-
pocket = PocketConditioning(
|
| 222 |
-
binder_chain_id=pocket_blk["binder_chain_id"],
|
| 223 |
-
contacts=[tuple(c) for c in pocket_blk["contacts"]],
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
distogram_conditioning: list[DistogramConditioning] | None = None
|
| 227 |
-
if (disto_blk := data.get("distogram_conditioning")) is not None:
|
| 228 |
-
distogram_conditioning = [
|
| 229 |
-
DistogramConditioning(
|
| 230 |
-
chain_id=d["chain_id"], distogram=np.asarray(d["distogram"])
|
| 231 |
-
)
|
| 232 |
-
for d in disto_blk
|
| 233 |
-
]
|
| 234 |
-
|
| 235 |
-
covalent_bonds: list[CovalentBond] | None = None
|
| 236 |
-
if (bonds_blk := data.get("covalent_bonds")) is not None:
|
| 237 |
-
covalent_bonds = [
|
| 238 |
-
CovalentBond(
|
| 239 |
-
chain_id1=b["chain_id1"],
|
| 240 |
-
res_idx1=b["res_idx1"],
|
| 241 |
-
atom_idx1=b["atom_idx1"],
|
| 242 |
-
chain_id2=b["chain_id2"],
|
| 243 |
-
res_idx2=b["res_idx2"],
|
| 244 |
-
atom_idx2=b["atom_idx2"],
|
| 245 |
-
)
|
| 246 |
-
for b in bonds_blk
|
| 247 |
-
]
|
| 248 |
-
|
| 249 |
-
return StructurePredictionInput(
|
| 250 |
-
sequences=sequences,
|
| 251 |
-
pocket=pocket,
|
| 252 |
-
distogram_conditioning=distogram_conditioning,
|
| 253 |
-
covalent_bonds=covalent_bonds,
|
| 254 |
-
)
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Any, Sequence, TypeAlias, Union
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from .esmfold2_msa import MSA
|
| 7 |
+
|
| 8 |
+
# fmt: off
|
| 9 |
+
MSAInput: TypeAlias = Union[
|
| 10 |
+
MSA,
|
| 11 |
+
None,
|
| 12 |
+
]
|
| 13 |
+
# fmt: on
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class Modification:
|
| 18 |
+
position: int # zero-indexed
|
| 19 |
+
ccd: str
|
| 20 |
+
smiles: str | None = None # TODO(mlee): add smiles support
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class ProteinInput:
|
| 25 |
+
id: str | list[str]
|
| 26 |
+
sequence: str
|
| 27 |
+
modifications: list[Modification] | None = None
|
| 28 |
+
msa: MSAInput = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class RNAInput:
|
| 33 |
+
id: str | list[str]
|
| 34 |
+
sequence: str
|
| 35 |
+
modifications: list[Modification] | None = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class DNAInput:
|
| 40 |
+
id: str | list[str]
|
| 41 |
+
sequence: str
|
| 42 |
+
modifications: list[Modification] | None = None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class LigandInput:
|
| 47 |
+
id: str | list[str]
|
| 48 |
+
smiles: str | None = None
|
| 49 |
+
ccd: list[str] | None = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class DistogramConditioning:
|
| 54 |
+
chain_id: str
|
| 55 |
+
distogram: np.ndarray
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class PocketConditioning:
|
| 60 |
+
binder_chain_id: str
|
| 61 |
+
contacts: list[tuple[str, int]]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class CovalentBond:
|
| 66 |
+
chain_id1: str
|
| 67 |
+
res_idx1: int
|
| 68 |
+
atom_idx1: int
|
| 69 |
+
chain_id2: str
|
| 70 |
+
res_idx2: int
|
| 71 |
+
atom_idx2: int
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@dataclass
|
| 75 |
+
class StructurePredictionInput:
|
| 76 |
+
sequences: Sequence[ProteinInput | RNAInput | DNAInput | LigandInput]
|
| 77 |
+
pocket: PocketConditioning | None = None
|
| 78 |
+
distogram_conditioning: list[DistogramConditioning] | None = None
|
| 79 |
+
covalent_bonds: list[CovalentBond] | None = None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def serialize_structure_prediction_input(all_atom_input: StructurePredictionInput):
|
| 83 |
+
def create_chain_data(seq_input, chain_type: str) -> dict[str, Any]:
|
| 84 |
+
chain_data: dict[str, Any] = {
|
| 85 |
+
"sequence": seq_input.sequence,
|
| 86 |
+
"id": seq_input.id,
|
| 87 |
+
"type": chain_type,
|
| 88 |
+
}
|
| 89 |
+
if hasattr(seq_input, "modifications") and seq_input.modifications:
|
| 90 |
+
mods = [
|
| 91 |
+
{"position": mod.position, "ccd": mod.ccd}
|
| 92 |
+
for mod in seq_input.modifications
|
| 93 |
+
]
|
| 94 |
+
chain_data["modifications"] = mods
|
| 95 |
+
if not hasattr(seq_input, "msa"):
|
| 96 |
+
pass
|
| 97 |
+
elif seq_input.msa is None:
|
| 98 |
+
chain_data["msa"] = None
|
| 99 |
+
elif isinstance(seq_input.msa, MSA):
|
| 100 |
+
chain_data["msa"] = {"sequences": seq_input.msa.sequences}
|
| 101 |
+
else:
|
| 102 |
+
error_msg = f"MSA must be None or MSA. Got {seq_input.msa} instead."
|
| 103 |
+
raise AttributeError(error_msg)
|
| 104 |
+
return chain_data
|
| 105 |
+
|
| 106 |
+
sequences = []
|
| 107 |
+
for seq_input in all_atom_input.sequences:
|
| 108 |
+
if isinstance(seq_input, ProteinInput):
|
| 109 |
+
sequences.append(create_chain_data(seq_input, "protein"))
|
| 110 |
+
elif isinstance(seq_input, RNAInput):
|
| 111 |
+
sequences.append(create_chain_data(seq_input, "rna"))
|
| 112 |
+
elif isinstance(seq_input, DNAInput):
|
| 113 |
+
sequences.append(create_chain_data(seq_input, "dna"))
|
| 114 |
+
elif isinstance(seq_input, LigandInput):
|
| 115 |
+
sequences.append(
|
| 116 |
+
{
|
| 117 |
+
"smiles": seq_input.smiles,
|
| 118 |
+
"id": seq_input.id,
|
| 119 |
+
"ccd": seq_input.ccd,
|
| 120 |
+
"type": "ligand",
|
| 121 |
+
}
|
| 122 |
+
)
|
| 123 |
+
else:
|
| 124 |
+
raise ValueError(f"Unsupported sequence input type: {type(seq_input)}")
|
| 125 |
+
|
| 126 |
+
result: dict[str, Any] = {"sequences": sequences}
|
| 127 |
+
|
| 128 |
+
if all_atom_input.covalent_bonds is not None:
|
| 129 |
+
result["covalent_bonds"] = [
|
| 130 |
+
{
|
| 131 |
+
"chain_id1": bond.chain_id1,
|
| 132 |
+
"res_idx1": bond.res_idx1,
|
| 133 |
+
"atom_idx1": bond.atom_idx1,
|
| 134 |
+
"chain_id2": bond.chain_id2,
|
| 135 |
+
"res_idx2": bond.res_idx2,
|
| 136 |
+
"atom_idx2": bond.atom_idx2,
|
| 137 |
+
}
|
| 138 |
+
for bond in all_atom_input.covalent_bonds
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
if all_atom_input.pocket is not None:
|
| 142 |
+
result["pocket"] = {
|
| 143 |
+
"binder_chain_id": all_atom_input.pocket.binder_chain_id,
|
| 144 |
+
"contacts": all_atom_input.pocket.contacts,
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if all_atom_input.distogram_conditioning is not None:
|
| 148 |
+
result["distogram_conditioning"] = [
|
| 149 |
+
{"chain_id": disto.chain_id, "distogram": disto.distogram.tolist()}
|
| 150 |
+
for disto in all_atom_input.distogram_conditioning
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
return result
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def deserialize_structure_prediction_input(
|
| 157 |
+
data: dict[str, Any],
|
| 158 |
+
) -> StructurePredictionInput:
|
| 159 |
+
"""Inverse of :func:`serialize_structure_prediction_input`.
|
| 160 |
+
|
| 161 |
+
Reconstructs a :class:`StructurePredictionInput` from the JSON-safe dict
|
| 162 |
+
produced by ``serialize_structure_prediction_input``. Values round-trip;
|
| 163 |
+
``DistogramConditioning.distogram`` dtype follows from JSON (``int64``
|
| 164 |
+
for integer entries, ``float64`` for floats) — cast back to the original
|
| 165 |
+
dtype if downstream code requires a specific one.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def _mods(chain: dict[str, Any]) -> list[Modification] | None:
|
| 169 |
+
raw = chain.get("modifications")
|
| 170 |
+
if not raw:
|
| 171 |
+
return None
|
| 172 |
+
return [Modification(position=m["position"], ccd=m["ccd"]) for m in raw]
|
| 173 |
+
|
| 174 |
+
def _msa(chain: dict[str, Any]) -> MSAInput:
|
| 175 |
+
if "msa" not in chain or chain["msa"] is None:
|
| 176 |
+
return None
|
| 177 |
+
msa_blk = chain["msa"]
|
| 178 |
+
if isinstance(msa_blk, str):
|
| 179 |
+
raise ValueError(f"Unexpected MSA string value: {msa_blk!r}")
|
| 180 |
+
return MSA.from_sequences(msa_blk["sequences"])
|
| 181 |
+
|
| 182 |
+
sequences: list[ProteinInput | RNAInput | DNAInput | LigandInput] = []
|
| 183 |
+
for chain in data["sequences"]:
|
| 184 |
+
t = chain["type"]
|
| 185 |
+
if t == "protein":
|
| 186 |
+
sequences.append(
|
| 187 |
+
ProteinInput(
|
| 188 |
+
id=chain["id"],
|
| 189 |
+
sequence=chain["sequence"],
|
| 190 |
+
modifications=_mods(chain),
|
| 191 |
+
msa=_msa(chain),
|
| 192 |
+
)
|
| 193 |
+
)
|
| 194 |
+
elif t == "rna":
|
| 195 |
+
sequences.append(
|
| 196 |
+
RNAInput(
|
| 197 |
+
id=chain["id"],
|
| 198 |
+
sequence=chain["sequence"],
|
| 199 |
+
modifications=_mods(chain),
|
| 200 |
+
)
|
| 201 |
+
)
|
| 202 |
+
elif t == "dna":
|
| 203 |
+
sequences.append(
|
| 204 |
+
DNAInput(
|
| 205 |
+
id=chain["id"],
|
| 206 |
+
sequence=chain["sequence"],
|
| 207 |
+
modifications=_mods(chain),
|
| 208 |
+
)
|
| 209 |
+
)
|
| 210 |
+
elif t == "ligand":
|
| 211 |
+
sequences.append(
|
| 212 |
+
LigandInput(
|
| 213 |
+
id=chain["id"], smiles=chain.get("smiles"), ccd=chain.get("ccd")
|
| 214 |
+
)
|
| 215 |
+
)
|
| 216 |
+
else:
|
| 217 |
+
raise ValueError(f"Unsupported sequence type: {t!r}")
|
| 218 |
+
|
| 219 |
+
pocket: PocketConditioning | None = None
|
| 220 |
+
if (pocket_blk := data.get("pocket")) is not None:
|
| 221 |
+
pocket = PocketConditioning(
|
| 222 |
+
binder_chain_id=pocket_blk["binder_chain_id"],
|
| 223 |
+
contacts=[tuple(c) for c in pocket_blk["contacts"]],
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
distogram_conditioning: list[DistogramConditioning] | None = None
|
| 227 |
+
if (disto_blk := data.get("distogram_conditioning")) is not None:
|
| 228 |
+
distogram_conditioning = [
|
| 229 |
+
DistogramConditioning(
|
| 230 |
+
chain_id=d["chain_id"], distogram=np.asarray(d["distogram"])
|
| 231 |
+
)
|
| 232 |
+
for d in disto_blk
|
| 233 |
+
]
|
| 234 |
+
|
| 235 |
+
covalent_bonds: list[CovalentBond] | None = None
|
| 236 |
+
if (bonds_blk := data.get("covalent_bonds")) is not None:
|
| 237 |
+
covalent_bonds = [
|
| 238 |
+
CovalentBond(
|
| 239 |
+
chain_id1=b["chain_id1"],
|
| 240 |
+
res_idx1=b["res_idx1"],
|
| 241 |
+
atom_idx1=b["atom_idx1"],
|
| 242 |
+
chain_id2=b["chain_id2"],
|
| 243 |
+
res_idx2=b["res_idx2"],
|
| 244 |
+
atom_idx2=b["atom_idx2"],
|
| 245 |
+
)
|
| 246 |
+
for b in bonds_blk
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
return StructurePredictionInput(
|
| 250 |
+
sequences=sequences,
|
| 251 |
+
pocket=pocket,
|
| 252 |
+
distogram_conditioning=distogram_conditioning,
|
| 253 |
+
covalent_bonds=covalent_bonds,
|
| 254 |
+
)
|
esmfold2_metrics.py
CHANGED
|
@@ -1,373 +1,373 @@
|
|
| 1 |
-
import numpy as np
|
| 2 |
-
import torch
|
| 3 |
-
import torch.nn.functional as F
|
| 4 |
-
from einops import rearrange
|
| 5 |
-
from torch import Tensor
|
| 6 |
-
from torch.amp import autocast # type: ignore
|
| 7 |
-
|
| 8 |
-
from . import esmfold2_residue_constants as residue_constants
|
| 9 |
-
from .esmfold2_misc import binpack, unbinpack
|
| 10 |
-
from .esmfold2_protein_structure import (
|
| 11 |
-
compute_alignment_tensors,
|
| 12 |
-
compute_gdt_ts_no_alignment,
|
| 13 |
-
compute_rmsd_no_alignment,
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def contact_precision(
|
| 18 |
-
predictions: Tensor,
|
| 19 |
-
targets: Tensor,
|
| 20 |
-
src_lengths: Tensor | None = None,
|
| 21 |
-
minsep: int = 6,
|
| 22 |
-
maxsep: int | None = None,
|
| 23 |
-
override_length: int | None = None, # for casp
|
| 24 |
-
):
|
| 25 |
-
"""Computes contact precisions.
|
| 26 |
-
|
| 27 |
-
For protein contact prediction, precision is measured for the top (L/K) highest confidence predictions,
|
| 28 |
-
with L being the length of the protein sequence and K generally being equal to 1 or 5.
|
| 29 |
-
|
| 30 |
-
K = 5 measures the predictions of the very highest confidence contacts, while K = 1 is a more general measure
|
| 31 |
-
over all relatively high confidence predictions.
|
| 32 |
-
|
| 33 |
-
Since there are roughly ~L true contacts in a protein, this is a reasonable cutoff.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
Args:
|
| 37 |
-
predictions (Tensor): Tensor of probabilities of size (B, L, L)
|
| 38 |
-
targets (Tensor): Tensor of true contacts of size (B, L, L)
|
| 39 |
-
src_lengths (Tensor, optional): Lengths of each sample in the batch, if using variable lengths.
|
| 40 |
-
If not provided, inferred from the size of the predictions.
|
| 41 |
-
minsep (int): Minimum separation distance to consider. We often want to measure contacts at a
|
| 42 |
-
certain range. Typical ranges are short [6, 12), medium [12, 24), and long [24, inf).
|
| 43 |
-
maxsep (int, optional): Used in conjunction with minsep to specify a contact range. If not provided uses
|
| 44 |
-
assumes no maximum range
|
| 45 |
-
override_length (int, optional): Used for casp evaluation where sometimes the "true" length is not
|
| 46 |
-
the same as the length of the input. Kept for posterity, we probably don't need this argument.
|
| 47 |
-
"""
|
| 48 |
-
if predictions.dim() == 2:
|
| 49 |
-
predictions = predictions.unsqueeze(0)
|
| 50 |
-
if targets.dim() == 2:
|
| 51 |
-
targets = targets.unsqueeze(0)
|
| 52 |
-
|
| 53 |
-
# Check sizes
|
| 54 |
-
if predictions.size() != targets.size():
|
| 55 |
-
raise ValueError(
|
| 56 |
-
f"Size mismatch. Received predictions of size {predictions.size()}, "
|
| 57 |
-
f"targets of size {targets.size()}"
|
| 58 |
-
)
|
| 59 |
-
device = predictions.device
|
| 60 |
-
|
| 61 |
-
batch_size, seqlen, _ = predictions.size()
|
| 62 |
-
|
| 63 |
-
# Step 1) Construct a mask of size [B, L, L] to mask invalid contacts
|
| 64 |
-
seqlen_range = torch.arange(seqlen, device=device)
|
| 65 |
-
sep = seqlen_range.unsqueeze(0) - seqlen_range.unsqueeze(1)
|
| 66 |
-
sep = sep.unsqueeze(0)
|
| 67 |
-
# Mask contacts that are closer than minsep
|
| 68 |
-
valid_mask = sep >= minsep
|
| 69 |
-
# Mask contacts where target is negative (padding or unknown)
|
| 70 |
-
valid_mask = valid_mask & (targets >= 0) # negative targets are invalid
|
| 71 |
-
|
| 72 |
-
# Mask contacts that are farther than maxsep, if provided
|
| 73 |
-
if maxsep is not None:
|
| 74 |
-
valid_mask &= sep < maxsep
|
| 75 |
-
|
| 76 |
-
if src_lengths is not None:
|
| 77 |
-
# If the lengths of the individual sequences are provided, mask positions
|
| 78 |
-
# that are farther than the end of the sequence.
|
| 79 |
-
valid = seqlen_range.unsqueeze(0) < src_lengths.unsqueeze(1)
|
| 80 |
-
valid_mask &= valid.unsqueeze(1) & valid.unsqueeze(2)
|
| 81 |
-
else:
|
| 82 |
-
src_lengths = torch.full([batch_size], seqlen, device=device, dtype=torch.long)
|
| 83 |
-
|
| 84 |
-
# Fill in the logit tensor with -inf for all invalid positions
|
| 85 |
-
predictions = predictions.masked_fill(~valid_mask, float("-inf"))
|
| 86 |
-
|
| 87 |
-
# Step 2) Select the top half of the prediction (should be symmetric)
|
| 88 |
-
x_ind, y_ind = np.triu_indices(seqlen, minsep)
|
| 89 |
-
predictions_upper = predictions[:, x_ind, y_ind]
|
| 90 |
-
targets_upper = targets[:, x_ind, y_ind]
|
| 91 |
-
|
| 92 |
-
# Step 3) Select the topk values in each batch where k = L (length of sequence)
|
| 93 |
-
topk = seqlen if override_length is None else max(seqlen, override_length)
|
| 94 |
-
# Indices are the indices into the predictions corresponding to the most confident predictions
|
| 95 |
-
indices = predictions_upper.argsort(dim=-1, descending=True)[:, :topk]
|
| 96 |
-
# topk_targets are the target values corresponding to the above indices
|
| 97 |
-
topk_targets = targets_upper[torch.arange(batch_size).unsqueeze(1), indices]
|
| 98 |
-
if topk_targets.size(1) < topk:
|
| 99 |
-
# If there aren't enough targets, pad to the output.
|
| 100 |
-
topk_targets = F.pad(topk_targets, [0, topk - topk_targets.size(1)])
|
| 101 |
-
|
| 102 |
-
# Step 4) Sum the accuracy at of the top-i predictions for i in 1, L
|
| 103 |
-
# topk_targets => 1/0 true vs. false contact, sorted by confidence of prediction
|
| 104 |
-
# cmumulative sum => Number of correct answers for the top-i predictions.
|
| 105 |
-
cumulative_dist = topk_targets.type_as(predictions).cumsum(-1)
|
| 106 |
-
|
| 107 |
-
# Step 5) Find the gather indices. This should be P@(L / K) for varous values of K
|
| 108 |
-
# The values will differ for each batch.
|
| 109 |
-
gather_lengths = src_lengths.unsqueeze(1)
|
| 110 |
-
if override_length is not None:
|
| 111 |
-
gather_lengths = override_length * torch.ones_like(
|
| 112 |
-
gather_lengths, device=device
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
# This gets you (0.1 * L, 0.2 * L, 0.3 * L, etc.)
|
| 116 |
-
gather_indices = (
|
| 117 |
-
(torch.arange(0.1, 1.1, 0.1, device=device).unsqueeze(0) * gather_lengths).type(
|
| 118 |
-
torch.long
|
| 119 |
-
)
|
| 120 |
-
- 1
|
| 121 |
-
).clamp_min(0)
|
| 122 |
-
|
| 123 |
-
# Step 6) Gather the results and divide by the number of guesses to get the precision.
|
| 124 |
-
binned_cumulative_dist = cumulative_dist.gather(1, gather_indices)
|
| 125 |
-
binned_precisions = binned_cumulative_dist / (gather_indices + 1).type_as(
|
| 126 |
-
binned_cumulative_dist
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
# Select specific P@L/k. pl5 is index 1 b/c that corresponds to L * 0.2 in
|
| 130 |
-
# gather_indices above
|
| 131 |
-
pl5 = binned_precisions[:, 1]
|
| 132 |
-
# pl2 = binned_precisions[:, 4]
|
| 133 |
-
pl = binned_precisions[:, 9]
|
| 134 |
-
# AUC is the integral wrt K of P@L/K for K in range(1, L)
|
| 135 |
-
auc = binned_precisions.mean(-1)
|
| 136 |
-
|
| 137 |
-
return {"AUC": auc, "P@L": pl, "P@L5": pl5}
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def compute_lddt(
|
| 141 |
-
all_atom_pred_pos: torch.Tensor,
|
| 142 |
-
all_atom_positions: torch.Tensor,
|
| 143 |
-
all_atom_mask: torch.Tensor,
|
| 144 |
-
pairwise_all_atom_mask: torch.Tensor | None = None,
|
| 145 |
-
cutoff: float | torch.Tensor = 15.0,
|
| 146 |
-
eps: float = 1e-10,
|
| 147 |
-
per_residue: bool = True,
|
| 148 |
-
sequence_id: torch.Tensor | None = None,
|
| 149 |
-
) -> torch.Tensor:
|
| 150 |
-
"""
|
| 151 |
-
Computes LDDT for a protein. Tensor sizes below include some optional dimensions. Specifically:
|
| 152 |
-
Nstates:
|
| 153 |
-
all_atom_pred_pos can contain multiple states in the first dimension which corresponds to outputs from different layers of a model (e.g. each IPA block). The return size will be [Nstates x Batch size] if this is included.
|
| 154 |
-
Natoms:
|
| 155 |
-
LDDT can be computed for all atoms or some atoms. The second to last dimension should contain the *FLATTENED* representation of L x Natoms. If you want to calculate for atom37, e.g., this will be of size (L * 37). If you are only calculating CA LDDT, it will be of size L.
|
| 156 |
-
|
| 157 |
-
Args:
|
| 158 |
-
all_atom_pred_pos (Tensor[float], [(Nstates x) B x (L * Natoms x) 3]): Tensor of predicted positions
|
| 159 |
-
all_atom_positions (Tensor[float], [B x (L * Natoms x) 3]): Tensor of true positions
|
| 160 |
-
all_atom_mask (Tensor[float], [B x (L * Natoms)]): Tensor of masks, indicating whether an atom exists.
|
| 161 |
-
pairwise_all_atom_mask (Tensor[float], [B x (L * Natoms x L * Natoms)], optional): Tensor of masks, indicating whether a pair of atoms should be considered in the LDDT calculation.
|
| 162 |
-
cutoff (float): Max distance to score lddt over. This can either be a float, or a tensor of shape [B, L, L] to allow for per-residue cutoffs, e.g. if you want to use a different cutoff for nucleic acids.
|
| 163 |
-
per_residue (bool): Whether to return per-residue or full-protein lddt.
|
| 164 |
-
sequence_id (Tensor, optional): Sequence id tensor for binpacking. NOTE: only supported for lddt_ca calculations, not when Natoms is passed!
|
| 165 |
-
|
| 166 |
-
Returns:
|
| 167 |
-
LDDT Tensor:
|
| 168 |
-
if per_residue:
|
| 169 |
-
Tensor[float], [(Nstates x) B x (L * Natoms)]
|
| 170 |
-
else:
|
| 171 |
-
Tensor[float], [(Nstates x) B]
|
| 172 |
-
"""
|
| 173 |
-
all_atom_mask = all_atom_mask[..., None] # add a dimension for broadcasting
|
| 174 |
-
dmat_true = torch.sqrt(
|
| 175 |
-
eps
|
| 176 |
-
+ torch.sum(
|
| 177 |
-
(all_atom_positions[..., None, :] - all_atom_positions[..., None, :, :])
|
| 178 |
-
** 2,
|
| 179 |
-
dim=-1,
|
| 180 |
-
)
|
| 181 |
-
)
|
| 182 |
-
|
| 183 |
-
dmat_pred = torch.sqrt(
|
| 184 |
-
eps
|
| 185 |
-
+ torch.sum(
|
| 186 |
-
(all_atom_pred_pos[..., None, :] - all_atom_pred_pos[..., None, :, :]) ** 2,
|
| 187 |
-
dim=-1,
|
| 188 |
-
)
|
| 189 |
-
)
|
| 190 |
-
mask = all_atom_mask * rearrange(all_atom_mask, "... a b -> ... b a")
|
| 191 |
-
if pairwise_all_atom_mask is not None:
|
| 192 |
-
mask = mask * pairwise_all_atom_mask
|
| 193 |
-
|
| 194 |
-
if sequence_id is not None:
|
| 195 |
-
# TODO: This will work for lddt_ca, but not for regular lddt
|
| 196 |
-
# Problem is that regular lddt has natoms * nres scores, so would need to repeat this mask by natoms
|
| 197 |
-
# Leaving for now because it won't fail silently so should be ook.
|
| 198 |
-
seqid_mask = sequence_id[..., None] == sequence_id[..., None, :]
|
| 199 |
-
mask = mask * seqid_mask.type_as(mask)
|
| 200 |
-
|
| 201 |
-
return compute_lddt_from_dmat(
|
| 202 |
-
dmat_pred, dmat_true, mask, cutoff=cutoff, eps=eps, per_residue=per_residue
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def compute_lddt_from_dmat(
|
| 207 |
-
dmat_pred: torch.Tensor,
|
| 208 |
-
dmat_true: torch.Tensor,
|
| 209 |
-
pairwise_mask: torch.Tensor,
|
| 210 |
-
cutoff: float | torch.Tensor = 15.0,
|
| 211 |
-
eps: float = 1e-10,
|
| 212 |
-
per_residue: bool = True,
|
| 213 |
-
):
|
| 214 |
-
"""
|
| 215 |
-
Compute LDDT from pre-computed distance matrices.
|
| 216 |
-
This is useful when you want to compute LDDT with multiple different masks or cutoffs, e.g. for different molecule types (protein, nucleic acid, etc.).
|
| 217 |
-
|
| 218 |
-
Args:
|
| 219 |
-
dmat_pred (Tensor[float], [B x L x L]): Predicted distance matrix
|
| 220 |
-
dmat_true (Tensor[float], [B x L x L]): True distance matrix
|
| 221 |
-
pairwise_mask (Tensor[float], [B x L x L]): Pairwise mask indicating which pairs of atoms to consider
|
| 222 |
-
cutoff (float): Max distance to score lddt over. This can either be a float, or a tensor of shape [B, L, L] to allow for per-residue cutoffs, e.g. if you want to use a different cutoff for nucleic acids.
|
| 223 |
-
per_residue (bool): Whether to return per-residue or full-protein lddt.
|
| 224 |
-
|
| 225 |
-
Returns:
|
| 226 |
-
LDDT Tensor:
|
| 227 |
-
if per_residue:
|
| 228 |
-
Tensor[float], [B x L]
|
| 229 |
-
else:
|
| 230 |
-
Tensor[float], [B]
|
| 231 |
-
"""
|
| 232 |
-
n = dmat_true.size(-1)
|
| 233 |
-
dists_to_score = (
|
| 234 |
-
(dmat_true < cutoff)
|
| 235 |
-
* pairwise_mask
|
| 236 |
-
* (1.0 - torch.eye(n, device=dmat_true.device))
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
dist_l1 = torch.abs(dmat_true - dmat_pred)
|
| 240 |
-
score = (
|
| 241 |
-
(dist_l1 < 0.5).type(dist_l1.dtype)
|
| 242 |
-
+ (dist_l1 < 1.0).type(dist_l1.dtype)
|
| 243 |
-
+ (dist_l1 < 2.0).type(dist_l1.dtype)
|
| 244 |
-
+ (dist_l1 < 4.0).type(dist_l1.dtype)
|
| 245 |
-
)
|
| 246 |
-
score = score * 0.25
|
| 247 |
-
|
| 248 |
-
dims = (-1,) if per_residue else (-2, -1)
|
| 249 |
-
norm = 1.0 / (eps + torch.sum(dists_to_score, dim=dims))
|
| 250 |
-
score = norm * (eps + torch.sum(dists_to_score * score, dim=dims))
|
| 251 |
-
return score
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def compute_lddt_ca(
|
| 255 |
-
all_atom_pred_pos: torch.Tensor,
|
| 256 |
-
all_atom_positions: torch.Tensor,
|
| 257 |
-
all_atom_mask: torch.Tensor,
|
| 258 |
-
cutoff: float = 15.0,
|
| 259 |
-
eps: float = 1e-10,
|
| 260 |
-
per_residue: bool = True,
|
| 261 |
-
sequence_id: torch.Tensor | None = None,
|
| 262 |
-
) -> torch.Tensor:
|
| 263 |
-
ca_pos = residue_constants.atom_order["CA"]
|
| 264 |
-
if all_atom_pred_pos.dim() != 3:
|
| 265 |
-
all_atom_pred_pos = all_atom_pred_pos[..., ca_pos, :]
|
| 266 |
-
all_atom_positions = all_atom_positions[..., ca_pos, :]
|
| 267 |
-
all_atom_mask = all_atom_mask[..., ca_pos]
|
| 268 |
-
|
| 269 |
-
return compute_lddt(
|
| 270 |
-
all_atom_pred_pos,
|
| 271 |
-
all_atom_positions,
|
| 272 |
-
all_atom_mask,
|
| 273 |
-
cutoff=cutoff,
|
| 274 |
-
eps=eps,
|
| 275 |
-
per_residue=per_residue,
|
| 276 |
-
sequence_id=sequence_id,
|
| 277 |
-
)
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
# NOTE(roshan): no_grad required for stack_variable_length_tensors apparently... let's revisit if we want to backprop
|
| 281 |
-
@torch.no_grad()
|
| 282 |
-
@autocast("cuda", enabled=False)
|
| 283 |
-
def compute_rmsd(
|
| 284 |
-
mobile: torch.Tensor,
|
| 285 |
-
target: torch.Tensor,
|
| 286 |
-
atom_exists_mask: torch.Tensor | None = None,
|
| 287 |
-
sequence_id: torch.Tensor | None = None,
|
| 288 |
-
reduction: str = "batch",
|
| 289 |
-
):
|
| 290 |
-
"""
|
| 291 |
-
Compute RMSD between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 292 |
-
|
| 293 |
-
Args:
|
| 294 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 295 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 296 |
-
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 297 |
-
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 298 |
-
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 299 |
-
|
| 300 |
-
Returns:
|
| 301 |
-
If reduction == "batch":
|
| 302 |
-
(torch.Tensor): 0-dim, Average Root Mean Square Deviation between the structures for each batch
|
| 303 |
-
If reduction == "per_sample":
|
| 304 |
-
(torch.Tensor): (B,)-dim, Root Mean Square Deviation between the structures for each batch
|
| 305 |
-
If reduction == "per_residue":
|
| 306 |
-
(torch.Tensor): (B, N)-dim, Root Mean Square Deviation between the structures for residue in the batch
|
| 307 |
-
"""
|
| 308 |
-
|
| 309 |
-
(centered_mobile, _, centered_target, _, rotation_matrix, num_valid_atoms) = (
|
| 310 |
-
compute_alignment_tensors(
|
| 311 |
-
mobile=mobile,
|
| 312 |
-
target=target,
|
| 313 |
-
atom_exists_mask=atom_exists_mask,
|
| 314 |
-
sequence_id=sequence_id,
|
| 315 |
-
)
|
| 316 |
-
)
|
| 317 |
-
|
| 318 |
-
# Apply transformation to centered structure
|
| 319 |
-
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 320 |
-
|
| 321 |
-
# Compute rmsd for centered structures
|
| 322 |
-
rmsd = compute_rmsd_no_alignment(
|
| 323 |
-
rotated_mobile, centered_target, num_valid_atoms, reduction=reduction
|
| 324 |
-
)
|
| 325 |
-
if reduction == "per_residue" and sequence_id is not None:
|
| 326 |
-
rmsd = binpack(rmsd, sequence_id, pad_value=0)
|
| 327 |
-
return rmsd
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def compute_gdt_ts(
|
| 331 |
-
mobile: torch.Tensor,
|
| 332 |
-
target: torch.Tensor,
|
| 333 |
-
atom_exists_mask: torch.Tensor | None = None,
|
| 334 |
-
sequence_id: torch.Tensor | None = None,
|
| 335 |
-
reduction: str = "per_sample",
|
| 336 |
-
):
|
| 337 |
-
"""
|
| 338 |
-
Compute GDT_TS between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 339 |
-
|
| 340 |
-
Args:
|
| 341 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 342 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 343 |
-
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 344 |
-
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 345 |
-
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 346 |
-
|
| 347 |
-
Returns:
|
| 348 |
-
If reduction == "batch":
|
| 349 |
-
(torch.Tensor): 0-dim, GDT_TS between the structures for each batch
|
| 350 |
-
If reduction == "per_sample":
|
| 351 |
-
(torch.Tensor): (B,)-dim, GDT_TS between the structures for each sample in the batch
|
| 352 |
-
"""
|
| 353 |
-
if atom_exists_mask is None:
|
| 354 |
-
atom_exists_mask = torch.isfinite(target).all(dim=-1)
|
| 355 |
-
(centered_mobile, _, centered_target, _, rotation_matrix, _) = (
|
| 356 |
-
compute_alignment_tensors(
|
| 357 |
-
mobile=mobile,
|
| 358 |
-
target=target,
|
| 359 |
-
atom_exists_mask=atom_exists_mask,
|
| 360 |
-
sequence_id=sequence_id,
|
| 361 |
-
)
|
| 362 |
-
)
|
| 363 |
-
|
| 364 |
-
# Apply transformation to centered structure
|
| 365 |
-
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 366 |
-
|
| 367 |
-
# the coordinate tensors returned by `compute_alignment_tensors` are unbinpacked and contain zeros for invalid positions
|
| 368 |
-
# so `compute_gdt_ts_no_alignment` requires `atom_exists_mask` to be passed and be unbinpacked
|
| 369 |
-
if sequence_id is not None:
|
| 370 |
-
atom_exists_mask = unbinpack(atom_exists_mask, sequence_id, pad_value=False)
|
| 371 |
-
return compute_gdt_ts_no_alignment(
|
| 372 |
-
rotated_mobile, centered_target, atom_exists_mask, reduction
|
| 373 |
-
)
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from einops import rearrange
|
| 5 |
+
from torch import Tensor
|
| 6 |
+
from torch.amp import autocast # type: ignore
|
| 7 |
+
|
| 8 |
+
from . import esmfold2_residue_constants as residue_constants
|
| 9 |
+
from .esmfold2_misc import binpack, unbinpack
|
| 10 |
+
from .esmfold2_protein_structure import (
|
| 11 |
+
compute_alignment_tensors,
|
| 12 |
+
compute_gdt_ts_no_alignment,
|
| 13 |
+
compute_rmsd_no_alignment,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def contact_precision(
|
| 18 |
+
predictions: Tensor,
|
| 19 |
+
targets: Tensor,
|
| 20 |
+
src_lengths: Tensor | None = None,
|
| 21 |
+
minsep: int = 6,
|
| 22 |
+
maxsep: int | None = None,
|
| 23 |
+
override_length: int | None = None, # for casp
|
| 24 |
+
):
|
| 25 |
+
"""Computes contact precisions.
|
| 26 |
+
|
| 27 |
+
For protein contact prediction, precision is measured for the top (L/K) highest confidence predictions,
|
| 28 |
+
with L being the length of the protein sequence and K generally being equal to 1 or 5.
|
| 29 |
+
|
| 30 |
+
K = 5 measures the predictions of the very highest confidence contacts, while K = 1 is a more general measure
|
| 31 |
+
over all relatively high confidence predictions.
|
| 32 |
+
|
| 33 |
+
Since there are roughly ~L true contacts in a protein, this is a reasonable cutoff.
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
predictions (Tensor): Tensor of probabilities of size (B, L, L)
|
| 38 |
+
targets (Tensor): Tensor of true contacts of size (B, L, L)
|
| 39 |
+
src_lengths (Tensor, optional): Lengths of each sample in the batch, if using variable lengths.
|
| 40 |
+
If not provided, inferred from the size of the predictions.
|
| 41 |
+
minsep (int): Minimum separation distance to consider. We often want to measure contacts at a
|
| 42 |
+
certain range. Typical ranges are short [6, 12), medium [12, 24), and long [24, inf).
|
| 43 |
+
maxsep (int, optional): Used in conjunction with minsep to specify a contact range. If not provided uses
|
| 44 |
+
assumes no maximum range
|
| 45 |
+
override_length (int, optional): Used for casp evaluation where sometimes the "true" length is not
|
| 46 |
+
the same as the length of the input. Kept for posterity, we probably don't need this argument.
|
| 47 |
+
"""
|
| 48 |
+
if predictions.dim() == 2:
|
| 49 |
+
predictions = predictions.unsqueeze(0)
|
| 50 |
+
if targets.dim() == 2:
|
| 51 |
+
targets = targets.unsqueeze(0)
|
| 52 |
+
|
| 53 |
+
# Check sizes
|
| 54 |
+
if predictions.size() != targets.size():
|
| 55 |
+
raise ValueError(
|
| 56 |
+
f"Size mismatch. Received predictions of size {predictions.size()}, "
|
| 57 |
+
f"targets of size {targets.size()}"
|
| 58 |
+
)
|
| 59 |
+
device = predictions.device
|
| 60 |
+
|
| 61 |
+
batch_size, seqlen, _ = predictions.size()
|
| 62 |
+
|
| 63 |
+
# Step 1) Construct a mask of size [B, L, L] to mask invalid contacts
|
| 64 |
+
seqlen_range = torch.arange(seqlen, device=device)
|
| 65 |
+
sep = seqlen_range.unsqueeze(0) - seqlen_range.unsqueeze(1)
|
| 66 |
+
sep = sep.unsqueeze(0)
|
| 67 |
+
# Mask contacts that are closer than minsep
|
| 68 |
+
valid_mask = sep >= minsep
|
| 69 |
+
# Mask contacts where target is negative (padding or unknown)
|
| 70 |
+
valid_mask = valid_mask & (targets >= 0) # negative targets are invalid
|
| 71 |
+
|
| 72 |
+
# Mask contacts that are farther than maxsep, if provided
|
| 73 |
+
if maxsep is not None:
|
| 74 |
+
valid_mask &= sep < maxsep
|
| 75 |
+
|
| 76 |
+
if src_lengths is not None:
|
| 77 |
+
# If the lengths of the individual sequences are provided, mask positions
|
| 78 |
+
# that are farther than the end of the sequence.
|
| 79 |
+
valid = seqlen_range.unsqueeze(0) < src_lengths.unsqueeze(1)
|
| 80 |
+
valid_mask &= valid.unsqueeze(1) & valid.unsqueeze(2)
|
| 81 |
+
else:
|
| 82 |
+
src_lengths = torch.full([batch_size], seqlen, device=device, dtype=torch.long)
|
| 83 |
+
|
| 84 |
+
# Fill in the logit tensor with -inf for all invalid positions
|
| 85 |
+
predictions = predictions.masked_fill(~valid_mask, float("-inf"))
|
| 86 |
+
|
| 87 |
+
# Step 2) Select the top half of the prediction (should be symmetric)
|
| 88 |
+
x_ind, y_ind = np.triu_indices(seqlen, minsep)
|
| 89 |
+
predictions_upper = predictions[:, x_ind, y_ind]
|
| 90 |
+
targets_upper = targets[:, x_ind, y_ind]
|
| 91 |
+
|
| 92 |
+
# Step 3) Select the topk values in each batch where k = L (length of sequence)
|
| 93 |
+
topk = seqlen if override_length is None else max(seqlen, override_length)
|
| 94 |
+
# Indices are the indices into the predictions corresponding to the most confident predictions
|
| 95 |
+
indices = predictions_upper.argsort(dim=-1, descending=True)[:, :topk]
|
| 96 |
+
# topk_targets are the target values corresponding to the above indices
|
| 97 |
+
topk_targets = targets_upper[torch.arange(batch_size).unsqueeze(1), indices]
|
| 98 |
+
if topk_targets.size(1) < topk:
|
| 99 |
+
# If there aren't enough targets, pad to the output.
|
| 100 |
+
topk_targets = F.pad(topk_targets, [0, topk - topk_targets.size(1)])
|
| 101 |
+
|
| 102 |
+
# Step 4) Sum the accuracy at of the top-i predictions for i in 1, L
|
| 103 |
+
# topk_targets => 1/0 true vs. false contact, sorted by confidence of prediction
|
| 104 |
+
# cmumulative sum => Number of correct answers for the top-i predictions.
|
| 105 |
+
cumulative_dist = topk_targets.type_as(predictions).cumsum(-1)
|
| 106 |
+
|
| 107 |
+
# Step 5) Find the gather indices. This should be P@(L / K) for varous values of K
|
| 108 |
+
# The values will differ for each batch.
|
| 109 |
+
gather_lengths = src_lengths.unsqueeze(1)
|
| 110 |
+
if override_length is not None:
|
| 111 |
+
gather_lengths = override_length * torch.ones_like(
|
| 112 |
+
gather_lengths, device=device
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# This gets you (0.1 * L, 0.2 * L, 0.3 * L, etc.)
|
| 116 |
+
gather_indices = (
|
| 117 |
+
(torch.arange(0.1, 1.1, 0.1, device=device).unsqueeze(0) * gather_lengths).type(
|
| 118 |
+
torch.long
|
| 119 |
+
)
|
| 120 |
+
- 1
|
| 121 |
+
).clamp_min(0)
|
| 122 |
+
|
| 123 |
+
# Step 6) Gather the results and divide by the number of guesses to get the precision.
|
| 124 |
+
binned_cumulative_dist = cumulative_dist.gather(1, gather_indices)
|
| 125 |
+
binned_precisions = binned_cumulative_dist / (gather_indices + 1).type_as(
|
| 126 |
+
binned_cumulative_dist
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Select specific P@L/k. pl5 is index 1 b/c that corresponds to L * 0.2 in
|
| 130 |
+
# gather_indices above
|
| 131 |
+
pl5 = binned_precisions[:, 1]
|
| 132 |
+
# pl2 = binned_precisions[:, 4]
|
| 133 |
+
pl = binned_precisions[:, 9]
|
| 134 |
+
# AUC is the integral wrt K of P@L/K for K in range(1, L)
|
| 135 |
+
auc = binned_precisions.mean(-1)
|
| 136 |
+
|
| 137 |
+
return {"AUC": auc, "P@L": pl, "P@L5": pl5}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def compute_lddt(
|
| 141 |
+
all_atom_pred_pos: torch.Tensor,
|
| 142 |
+
all_atom_positions: torch.Tensor,
|
| 143 |
+
all_atom_mask: torch.Tensor,
|
| 144 |
+
pairwise_all_atom_mask: torch.Tensor | None = None,
|
| 145 |
+
cutoff: float | torch.Tensor = 15.0,
|
| 146 |
+
eps: float = 1e-10,
|
| 147 |
+
per_residue: bool = True,
|
| 148 |
+
sequence_id: torch.Tensor | None = None,
|
| 149 |
+
) -> torch.Tensor:
|
| 150 |
+
"""
|
| 151 |
+
Computes LDDT for a protein. Tensor sizes below include some optional dimensions. Specifically:
|
| 152 |
+
Nstates:
|
| 153 |
+
all_atom_pred_pos can contain multiple states in the first dimension which corresponds to outputs from different layers of a model (e.g. each IPA block). The return size will be [Nstates x Batch size] if this is included.
|
| 154 |
+
Natoms:
|
| 155 |
+
LDDT can be computed for all atoms or some atoms. The second to last dimension should contain the *FLATTENED* representation of L x Natoms. If you want to calculate for atom37, e.g., this will be of size (L * 37). If you are only calculating CA LDDT, it will be of size L.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
all_atom_pred_pos (Tensor[float], [(Nstates x) B x (L * Natoms x) 3]): Tensor of predicted positions
|
| 159 |
+
all_atom_positions (Tensor[float], [B x (L * Natoms x) 3]): Tensor of true positions
|
| 160 |
+
all_atom_mask (Tensor[float], [B x (L * Natoms)]): Tensor of masks, indicating whether an atom exists.
|
| 161 |
+
pairwise_all_atom_mask (Tensor[float], [B x (L * Natoms x L * Natoms)], optional): Tensor of masks, indicating whether a pair of atoms should be considered in the LDDT calculation.
|
| 162 |
+
cutoff (float): Max distance to score lddt over. This can either be a float, or a tensor of shape [B, L, L] to allow for per-residue cutoffs, e.g. if you want to use a different cutoff for nucleic acids.
|
| 163 |
+
per_residue (bool): Whether to return per-residue or full-protein lddt.
|
| 164 |
+
sequence_id (Tensor, optional): Sequence id tensor for binpacking. NOTE: only supported for lddt_ca calculations, not when Natoms is passed!
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
LDDT Tensor:
|
| 168 |
+
if per_residue:
|
| 169 |
+
Tensor[float], [(Nstates x) B x (L * Natoms)]
|
| 170 |
+
else:
|
| 171 |
+
Tensor[float], [(Nstates x) B]
|
| 172 |
+
"""
|
| 173 |
+
all_atom_mask = all_atom_mask[..., None] # add a dimension for broadcasting
|
| 174 |
+
dmat_true = torch.sqrt(
|
| 175 |
+
eps
|
| 176 |
+
+ torch.sum(
|
| 177 |
+
(all_atom_positions[..., None, :] - all_atom_positions[..., None, :, :])
|
| 178 |
+
** 2,
|
| 179 |
+
dim=-1,
|
| 180 |
+
)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
dmat_pred = torch.sqrt(
|
| 184 |
+
eps
|
| 185 |
+
+ torch.sum(
|
| 186 |
+
(all_atom_pred_pos[..., None, :] - all_atom_pred_pos[..., None, :, :]) ** 2,
|
| 187 |
+
dim=-1,
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
mask = all_atom_mask * rearrange(all_atom_mask, "... a b -> ... b a")
|
| 191 |
+
if pairwise_all_atom_mask is not None:
|
| 192 |
+
mask = mask * pairwise_all_atom_mask
|
| 193 |
+
|
| 194 |
+
if sequence_id is not None:
|
| 195 |
+
# TODO: This will work for lddt_ca, but not for regular lddt
|
| 196 |
+
# Problem is that regular lddt has natoms * nres scores, so would need to repeat this mask by natoms
|
| 197 |
+
# Leaving for now because it won't fail silently so should be ook.
|
| 198 |
+
seqid_mask = sequence_id[..., None] == sequence_id[..., None, :]
|
| 199 |
+
mask = mask * seqid_mask.type_as(mask)
|
| 200 |
+
|
| 201 |
+
return compute_lddt_from_dmat(
|
| 202 |
+
dmat_pred, dmat_true, mask, cutoff=cutoff, eps=eps, per_residue=per_residue
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def compute_lddt_from_dmat(
|
| 207 |
+
dmat_pred: torch.Tensor,
|
| 208 |
+
dmat_true: torch.Tensor,
|
| 209 |
+
pairwise_mask: torch.Tensor,
|
| 210 |
+
cutoff: float | torch.Tensor = 15.0,
|
| 211 |
+
eps: float = 1e-10,
|
| 212 |
+
per_residue: bool = True,
|
| 213 |
+
):
|
| 214 |
+
"""
|
| 215 |
+
Compute LDDT from pre-computed distance matrices.
|
| 216 |
+
This is useful when you want to compute LDDT with multiple different masks or cutoffs, e.g. for different molecule types (protein, nucleic acid, etc.).
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
dmat_pred (Tensor[float], [B x L x L]): Predicted distance matrix
|
| 220 |
+
dmat_true (Tensor[float], [B x L x L]): True distance matrix
|
| 221 |
+
pairwise_mask (Tensor[float], [B x L x L]): Pairwise mask indicating which pairs of atoms to consider
|
| 222 |
+
cutoff (float): Max distance to score lddt over. This can either be a float, or a tensor of shape [B, L, L] to allow for per-residue cutoffs, e.g. if you want to use a different cutoff for nucleic acids.
|
| 223 |
+
per_residue (bool): Whether to return per-residue or full-protein lddt.
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
LDDT Tensor:
|
| 227 |
+
if per_residue:
|
| 228 |
+
Tensor[float], [B x L]
|
| 229 |
+
else:
|
| 230 |
+
Tensor[float], [B]
|
| 231 |
+
"""
|
| 232 |
+
n = dmat_true.size(-1)
|
| 233 |
+
dists_to_score = (
|
| 234 |
+
(dmat_true < cutoff)
|
| 235 |
+
* pairwise_mask
|
| 236 |
+
* (1.0 - torch.eye(n, device=dmat_true.device))
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
dist_l1 = torch.abs(dmat_true - dmat_pred)
|
| 240 |
+
score = (
|
| 241 |
+
(dist_l1 < 0.5).type(dist_l1.dtype)
|
| 242 |
+
+ (dist_l1 < 1.0).type(dist_l1.dtype)
|
| 243 |
+
+ (dist_l1 < 2.0).type(dist_l1.dtype)
|
| 244 |
+
+ (dist_l1 < 4.0).type(dist_l1.dtype)
|
| 245 |
+
)
|
| 246 |
+
score = score * 0.25
|
| 247 |
+
|
| 248 |
+
dims = (-1,) if per_residue else (-2, -1)
|
| 249 |
+
norm = 1.0 / (eps + torch.sum(dists_to_score, dim=dims))
|
| 250 |
+
score = norm * (eps + torch.sum(dists_to_score * score, dim=dims))
|
| 251 |
+
return score
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def compute_lddt_ca(
|
| 255 |
+
all_atom_pred_pos: torch.Tensor,
|
| 256 |
+
all_atom_positions: torch.Tensor,
|
| 257 |
+
all_atom_mask: torch.Tensor,
|
| 258 |
+
cutoff: float = 15.0,
|
| 259 |
+
eps: float = 1e-10,
|
| 260 |
+
per_residue: bool = True,
|
| 261 |
+
sequence_id: torch.Tensor | None = None,
|
| 262 |
+
) -> torch.Tensor:
|
| 263 |
+
ca_pos = residue_constants.atom_order["CA"]
|
| 264 |
+
if all_atom_pred_pos.dim() != 3:
|
| 265 |
+
all_atom_pred_pos = all_atom_pred_pos[..., ca_pos, :]
|
| 266 |
+
all_atom_positions = all_atom_positions[..., ca_pos, :]
|
| 267 |
+
all_atom_mask = all_atom_mask[..., ca_pos]
|
| 268 |
+
|
| 269 |
+
return compute_lddt(
|
| 270 |
+
all_atom_pred_pos,
|
| 271 |
+
all_atom_positions,
|
| 272 |
+
all_atom_mask,
|
| 273 |
+
cutoff=cutoff,
|
| 274 |
+
eps=eps,
|
| 275 |
+
per_residue=per_residue,
|
| 276 |
+
sequence_id=sequence_id,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
# NOTE(roshan): no_grad required for stack_variable_length_tensors apparently... let's revisit if we want to backprop
|
| 281 |
+
@torch.no_grad()
|
| 282 |
+
@autocast("cuda", enabled=False)
|
| 283 |
+
def compute_rmsd(
|
| 284 |
+
mobile: torch.Tensor,
|
| 285 |
+
target: torch.Tensor,
|
| 286 |
+
atom_exists_mask: torch.Tensor | None = None,
|
| 287 |
+
sequence_id: torch.Tensor | None = None,
|
| 288 |
+
reduction: str = "batch",
|
| 289 |
+
):
|
| 290 |
+
"""
|
| 291 |
+
Compute RMSD between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 295 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 296 |
+
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 297 |
+
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 298 |
+
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
If reduction == "batch":
|
| 302 |
+
(torch.Tensor): 0-dim, Average Root Mean Square Deviation between the structures for each batch
|
| 303 |
+
If reduction == "per_sample":
|
| 304 |
+
(torch.Tensor): (B,)-dim, Root Mean Square Deviation between the structures for each batch
|
| 305 |
+
If reduction == "per_residue":
|
| 306 |
+
(torch.Tensor): (B, N)-dim, Root Mean Square Deviation between the structures for residue in the batch
|
| 307 |
+
"""
|
| 308 |
+
|
| 309 |
+
(centered_mobile, _, centered_target, _, rotation_matrix, num_valid_atoms) = (
|
| 310 |
+
compute_alignment_tensors(
|
| 311 |
+
mobile=mobile,
|
| 312 |
+
target=target,
|
| 313 |
+
atom_exists_mask=atom_exists_mask,
|
| 314 |
+
sequence_id=sequence_id,
|
| 315 |
+
)
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
# Apply transformation to centered structure
|
| 319 |
+
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 320 |
+
|
| 321 |
+
# Compute rmsd for centered structures
|
| 322 |
+
rmsd = compute_rmsd_no_alignment(
|
| 323 |
+
rotated_mobile, centered_target, num_valid_atoms, reduction=reduction
|
| 324 |
+
)
|
| 325 |
+
if reduction == "per_residue" and sequence_id is not None:
|
| 326 |
+
rmsd = binpack(rmsd, sequence_id, pad_value=0)
|
| 327 |
+
return rmsd
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def compute_gdt_ts(
|
| 331 |
+
mobile: torch.Tensor,
|
| 332 |
+
target: torch.Tensor,
|
| 333 |
+
atom_exists_mask: torch.Tensor | None = None,
|
| 334 |
+
sequence_id: torch.Tensor | None = None,
|
| 335 |
+
reduction: str = "per_sample",
|
| 336 |
+
):
|
| 337 |
+
"""
|
| 338 |
+
Compute GDT_TS between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 339 |
+
|
| 340 |
+
Args:
|
| 341 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 342 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 343 |
+
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 344 |
+
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 345 |
+
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 346 |
+
|
| 347 |
+
Returns:
|
| 348 |
+
If reduction == "batch":
|
| 349 |
+
(torch.Tensor): 0-dim, GDT_TS between the structures for each batch
|
| 350 |
+
If reduction == "per_sample":
|
| 351 |
+
(torch.Tensor): (B,)-dim, GDT_TS between the structures for each sample in the batch
|
| 352 |
+
"""
|
| 353 |
+
if atom_exists_mask is None:
|
| 354 |
+
atom_exists_mask = torch.isfinite(target).all(dim=-1)
|
| 355 |
+
(centered_mobile, _, centered_target, _, rotation_matrix, _) = (
|
| 356 |
+
compute_alignment_tensors(
|
| 357 |
+
mobile=mobile,
|
| 358 |
+
target=target,
|
| 359 |
+
atom_exists_mask=atom_exists_mask,
|
| 360 |
+
sequence_id=sequence_id,
|
| 361 |
+
)
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Apply transformation to centered structure
|
| 365 |
+
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 366 |
+
|
| 367 |
+
# the coordinate tensors returned by `compute_alignment_tensors` are unbinpacked and contain zeros for invalid positions
|
| 368 |
+
# so `compute_gdt_ts_no_alignment` requires `atom_exists_mask` to be passed and be unbinpacked
|
| 369 |
+
if sequence_id is not None:
|
| 370 |
+
atom_exists_mask = unbinpack(atom_exists_mask, sequence_id, pad_value=False)
|
| 371 |
+
return compute_gdt_ts_no_alignment(
|
| 372 |
+
rotated_mobile, centered_target, atom_exists_mask, reduction
|
| 373 |
+
)
|
esmfold2_misc.py
CHANGED
|
@@ -1,504 +1,504 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
from collections import defaultdict
|
| 5 |
-
from contextlib import nullcontext
|
| 6 |
-
from dataclasses import is_dataclass
|
| 7 |
-
from io import BytesIO
|
| 8 |
-
from typing import (
|
| 9 |
-
Any,
|
| 10 |
-
ContextManager,
|
| 11 |
-
Generator,
|
| 12 |
-
Iterable,
|
| 13 |
-
Protocol,
|
| 14 |
-
Sequence,
|
| 15 |
-
TypeVar,
|
| 16 |
-
runtime_checkable,
|
| 17 |
-
)
|
| 18 |
-
from warnings import warn
|
| 19 |
-
|
| 20 |
-
import huggingface_hub
|
| 21 |
-
import numpy as np
|
| 22 |
-
import torch
|
| 23 |
-
import zstd
|
| 24 |
-
|
| 25 |
-
from .esmfold2_constants_esm3 import CHAIN_BREAK_STR
|
| 26 |
-
from .esmfold2_utils_types import FunctionAnnotation
|
| 27 |
-
|
| 28 |
-
MAX_SUPPORTED_DISTANCE = 1e6
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
TSequence = TypeVar("TSequence", bound=Sequence)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@runtime_checkable
|
| 35 |
-
class Concatable(Protocol):
|
| 36 |
-
@classmethod
|
| 37 |
-
def concat(cls, objs: list[Concatable]) -> Concatable: ...
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def slice_python_object_as_numpy(
|
| 41 |
-
obj: TSequence, idx: int | list[int] | slice | np.ndarray
|
| 42 |
-
) -> TSequence:
|
| 43 |
-
"""
|
| 44 |
-
Slice a python object (like a list, string, or tuple) as if it was a numpy object.
|
| 45 |
-
|
| 46 |
-
Example:
|
| 47 |
-
>>> obj = "ABCDE"
|
| 48 |
-
>>> slice_python_object_as_numpy(obj, [1, 3, 4])
|
| 49 |
-
"BDE"
|
| 50 |
-
|
| 51 |
-
>>> obj = [1, 2, 3, 4, 5]
|
| 52 |
-
>>> slice_python_object_as_numpy(obj, np.arange(5) < 3)
|
| 53 |
-
[1, 2, 3]
|
| 54 |
-
"""
|
| 55 |
-
if np.isscalar(idx):
|
| 56 |
-
idx = [int(idx)] # type: ignore
|
| 57 |
-
|
| 58 |
-
if isinstance(idx, np.ndarray) and idx.dtype == bool:
|
| 59 |
-
sliced_obj = [obj[i] for i in np.where(idx)[0]]
|
| 60 |
-
elif isinstance(idx, slice):
|
| 61 |
-
sliced_obj = obj[idx]
|
| 62 |
-
else:
|
| 63 |
-
sliced_obj = [obj[i] for i in idx] # type: ignore
|
| 64 |
-
|
| 65 |
-
match obj, sliced_obj:
|
| 66 |
-
case str(), list():
|
| 67 |
-
sliced_obj = "".join(sliced_obj)
|
| 68 |
-
case _:
|
| 69 |
-
sliced_obj = obj.__class__(sliced_obj) # type: ignore
|
| 70 |
-
|
| 71 |
-
return sliced_obj # type: ignore
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def slice_any_object(
|
| 75 |
-
obj: TSequence, idx: int | list[int] | slice | np.ndarray
|
| 76 |
-
) -> TSequence:
|
| 77 |
-
"""
|
| 78 |
-
Slice a arbitrary object (like a list, string, or tuple) as if it was a numpy object. Similar to `slice_python_object_as_numpy`, but detects if it's a numpy array or Tensor and uses the existing slice method if so.
|
| 79 |
-
|
| 80 |
-
If the object is a dataclass, it will simply apply the index to the object, under the assumption that the object has correcty implemented numpy indexing.
|
| 81 |
-
|
| 82 |
-
Example:
|
| 83 |
-
>>> obj = "ABCDE"
|
| 84 |
-
>>> slice_any_object(obj, [1, 3, 4])
|
| 85 |
-
"BDE"
|
| 86 |
-
|
| 87 |
-
>>> obj = np.array([1, 2, 3, 4, 5])
|
| 88 |
-
>>> slice_any_object(obj, np.arange(5) < 3)
|
| 89 |
-
np.array([1, 2, 3])
|
| 90 |
-
|
| 91 |
-
>>> obj = ProteinChain.from_rcsb("1a3a", "A")
|
| 92 |
-
>>> slice_any_object(obj, np.arange(len(obj)) < 10)
|
| 93 |
-
# ProteinChain w/ length 10
|
| 94 |
-
|
| 95 |
-
"""
|
| 96 |
-
if isinstance(obj, (np.ndarray, torch.Tensor)):
|
| 97 |
-
return obj[idx] # type: ignore
|
| 98 |
-
elif is_dataclass(obj):
|
| 99 |
-
# if passing a dataclass, assume it implements a custom slice
|
| 100 |
-
return obj[idx] # type: ignore
|
| 101 |
-
else:
|
| 102 |
-
return slice_python_object_as_numpy(obj, idx)
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def rbf(values, v_min, v_max, n_bins=16):
|
| 106 |
-
"""
|
| 107 |
-
Returns RBF encodings in a new dimension at the end.
|
| 108 |
-
"""
|
| 109 |
-
rbf_centers = torch.linspace(
|
| 110 |
-
v_min, v_max, n_bins, device=values.device, dtype=values.dtype
|
| 111 |
-
)
|
| 112 |
-
rbf_centers = rbf_centers.view([1] * len(values.shape) + [-1])
|
| 113 |
-
rbf_std = (v_max - v_min) / n_bins
|
| 114 |
-
z = (values.unsqueeze(-1) - rbf_centers) / rbf_std
|
| 115 |
-
return torch.exp(-(z**2))
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def batched_gather(data, inds, dim=0, no_batch_dims=0):
|
| 119 |
-
ranges = []
|
| 120 |
-
for i, s in enumerate(data.shape[:no_batch_dims]):
|
| 121 |
-
r = torch.arange(s)
|
| 122 |
-
r = r.view(*(*((1,) * i), -1, *((1,) * (len(inds.shape) - i - 1))))
|
| 123 |
-
ranges.append(r)
|
| 124 |
-
|
| 125 |
-
remaining_dims = [slice(None) for _ in range(len(data.shape) - no_batch_dims)]
|
| 126 |
-
remaining_dims[dim - no_batch_dims if dim >= 0 else dim] = inds
|
| 127 |
-
ranges.extend(remaining_dims)
|
| 128 |
-
return data[ranges]
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def node_gather(s: torch.Tensor, edges: torch.Tensor) -> torch.Tensor:
|
| 132 |
-
return batched_gather(s.unsqueeze(-3), edges, -2, no_batch_dims=len(s.shape) - 1)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def knn_graph(
|
| 136 |
-
coords: torch.Tensor,
|
| 137 |
-
coord_mask: torch.Tensor,
|
| 138 |
-
padding_mask: torch.Tensor,
|
| 139 |
-
sequence_id: torch.Tensor,
|
| 140 |
-
*,
|
| 141 |
-
no_knn: int,
|
| 142 |
-
):
|
| 143 |
-
L = coords.shape[-2]
|
| 144 |
-
num_by_dist = min(no_knn, L)
|
| 145 |
-
device = coords.device
|
| 146 |
-
|
| 147 |
-
coords = coords.nan_to_num()
|
| 148 |
-
coord_mask = ~(coord_mask[..., None, :] & coord_mask[..., :, None])
|
| 149 |
-
padding_pairwise_mask = padding_mask[..., None, :] | padding_mask[..., :, None]
|
| 150 |
-
if sequence_id is not None:
|
| 151 |
-
padding_pairwise_mask |= torch.unsqueeze(sequence_id, 1) != torch.unsqueeze(
|
| 152 |
-
sequence_id, 2
|
| 153 |
-
)
|
| 154 |
-
dists = (coords.unsqueeze(-2) - coords.unsqueeze(-3)).norm(dim=-1)
|
| 155 |
-
arange = torch.arange(L, device=device)
|
| 156 |
-
seq_dists = (arange.unsqueeze(-1) - arange.unsqueeze(-2)).abs()
|
| 157 |
-
# We only support up to a certain distance, above that, we use sequence distance
|
| 158 |
-
# instead. This is so that when a large portion of the structure is masked out,
|
| 159 |
-
# the edges are built according to sequence distance.
|
| 160 |
-
max_dist = MAX_SUPPORTED_DISTANCE
|
| 161 |
-
if not (dists[~coord_mask] < max_dist).all():
|
| 162 |
-
raise ValueError(
|
| 163 |
-
f"Coordinate pairwise distances exceed max supported distance ({max_dist}). "
|
| 164 |
-
)
|
| 165 |
-
struct_then_seq_dist = (
|
| 166 |
-
seq_dists.to(dists.dtype)
|
| 167 |
-
.mul(1e2)
|
| 168 |
-
.add(max_dist)
|
| 169 |
-
.where(coord_mask, dists)
|
| 170 |
-
.masked_fill(padding_pairwise_mask, torch.inf)
|
| 171 |
-
)
|
| 172 |
-
dists, edges = struct_then_seq_dist.sort(dim=-1, descending=False)
|
| 173 |
-
# This is a L x L tensor, where we index by rows first,
|
| 174 |
-
# and columns are the edges we should pick.
|
| 175 |
-
chosen_edges = edges[..., :num_by_dist]
|
| 176 |
-
chosen_mask = dists[..., :num_by_dist].isfinite()
|
| 177 |
-
return chosen_edges, chosen_mask
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def stack_variable_length_tensors(
|
| 181 |
-
sequences: Sequence[torch.Tensor],
|
| 182 |
-
constant_value: int | float = 0,
|
| 183 |
-
dtype: torch.dtype | None = None,
|
| 184 |
-
) -> torch.Tensor:
|
| 185 |
-
"""Automatically stack tensors together, padding variable lengths with the
|
| 186 |
-
value in constant_value. Handles an arbitrary number of dimensions.
|
| 187 |
-
|
| 188 |
-
Examples:
|
| 189 |
-
>>> tensor1, tensor2 = torch.ones([2]), torch.ones([5])
|
| 190 |
-
>>> stack_variable_length_tensors(tensor1, tensor2)
|
| 191 |
-
tensor of shape [2, 5]. First row is [1, 1, 0, 0, 0]. Second row is all ones.
|
| 192 |
-
|
| 193 |
-
>>> tensor1, tensor2 = torch.ones([2, 4]), torch.ones([5, 3])
|
| 194 |
-
>>> stack_variable_length_tensors(tensor1, tensor2)
|
| 195 |
-
tensor of shape [2, 5, 4]
|
| 196 |
-
"""
|
| 197 |
-
batch_size = len(sequences)
|
| 198 |
-
shape = [batch_size] + np.max([seq.shape for seq in sequences], 0).tolist()
|
| 199 |
-
|
| 200 |
-
if dtype is None:
|
| 201 |
-
dtype = sequences[0].dtype
|
| 202 |
-
device = sequences[0].device
|
| 203 |
-
|
| 204 |
-
array = torch.full(shape, constant_value, dtype=dtype, device=device)
|
| 205 |
-
for arr, seq in zip(array, sequences):
|
| 206 |
-
arrslice = tuple(slice(dim) for dim in seq.shape)
|
| 207 |
-
arr[arrslice] = seq
|
| 208 |
-
|
| 209 |
-
return array
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def binpack(
|
| 213 |
-
tensor: torch.Tensor, sequence_id: torch.Tensor | None, pad_value: int | float
|
| 214 |
-
):
|
| 215 |
-
"""
|
| 216 |
-
Args:
|
| 217 |
-
tensor (Tensor): [B, L, ...]
|
| 218 |
-
|
| 219 |
-
Returns:
|
| 220 |
-
Tensor: [B_binpacked, L_binpacked, ...]
|
| 221 |
-
"""
|
| 222 |
-
if sequence_id is None:
|
| 223 |
-
return tensor
|
| 224 |
-
|
| 225 |
-
num_sequences = sequence_id.max(dim=-1).values + 1
|
| 226 |
-
|
| 227 |
-
dims = sequence_id.shape + tensor.shape[2:]
|
| 228 |
-
output_tensor = torch.full(
|
| 229 |
-
dims, fill_value=pad_value, dtype=tensor.dtype, device=tensor.device
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
idx = 0
|
| 233 |
-
for batch_idx, (batch_seqid, batch_num_sequences) in enumerate(
|
| 234 |
-
zip(sequence_id, num_sequences)
|
| 235 |
-
):
|
| 236 |
-
for seqid in range(batch_num_sequences):
|
| 237 |
-
mask = batch_seqid == seqid
|
| 238 |
-
output_tensor[batch_idx, mask] = tensor[idx, : mask.sum()]
|
| 239 |
-
idx += 1
|
| 240 |
-
return output_tensor
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
def unbinpack(
|
| 244 |
-
tensor: torch.Tensor, sequence_id: torch.Tensor | None, pad_value: int | float
|
| 245 |
-
):
|
| 246 |
-
"""
|
| 247 |
-
Args:
|
| 248 |
-
tensor (Tensor): [B, L, ...]
|
| 249 |
-
|
| 250 |
-
Returns:
|
| 251 |
-
Tensor: [B_unbinpacked, L_unbinpack, ...]
|
| 252 |
-
"""
|
| 253 |
-
if sequence_id is None:
|
| 254 |
-
return tensor
|
| 255 |
-
|
| 256 |
-
unpacked_tensors = []
|
| 257 |
-
num_sequences = sequence_id.max(dim=-1).values + 1
|
| 258 |
-
for batch_idx, (batch_seqid, batch_num_sequences) in enumerate(
|
| 259 |
-
zip(sequence_id, num_sequences)
|
| 260 |
-
):
|
| 261 |
-
for seqid in range(batch_num_sequences):
|
| 262 |
-
mask = batch_seqid == seqid
|
| 263 |
-
unpacked = tensor[batch_idx, mask]
|
| 264 |
-
unpacked_tensors.append(unpacked)
|
| 265 |
-
return stack_variable_length_tensors(unpacked_tensors, pad_value)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def fp32_autocast_context(device_type: str) -> ContextManager[Any]: # type: ignore
|
| 269 |
-
"""
|
| 270 |
-
Returns an autocast context manager that disables downcasting by AMP.
|
| 271 |
-
|
| 272 |
-
Args:
|
| 273 |
-
device_type: The device type ('cpu' or 'cuda')
|
| 274 |
-
|
| 275 |
-
Returns:
|
| 276 |
-
An autocast context manager with the specified behavior.
|
| 277 |
-
"""
|
| 278 |
-
if device_type == "cpu":
|
| 279 |
-
return torch.amp.autocast(device_type, enabled=False) # type: ignore
|
| 280 |
-
elif device_type == "mps":
|
| 281 |
-
# For MPS, just return a no-op context manager (nullcontext) since MPS does not support autocast.
|
| 282 |
-
return nullcontext()
|
| 283 |
-
elif device_type == "cuda":
|
| 284 |
-
return torch.amp.autocast(device_type, dtype=torch.float32) # type: ignore
|
| 285 |
-
else:
|
| 286 |
-
raise ValueError(f"Unsupported device type: {device_type}")
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
def merge_ranges(ranges: list[range], merge_gap_max: int | None = None) -> list[range]:
|
| 290 |
-
"""Merge overlapping ranges into sorted, non-overlapping segments.
|
| 291 |
-
|
| 292 |
-
Args:
|
| 293 |
-
ranges: collection of ranges to merge.
|
| 294 |
-
merge_gap_max: optionally merge neighboring ranges that are separated by a gap
|
| 295 |
-
no larger than this size.
|
| 296 |
-
Returns:
|
| 297 |
-
non-overlapping ranges merged from the inputs, sorted by position.
|
| 298 |
-
"""
|
| 299 |
-
ranges = sorted(ranges, key=lambda r: r.start)
|
| 300 |
-
merge_gap_max = merge_gap_max if merge_gap_max is not None else 0
|
| 301 |
-
assert merge_gap_max >= 0, f"Invalid merge_gap_max: {merge_gap_max}"
|
| 302 |
-
|
| 303 |
-
merged = []
|
| 304 |
-
for r in ranges:
|
| 305 |
-
if not merged:
|
| 306 |
-
merged.append(r)
|
| 307 |
-
else:
|
| 308 |
-
last = merged[-1]
|
| 309 |
-
if last.stop + merge_gap_max >= r.start:
|
| 310 |
-
merged[-1] = range(last.start, max(last.stop, r.stop))
|
| 311 |
-
else:
|
| 312 |
-
merged.append(r)
|
| 313 |
-
return merged
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
def merge_annotations(
|
| 317 |
-
annotations: list[FunctionAnnotation], merge_gap_max: int | None = None
|
| 318 |
-
) -> list[FunctionAnnotation]:
|
| 319 |
-
"""Merges annotations into non-overlapping segments.
|
| 320 |
-
|
| 321 |
-
Args:
|
| 322 |
-
annotations: annotations to merge.
|
| 323 |
-
merge_gap_max: optionally merge neighboring ranges that are separated by a gap
|
| 324 |
-
no larger than this size.
|
| 325 |
-
Returns:
|
| 326 |
-
non-overlapping annotations with gaps merged.
|
| 327 |
-
"""
|
| 328 |
-
grouped: dict[str, list[range]] = defaultdict(list)
|
| 329 |
-
for a in annotations:
|
| 330 |
-
# +1 since FunctionAnnotation.end is inlcusive.
|
| 331 |
-
grouped[a.label].append(range(a.start, a.end + 1))
|
| 332 |
-
|
| 333 |
-
merged = []
|
| 334 |
-
for label, ranges in grouped.items():
|
| 335 |
-
merged_ranges = merge_ranges(ranges, merge_gap_max=merge_gap_max)
|
| 336 |
-
for range_ in merged_ranges:
|
| 337 |
-
annotation = FunctionAnnotation(
|
| 338 |
-
label=label,
|
| 339 |
-
start=range_.start,
|
| 340 |
-
end=range_.stop - 1, # convert range.stop exclusive -> inclusive.
|
| 341 |
-
)
|
| 342 |
-
merged.append(annotation)
|
| 343 |
-
return merged
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
def replace_inf(data):
|
| 347 |
-
if data is None:
|
| 348 |
-
return None
|
| 349 |
-
array = np.asarray(data, dtype=np.float32)
|
| 350 |
-
array = np.where(np.isinf(array), 1000, array)
|
| 351 |
-
return array.tolist()
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
def maybe_tensor(x, convert_none_to_nan: bool = False) -> torch.Tensor | None:
|
| 355 |
-
if x is None:
|
| 356 |
-
return None
|
| 357 |
-
if isinstance(x, torch.Tensor):
|
| 358 |
-
return x
|
| 359 |
-
if isinstance(x, list) and all(isinstance(t, torch.Tensor) for t in x):
|
| 360 |
-
return torch.stack(x)
|
| 361 |
-
if convert_none_to_nan:
|
| 362 |
-
x = np.asarray(x, dtype=np.float32)
|
| 363 |
-
x = np.where(x is None, np.nan, x)
|
| 364 |
-
return torch.tensor(x)
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
def maybe_list(x, convert_nan_to_none: bool = False) -> list | None:
|
| 368 |
-
if x is None:
|
| 369 |
-
return None
|
| 370 |
-
if not convert_nan_to_none:
|
| 371 |
-
return x.tolist()
|
| 372 |
-
|
| 373 |
-
# Handle both torch.tensor and np.ndarray input.
|
| 374 |
-
if isinstance(x, torch.Tensor):
|
| 375 |
-
nan_mask = torch.isnan(x).cpu().numpy()
|
| 376 |
-
np_arr = x.cpu().numpy().astype(object)
|
| 377 |
-
elif isinstance(x, np.ndarray):
|
| 378 |
-
nan_mask = np.isnan(x)
|
| 379 |
-
np_arr = x.astype(object)
|
| 380 |
-
else:
|
| 381 |
-
raise TypeError("maybe_list can only work with torch.tensor or np.ndarray.")
|
| 382 |
-
|
| 383 |
-
np_arr[nan_mask] = None
|
| 384 |
-
return np_arr.tolist()
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
def huggingfacehub_login():
|
| 388 |
-
"""Authenticates with the Hugging Face Hub using the HF_TOKEN environment
|
| 389 |
-
variable, else by prompting the user"""
|
| 390 |
-
token = os.environ.get("HF_TOKEN")
|
| 391 |
-
huggingface_hub.login(token=token)
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
def get_chainbreak_boundaries_from_sequence(sequence: Sequence[str]) -> np.ndarray:
|
| 395 |
-
chain_boundaries = [0]
|
| 396 |
-
for i, aa in enumerate(sequence):
|
| 397 |
-
if aa == CHAIN_BREAK_STR:
|
| 398 |
-
if i == (len(sequence) - 1):
|
| 399 |
-
raise ValueError(
|
| 400 |
-
"Encountered chain break token at end of sequence, this is unexpected."
|
| 401 |
-
)
|
| 402 |
-
if i == (len(sequence) - 2):
|
| 403 |
-
warn(
|
| 404 |
-
"Encountered chain break token at penultimate position, this is unexpected."
|
| 405 |
-
)
|
| 406 |
-
chain_boundaries.append(i)
|
| 407 |
-
chain_boundaries.append(i + 1)
|
| 408 |
-
chain_boundaries.append(len(sequence))
|
| 409 |
-
assert len(chain_boundaries) % 2 == 0
|
| 410 |
-
chain_boundaries = np.array(chain_boundaries).reshape(-1, 2)
|
| 411 |
-
return chain_boundaries
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
def deserialize_tensors(b: bytes) -> Any:
|
| 415 |
-
buf = BytesIO(zstd.ZSTD_uncompress(b))
|
| 416 |
-
d = torch.load(buf, map_location="cpu", weights_only=False)
|
| 417 |
-
return d
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
def join_lists(
|
| 421 |
-
lists: Sequence[Sequence[Any]], separator: Sequence[Any] | None = None
|
| 422 |
-
) -> list[Any]:
|
| 423 |
-
"""Joins multiple lists with separator element. Like str.join but for lists.
|
| 424 |
-
|
| 425 |
-
Example: [[1, 2], [3], [4]], separator=[0] -> [1, 2, 0, 3, 0, 4]
|
| 426 |
-
|
| 427 |
-
Args:
|
| 428 |
-
lists: Lists of elements to chain
|
| 429 |
-
separator: separators to intsert between chained output.
|
| 430 |
-
Returns:
|
| 431 |
-
Joined lists.
|
| 432 |
-
"""
|
| 433 |
-
if not lists:
|
| 434 |
-
return []
|
| 435 |
-
joined = []
|
| 436 |
-
joined.extend(lists[0])
|
| 437 |
-
for l in lists[1:]:
|
| 438 |
-
if separator:
|
| 439 |
-
joined.extend(separator)
|
| 440 |
-
joined.extend(l)
|
| 441 |
-
return joined
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
def iterate_with_intermediate(
|
| 445 |
-
lists: Iterable, intermediate
|
| 446 |
-
) -> Generator[Any, None, None]:
|
| 447 |
-
"""
|
| 448 |
-
Iterate over the iterable, yielding the intermediate value between
|
| 449 |
-
every element of the intermediate. Useful for joining objects with
|
| 450 |
-
separator tokens.
|
| 451 |
-
"""
|
| 452 |
-
it = iter(lists)
|
| 453 |
-
yield next(it)
|
| 454 |
-
for l in it:
|
| 455 |
-
yield intermediate
|
| 456 |
-
yield l
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
def concat_objects(objs: Sequence[Any], separator: Any | None = None):
|
| 460 |
-
"""
|
| 461 |
-
Concat objects with each other using a separator token.
|
| 462 |
-
|
| 463 |
-
Supports:
|
| 464 |
-
- Concatable (objects that implement `concat` classmethod)
|
| 465 |
-
- strings
|
| 466 |
-
- lists
|
| 467 |
-
- numpy arrays
|
| 468 |
-
- torch Tensors
|
| 469 |
-
|
| 470 |
-
Example:
|
| 471 |
-
>>> foo = "abc"
|
| 472 |
-
>>> bar = "def"
|
| 473 |
-
>>> concat_objects([foo, bar], "|")
|
| 474 |
-
"abc|def"
|
| 475 |
-
"""
|
| 476 |
-
match objs[0]:
|
| 477 |
-
case Concatable():
|
| 478 |
-
return objs[0].__class__.concat(objs) # type: ignore
|
| 479 |
-
case str():
|
| 480 |
-
assert isinstance(
|
| 481 |
-
separator, str
|
| 482 |
-
), "Trying to join strings but separator is not a string"
|
| 483 |
-
return separator.join(objs)
|
| 484 |
-
case list():
|
| 485 |
-
if separator is not None:
|
| 486 |
-
return join_lists(objs, [separator])
|
| 487 |
-
else:
|
| 488 |
-
return join_lists(objs)
|
| 489 |
-
case np.ndarray():
|
| 490 |
-
if separator is not None:
|
| 491 |
-
return np.concatenate(
|
| 492 |
-
list(iterate_with_intermediate(objs, np.array([separator])))
|
| 493 |
-
)
|
| 494 |
-
else:
|
| 495 |
-
return np.concatenate(objs)
|
| 496 |
-
case torch.Tensor():
|
| 497 |
-
if separator is not None:
|
| 498 |
-
return torch.cat(
|
| 499 |
-
list(iterate_with_intermediate(objs, torch.tensor([separator])))
|
| 500 |
-
)
|
| 501 |
-
else:
|
| 502 |
-
return torch.cat(objs) # type: ignore
|
| 503 |
-
case _:
|
| 504 |
-
raise TypeError(type(objs[0]))
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
from contextlib import nullcontext
|
| 6 |
+
from dataclasses import is_dataclass
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
from typing import (
|
| 9 |
+
Any,
|
| 10 |
+
ContextManager,
|
| 11 |
+
Generator,
|
| 12 |
+
Iterable,
|
| 13 |
+
Protocol,
|
| 14 |
+
Sequence,
|
| 15 |
+
TypeVar,
|
| 16 |
+
runtime_checkable,
|
| 17 |
+
)
|
| 18 |
+
from warnings import warn
|
| 19 |
+
|
| 20 |
+
import huggingface_hub
|
| 21 |
+
import numpy as np
|
| 22 |
+
import torch
|
| 23 |
+
import zstd
|
| 24 |
+
|
| 25 |
+
from .esmfold2_constants_esm3 import CHAIN_BREAK_STR
|
| 26 |
+
from .esmfold2_utils_types import FunctionAnnotation
|
| 27 |
+
|
| 28 |
+
MAX_SUPPORTED_DISTANCE = 1e6
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
TSequence = TypeVar("TSequence", bound=Sequence)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@runtime_checkable
|
| 35 |
+
class Concatable(Protocol):
|
| 36 |
+
@classmethod
|
| 37 |
+
def concat(cls, objs: list[Concatable]) -> Concatable: ...
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def slice_python_object_as_numpy(
|
| 41 |
+
obj: TSequence, idx: int | list[int] | slice | np.ndarray
|
| 42 |
+
) -> TSequence:
|
| 43 |
+
"""
|
| 44 |
+
Slice a python object (like a list, string, or tuple) as if it was a numpy object.
|
| 45 |
+
|
| 46 |
+
Example:
|
| 47 |
+
>>> obj = "ABCDE"
|
| 48 |
+
>>> slice_python_object_as_numpy(obj, [1, 3, 4])
|
| 49 |
+
"BDE"
|
| 50 |
+
|
| 51 |
+
>>> obj = [1, 2, 3, 4, 5]
|
| 52 |
+
>>> slice_python_object_as_numpy(obj, np.arange(5) < 3)
|
| 53 |
+
[1, 2, 3]
|
| 54 |
+
"""
|
| 55 |
+
if np.isscalar(idx):
|
| 56 |
+
idx = [int(idx)] # type: ignore
|
| 57 |
+
|
| 58 |
+
if isinstance(idx, np.ndarray) and idx.dtype == bool:
|
| 59 |
+
sliced_obj = [obj[i] for i in np.where(idx)[0]]
|
| 60 |
+
elif isinstance(idx, slice):
|
| 61 |
+
sliced_obj = obj[idx]
|
| 62 |
+
else:
|
| 63 |
+
sliced_obj = [obj[i] for i in idx] # type: ignore
|
| 64 |
+
|
| 65 |
+
match obj, sliced_obj:
|
| 66 |
+
case str(), list():
|
| 67 |
+
sliced_obj = "".join(sliced_obj)
|
| 68 |
+
case _:
|
| 69 |
+
sliced_obj = obj.__class__(sliced_obj) # type: ignore
|
| 70 |
+
|
| 71 |
+
return sliced_obj # type: ignore
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def slice_any_object(
|
| 75 |
+
obj: TSequence, idx: int | list[int] | slice | np.ndarray
|
| 76 |
+
) -> TSequence:
|
| 77 |
+
"""
|
| 78 |
+
Slice a arbitrary object (like a list, string, or tuple) as if it was a numpy object. Similar to `slice_python_object_as_numpy`, but detects if it's a numpy array or Tensor and uses the existing slice method if so.
|
| 79 |
+
|
| 80 |
+
If the object is a dataclass, it will simply apply the index to the object, under the assumption that the object has correcty implemented numpy indexing.
|
| 81 |
+
|
| 82 |
+
Example:
|
| 83 |
+
>>> obj = "ABCDE"
|
| 84 |
+
>>> slice_any_object(obj, [1, 3, 4])
|
| 85 |
+
"BDE"
|
| 86 |
+
|
| 87 |
+
>>> obj = np.array([1, 2, 3, 4, 5])
|
| 88 |
+
>>> slice_any_object(obj, np.arange(5) < 3)
|
| 89 |
+
np.array([1, 2, 3])
|
| 90 |
+
|
| 91 |
+
>>> obj = ProteinChain.from_rcsb("1a3a", "A")
|
| 92 |
+
>>> slice_any_object(obj, np.arange(len(obj)) < 10)
|
| 93 |
+
# ProteinChain w/ length 10
|
| 94 |
+
|
| 95 |
+
"""
|
| 96 |
+
if isinstance(obj, (np.ndarray, torch.Tensor)):
|
| 97 |
+
return obj[idx] # type: ignore
|
| 98 |
+
elif is_dataclass(obj):
|
| 99 |
+
# if passing a dataclass, assume it implements a custom slice
|
| 100 |
+
return obj[idx] # type: ignore
|
| 101 |
+
else:
|
| 102 |
+
return slice_python_object_as_numpy(obj, idx)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def rbf(values, v_min, v_max, n_bins=16):
|
| 106 |
+
"""
|
| 107 |
+
Returns RBF encodings in a new dimension at the end.
|
| 108 |
+
"""
|
| 109 |
+
rbf_centers = torch.linspace(
|
| 110 |
+
v_min, v_max, n_bins, device=values.device, dtype=values.dtype
|
| 111 |
+
)
|
| 112 |
+
rbf_centers = rbf_centers.view([1] * len(values.shape) + [-1])
|
| 113 |
+
rbf_std = (v_max - v_min) / n_bins
|
| 114 |
+
z = (values.unsqueeze(-1) - rbf_centers) / rbf_std
|
| 115 |
+
return torch.exp(-(z**2))
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def batched_gather(data, inds, dim=0, no_batch_dims=0):
|
| 119 |
+
ranges = []
|
| 120 |
+
for i, s in enumerate(data.shape[:no_batch_dims]):
|
| 121 |
+
r = torch.arange(s)
|
| 122 |
+
r = r.view(*(*((1,) * i), -1, *((1,) * (len(inds.shape) - i - 1))))
|
| 123 |
+
ranges.append(r)
|
| 124 |
+
|
| 125 |
+
remaining_dims = [slice(None) for _ in range(len(data.shape) - no_batch_dims)]
|
| 126 |
+
remaining_dims[dim - no_batch_dims if dim >= 0 else dim] = inds
|
| 127 |
+
ranges.extend(remaining_dims)
|
| 128 |
+
return data[ranges]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def node_gather(s: torch.Tensor, edges: torch.Tensor) -> torch.Tensor:
|
| 132 |
+
return batched_gather(s.unsqueeze(-3), edges, -2, no_batch_dims=len(s.shape) - 1)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def knn_graph(
|
| 136 |
+
coords: torch.Tensor,
|
| 137 |
+
coord_mask: torch.Tensor,
|
| 138 |
+
padding_mask: torch.Tensor,
|
| 139 |
+
sequence_id: torch.Tensor,
|
| 140 |
+
*,
|
| 141 |
+
no_knn: int,
|
| 142 |
+
):
|
| 143 |
+
L = coords.shape[-2]
|
| 144 |
+
num_by_dist = min(no_knn, L)
|
| 145 |
+
device = coords.device
|
| 146 |
+
|
| 147 |
+
coords = coords.nan_to_num()
|
| 148 |
+
coord_mask = ~(coord_mask[..., None, :] & coord_mask[..., :, None])
|
| 149 |
+
padding_pairwise_mask = padding_mask[..., None, :] | padding_mask[..., :, None]
|
| 150 |
+
if sequence_id is not None:
|
| 151 |
+
padding_pairwise_mask |= torch.unsqueeze(sequence_id, 1) != torch.unsqueeze(
|
| 152 |
+
sequence_id, 2
|
| 153 |
+
)
|
| 154 |
+
dists = (coords.unsqueeze(-2) - coords.unsqueeze(-3)).norm(dim=-1)
|
| 155 |
+
arange = torch.arange(L, device=device)
|
| 156 |
+
seq_dists = (arange.unsqueeze(-1) - arange.unsqueeze(-2)).abs()
|
| 157 |
+
# We only support up to a certain distance, above that, we use sequence distance
|
| 158 |
+
# instead. This is so that when a large portion of the structure is masked out,
|
| 159 |
+
# the edges are built according to sequence distance.
|
| 160 |
+
max_dist = MAX_SUPPORTED_DISTANCE
|
| 161 |
+
if not (dists[~coord_mask] < max_dist).all():
|
| 162 |
+
raise ValueError(
|
| 163 |
+
f"Coordinate pairwise distances exceed max supported distance ({max_dist}). "
|
| 164 |
+
)
|
| 165 |
+
struct_then_seq_dist = (
|
| 166 |
+
seq_dists.to(dists.dtype)
|
| 167 |
+
.mul(1e2)
|
| 168 |
+
.add(max_dist)
|
| 169 |
+
.where(coord_mask, dists)
|
| 170 |
+
.masked_fill(padding_pairwise_mask, torch.inf)
|
| 171 |
+
)
|
| 172 |
+
dists, edges = struct_then_seq_dist.sort(dim=-1, descending=False)
|
| 173 |
+
# This is a L x L tensor, where we index by rows first,
|
| 174 |
+
# and columns are the edges we should pick.
|
| 175 |
+
chosen_edges = edges[..., :num_by_dist]
|
| 176 |
+
chosen_mask = dists[..., :num_by_dist].isfinite()
|
| 177 |
+
return chosen_edges, chosen_mask
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def stack_variable_length_tensors(
|
| 181 |
+
sequences: Sequence[torch.Tensor],
|
| 182 |
+
constant_value: int | float = 0,
|
| 183 |
+
dtype: torch.dtype | None = None,
|
| 184 |
+
) -> torch.Tensor:
|
| 185 |
+
"""Automatically stack tensors together, padding variable lengths with the
|
| 186 |
+
value in constant_value. Handles an arbitrary number of dimensions.
|
| 187 |
+
|
| 188 |
+
Examples:
|
| 189 |
+
>>> tensor1, tensor2 = torch.ones([2]), torch.ones([5])
|
| 190 |
+
>>> stack_variable_length_tensors(tensor1, tensor2)
|
| 191 |
+
tensor of shape [2, 5]. First row is [1, 1, 0, 0, 0]. Second row is all ones.
|
| 192 |
+
|
| 193 |
+
>>> tensor1, tensor2 = torch.ones([2, 4]), torch.ones([5, 3])
|
| 194 |
+
>>> stack_variable_length_tensors(tensor1, tensor2)
|
| 195 |
+
tensor of shape [2, 5, 4]
|
| 196 |
+
"""
|
| 197 |
+
batch_size = len(sequences)
|
| 198 |
+
shape = [batch_size] + np.max([seq.shape for seq in sequences], 0).tolist()
|
| 199 |
+
|
| 200 |
+
if dtype is None:
|
| 201 |
+
dtype = sequences[0].dtype
|
| 202 |
+
device = sequences[0].device
|
| 203 |
+
|
| 204 |
+
array = torch.full(shape, constant_value, dtype=dtype, device=device)
|
| 205 |
+
for arr, seq in zip(array, sequences):
|
| 206 |
+
arrslice = tuple(slice(dim) for dim in seq.shape)
|
| 207 |
+
arr[arrslice] = seq
|
| 208 |
+
|
| 209 |
+
return array
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def binpack(
|
| 213 |
+
tensor: torch.Tensor, sequence_id: torch.Tensor | None, pad_value: int | float
|
| 214 |
+
):
|
| 215 |
+
"""
|
| 216 |
+
Args:
|
| 217 |
+
tensor (Tensor): [B, L, ...]
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Tensor: [B_binpacked, L_binpacked, ...]
|
| 221 |
+
"""
|
| 222 |
+
if sequence_id is None:
|
| 223 |
+
return tensor
|
| 224 |
+
|
| 225 |
+
num_sequences = sequence_id.max(dim=-1).values + 1
|
| 226 |
+
|
| 227 |
+
dims = sequence_id.shape + tensor.shape[2:]
|
| 228 |
+
output_tensor = torch.full(
|
| 229 |
+
dims, fill_value=pad_value, dtype=tensor.dtype, device=tensor.device
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
idx = 0
|
| 233 |
+
for batch_idx, (batch_seqid, batch_num_sequences) in enumerate(
|
| 234 |
+
zip(sequence_id, num_sequences)
|
| 235 |
+
):
|
| 236 |
+
for seqid in range(batch_num_sequences):
|
| 237 |
+
mask = batch_seqid == seqid
|
| 238 |
+
output_tensor[batch_idx, mask] = tensor[idx, : mask.sum()]
|
| 239 |
+
idx += 1
|
| 240 |
+
return output_tensor
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def unbinpack(
|
| 244 |
+
tensor: torch.Tensor, sequence_id: torch.Tensor | None, pad_value: int | float
|
| 245 |
+
):
|
| 246 |
+
"""
|
| 247 |
+
Args:
|
| 248 |
+
tensor (Tensor): [B, L, ...]
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
Tensor: [B_unbinpacked, L_unbinpack, ...]
|
| 252 |
+
"""
|
| 253 |
+
if sequence_id is None:
|
| 254 |
+
return tensor
|
| 255 |
+
|
| 256 |
+
unpacked_tensors = []
|
| 257 |
+
num_sequences = sequence_id.max(dim=-1).values + 1
|
| 258 |
+
for batch_idx, (batch_seqid, batch_num_sequences) in enumerate(
|
| 259 |
+
zip(sequence_id, num_sequences)
|
| 260 |
+
):
|
| 261 |
+
for seqid in range(batch_num_sequences):
|
| 262 |
+
mask = batch_seqid == seqid
|
| 263 |
+
unpacked = tensor[batch_idx, mask]
|
| 264 |
+
unpacked_tensors.append(unpacked)
|
| 265 |
+
return stack_variable_length_tensors(unpacked_tensors, pad_value)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def fp32_autocast_context(device_type: str) -> ContextManager[Any]: # type: ignore
|
| 269 |
+
"""
|
| 270 |
+
Returns an autocast context manager that disables downcasting by AMP.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
device_type: The device type ('cpu' or 'cuda')
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
An autocast context manager with the specified behavior.
|
| 277 |
+
"""
|
| 278 |
+
if device_type == "cpu":
|
| 279 |
+
return torch.amp.autocast(device_type, enabled=False) # type: ignore
|
| 280 |
+
elif device_type == "mps":
|
| 281 |
+
# For MPS, just return a no-op context manager (nullcontext) since MPS does not support autocast.
|
| 282 |
+
return nullcontext()
|
| 283 |
+
elif device_type == "cuda":
|
| 284 |
+
return torch.amp.autocast(device_type, dtype=torch.float32) # type: ignore
|
| 285 |
+
else:
|
| 286 |
+
raise ValueError(f"Unsupported device type: {device_type}")
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def merge_ranges(ranges: list[range], merge_gap_max: int | None = None) -> list[range]:
|
| 290 |
+
"""Merge overlapping ranges into sorted, non-overlapping segments.
|
| 291 |
+
|
| 292 |
+
Args:
|
| 293 |
+
ranges: collection of ranges to merge.
|
| 294 |
+
merge_gap_max: optionally merge neighboring ranges that are separated by a gap
|
| 295 |
+
no larger than this size.
|
| 296 |
+
Returns:
|
| 297 |
+
non-overlapping ranges merged from the inputs, sorted by position.
|
| 298 |
+
"""
|
| 299 |
+
ranges = sorted(ranges, key=lambda r: r.start)
|
| 300 |
+
merge_gap_max = merge_gap_max if merge_gap_max is not None else 0
|
| 301 |
+
assert merge_gap_max >= 0, f"Invalid merge_gap_max: {merge_gap_max}"
|
| 302 |
+
|
| 303 |
+
merged = []
|
| 304 |
+
for r in ranges:
|
| 305 |
+
if not merged:
|
| 306 |
+
merged.append(r)
|
| 307 |
+
else:
|
| 308 |
+
last = merged[-1]
|
| 309 |
+
if last.stop + merge_gap_max >= r.start:
|
| 310 |
+
merged[-1] = range(last.start, max(last.stop, r.stop))
|
| 311 |
+
else:
|
| 312 |
+
merged.append(r)
|
| 313 |
+
return merged
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def merge_annotations(
|
| 317 |
+
annotations: list[FunctionAnnotation], merge_gap_max: int | None = None
|
| 318 |
+
) -> list[FunctionAnnotation]:
|
| 319 |
+
"""Merges annotations into non-overlapping segments.
|
| 320 |
+
|
| 321 |
+
Args:
|
| 322 |
+
annotations: annotations to merge.
|
| 323 |
+
merge_gap_max: optionally merge neighboring ranges that are separated by a gap
|
| 324 |
+
no larger than this size.
|
| 325 |
+
Returns:
|
| 326 |
+
non-overlapping annotations with gaps merged.
|
| 327 |
+
"""
|
| 328 |
+
grouped: dict[str, list[range]] = defaultdict(list)
|
| 329 |
+
for a in annotations:
|
| 330 |
+
# +1 since FunctionAnnotation.end is inlcusive.
|
| 331 |
+
grouped[a.label].append(range(a.start, a.end + 1))
|
| 332 |
+
|
| 333 |
+
merged = []
|
| 334 |
+
for label, ranges in grouped.items():
|
| 335 |
+
merged_ranges = merge_ranges(ranges, merge_gap_max=merge_gap_max)
|
| 336 |
+
for range_ in merged_ranges:
|
| 337 |
+
annotation = FunctionAnnotation(
|
| 338 |
+
label=label,
|
| 339 |
+
start=range_.start,
|
| 340 |
+
end=range_.stop - 1, # convert range.stop exclusive -> inclusive.
|
| 341 |
+
)
|
| 342 |
+
merged.append(annotation)
|
| 343 |
+
return merged
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def replace_inf(data):
|
| 347 |
+
if data is None:
|
| 348 |
+
return None
|
| 349 |
+
array = np.asarray(data, dtype=np.float32)
|
| 350 |
+
array = np.where(np.isinf(array), 1000, array)
|
| 351 |
+
return array.tolist()
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def maybe_tensor(x, convert_none_to_nan: bool = False) -> torch.Tensor | None:
|
| 355 |
+
if x is None:
|
| 356 |
+
return None
|
| 357 |
+
if isinstance(x, torch.Tensor):
|
| 358 |
+
return x
|
| 359 |
+
if isinstance(x, list) and all(isinstance(t, torch.Tensor) for t in x):
|
| 360 |
+
return torch.stack(x)
|
| 361 |
+
if convert_none_to_nan:
|
| 362 |
+
x = np.asarray(x, dtype=np.float32)
|
| 363 |
+
x = np.where(x is None, np.nan, x)
|
| 364 |
+
return torch.tensor(x)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def maybe_list(x, convert_nan_to_none: bool = False) -> list | None:
|
| 368 |
+
if x is None:
|
| 369 |
+
return None
|
| 370 |
+
if not convert_nan_to_none:
|
| 371 |
+
return x.tolist()
|
| 372 |
+
|
| 373 |
+
# Handle both torch.tensor and np.ndarray input.
|
| 374 |
+
if isinstance(x, torch.Tensor):
|
| 375 |
+
nan_mask = torch.isnan(x).cpu().numpy()
|
| 376 |
+
np_arr = x.cpu().numpy().astype(object)
|
| 377 |
+
elif isinstance(x, np.ndarray):
|
| 378 |
+
nan_mask = np.isnan(x)
|
| 379 |
+
np_arr = x.astype(object)
|
| 380 |
+
else:
|
| 381 |
+
raise TypeError("maybe_list can only work with torch.tensor or np.ndarray.")
|
| 382 |
+
|
| 383 |
+
np_arr[nan_mask] = None
|
| 384 |
+
return np_arr.tolist()
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def huggingfacehub_login():
|
| 388 |
+
"""Authenticates with the Hugging Face Hub using the HF_TOKEN environment
|
| 389 |
+
variable, else by prompting the user"""
|
| 390 |
+
token = os.environ.get("HF_TOKEN")
|
| 391 |
+
huggingface_hub.login(token=token)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def get_chainbreak_boundaries_from_sequence(sequence: Sequence[str]) -> np.ndarray:
|
| 395 |
+
chain_boundaries = [0]
|
| 396 |
+
for i, aa in enumerate(sequence):
|
| 397 |
+
if aa == CHAIN_BREAK_STR:
|
| 398 |
+
if i == (len(sequence) - 1):
|
| 399 |
+
raise ValueError(
|
| 400 |
+
"Encountered chain break token at end of sequence, this is unexpected."
|
| 401 |
+
)
|
| 402 |
+
if i == (len(sequence) - 2):
|
| 403 |
+
warn(
|
| 404 |
+
"Encountered chain break token at penultimate position, this is unexpected."
|
| 405 |
+
)
|
| 406 |
+
chain_boundaries.append(i)
|
| 407 |
+
chain_boundaries.append(i + 1)
|
| 408 |
+
chain_boundaries.append(len(sequence))
|
| 409 |
+
assert len(chain_boundaries) % 2 == 0
|
| 410 |
+
chain_boundaries = np.array(chain_boundaries).reshape(-1, 2)
|
| 411 |
+
return chain_boundaries
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def deserialize_tensors(b: bytes) -> Any:
|
| 415 |
+
buf = BytesIO(zstd.ZSTD_uncompress(b))
|
| 416 |
+
d = torch.load(buf, map_location="cpu", weights_only=False)
|
| 417 |
+
return d
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def join_lists(
|
| 421 |
+
lists: Sequence[Sequence[Any]], separator: Sequence[Any] | None = None
|
| 422 |
+
) -> list[Any]:
|
| 423 |
+
"""Joins multiple lists with separator element. Like str.join but for lists.
|
| 424 |
+
|
| 425 |
+
Example: [[1, 2], [3], [4]], separator=[0] -> [1, 2, 0, 3, 0, 4]
|
| 426 |
+
|
| 427 |
+
Args:
|
| 428 |
+
lists: Lists of elements to chain
|
| 429 |
+
separator: separators to intsert between chained output.
|
| 430 |
+
Returns:
|
| 431 |
+
Joined lists.
|
| 432 |
+
"""
|
| 433 |
+
if not lists:
|
| 434 |
+
return []
|
| 435 |
+
joined = []
|
| 436 |
+
joined.extend(lists[0])
|
| 437 |
+
for l in lists[1:]:
|
| 438 |
+
if separator:
|
| 439 |
+
joined.extend(separator)
|
| 440 |
+
joined.extend(l)
|
| 441 |
+
return joined
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def iterate_with_intermediate(
|
| 445 |
+
lists: Iterable, intermediate
|
| 446 |
+
) -> Generator[Any, None, None]:
|
| 447 |
+
"""
|
| 448 |
+
Iterate over the iterable, yielding the intermediate value between
|
| 449 |
+
every element of the intermediate. Useful for joining objects with
|
| 450 |
+
separator tokens.
|
| 451 |
+
"""
|
| 452 |
+
it = iter(lists)
|
| 453 |
+
yield next(it)
|
| 454 |
+
for l in it:
|
| 455 |
+
yield intermediate
|
| 456 |
+
yield l
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def concat_objects(objs: Sequence[Any], separator: Any | None = None):
|
| 460 |
+
"""
|
| 461 |
+
Concat objects with each other using a separator token.
|
| 462 |
+
|
| 463 |
+
Supports:
|
| 464 |
+
- Concatable (objects that implement `concat` classmethod)
|
| 465 |
+
- strings
|
| 466 |
+
- lists
|
| 467 |
+
- numpy arrays
|
| 468 |
+
- torch Tensors
|
| 469 |
+
|
| 470 |
+
Example:
|
| 471 |
+
>>> foo = "abc"
|
| 472 |
+
>>> bar = "def"
|
| 473 |
+
>>> concat_objects([foo, bar], "|")
|
| 474 |
+
"abc|def"
|
| 475 |
+
"""
|
| 476 |
+
match objs[0]:
|
| 477 |
+
case Concatable():
|
| 478 |
+
return objs[0].__class__.concat(objs) # type: ignore
|
| 479 |
+
case str():
|
| 480 |
+
assert isinstance(
|
| 481 |
+
separator, str
|
| 482 |
+
), "Trying to join strings but separator is not a string"
|
| 483 |
+
return separator.join(objs)
|
| 484 |
+
case list():
|
| 485 |
+
if separator is not None:
|
| 486 |
+
return join_lists(objs, [separator])
|
| 487 |
+
else:
|
| 488 |
+
return join_lists(objs)
|
| 489 |
+
case np.ndarray():
|
| 490 |
+
if separator is not None:
|
| 491 |
+
return np.concatenate(
|
| 492 |
+
list(iterate_with_intermediate(objs, np.array([separator])))
|
| 493 |
+
)
|
| 494 |
+
else:
|
| 495 |
+
return np.concatenate(objs)
|
| 496 |
+
case torch.Tensor():
|
| 497 |
+
if separator is not None:
|
| 498 |
+
return torch.cat(
|
| 499 |
+
list(iterate_with_intermediate(objs, torch.tensor([separator])))
|
| 500 |
+
)
|
| 501 |
+
else:
|
| 502 |
+
return torch.cat(objs) # type: ignore
|
| 503 |
+
case _:
|
| 504 |
+
raise TypeError(type(objs[0]))
|
esmfold2_mmcif_parsing.py
CHANGED
|
@@ -1,469 +1,469 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import functools
|
| 4 |
-
import io
|
| 5 |
-
import os
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
from datetime import datetime
|
| 8 |
-
from typing import Union
|
| 9 |
-
|
| 10 |
-
import biotite.structure as bs
|
| 11 |
-
import biotite.structure.io.pdbx as pdbx
|
| 12 |
-
|
| 13 |
-
from . import esmfold2_residue_constants as residue_constants
|
| 14 |
-
|
| 15 |
-
# Define PathOrBuffer for the opensource version
|
| 16 |
-
PathOrBuffer = Union[str, os.PathLike, io.StringIO]
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class NoProteinError(Exception):
|
| 20 |
-
pass
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
@dataclass
|
| 24 |
-
class Residue:
|
| 25 |
-
residue_number: int | None = None
|
| 26 |
-
insertion_code: str = ""
|
| 27 |
-
hetflag: bool = False
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
@dataclass
|
| 31 |
-
class MmcifHeader:
|
| 32 |
-
release_date: datetime | None = None
|
| 33 |
-
resolution: float | None = None
|
| 34 |
-
structure_method: str = "UNKNOWN"
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class MmcifWrapper:
|
| 38 |
-
def __init__(self, id: str | None = None):
|
| 39 |
-
self.id: str = id or ""
|
| 40 |
-
self.raw: pdbx.CIFFile | None = None
|
| 41 |
-
self.structure: bs.AtomArray
|
| 42 |
-
self.header: MmcifHeader = MmcifHeader()
|
| 43 |
-
self.entities: dict[int, list[str]] = {}
|
| 44 |
-
self.chain_to_seqres: dict[str, str] = {}
|
| 45 |
-
self.seqres_to_structure: dict[str, dict[int, Residue]] = {}
|
| 46 |
-
|
| 47 |
-
@classmethod
|
| 48 |
-
def read(cls, path: PathOrBuffer, id: str | None = None) -> MmcifWrapper:
|
| 49 |
-
obj = cls(id=id)
|
| 50 |
-
obj._load(path)
|
| 51 |
-
return obj
|
| 52 |
-
|
| 53 |
-
def _load(self, path: PathOrBuffer, fileid: str | None = None):
|
| 54 |
-
"""Load mmCIF data from file."""
|
| 55 |
-
self.raw = pdbx.CIFFile.read(path)
|
| 56 |
-
|
| 57 |
-
self._parse_structure()
|
| 58 |
-
self._parse_header()
|
| 59 |
-
self._parse_entities()
|
| 60 |
-
self._parse_sequences()
|
| 61 |
-
|
| 62 |
-
def _parse_structure(self):
|
| 63 |
-
"""Parse the atomic structure from mmCIF."""
|
| 64 |
-
try:
|
| 65 |
-
structure = pdbx.get_structure(self.raw, model=1)
|
| 66 |
-
if structure is None or not isinstance(structure, bs.AtomArray):
|
| 67 |
-
raise NoProteinError("No structure found in mmCIF file")
|
| 68 |
-
if len(structure) == 0:
|
| 69 |
-
raise NoProteinError("Empty structure in mmCIF file")
|
| 70 |
-
self.structure = structure
|
| 71 |
-
except Exception as e:
|
| 72 |
-
raise ValueError(f"Failed to parse structure: {e}")
|
| 73 |
-
|
| 74 |
-
def _parse_header(self):
|
| 75 |
-
"""Parse header information from mmCIF."""
|
| 76 |
-
if not self.raw:
|
| 77 |
-
return
|
| 78 |
-
|
| 79 |
-
try:
|
| 80 |
-
# Get the first (and usually only) block
|
| 81 |
-
block = self.raw.block
|
| 82 |
-
|
| 83 |
-
# Parse release date
|
| 84 |
-
if "pdbx_database_status" in block:
|
| 85 |
-
status_cat = block["pdbx_database_status"]
|
| 86 |
-
if "recvd_initial_deposition_date" in status_cat:
|
| 87 |
-
date_str = status_cat["recvd_initial_deposition_date"].as_item()
|
| 88 |
-
if date_str and date_str != "?":
|
| 89 |
-
try:
|
| 90 |
-
self.header.release_date = datetime.strptime(
|
| 91 |
-
date_str, "%Y-%m-%d"
|
| 92 |
-
)
|
| 93 |
-
except ValueError:
|
| 94 |
-
pass
|
| 95 |
-
|
| 96 |
-
# Parse resolution
|
| 97 |
-
if "refine" in block:
|
| 98 |
-
refine_cat = block["refine"]
|
| 99 |
-
if "ls_d_res_high" in refine_cat:
|
| 100 |
-
res_str = refine_cat["ls_d_res_high"].as_item()
|
| 101 |
-
if res_str and res_str != "?":
|
| 102 |
-
try:
|
| 103 |
-
self.header.resolution = float(res_str)
|
| 104 |
-
except ValueError:
|
| 105 |
-
pass
|
| 106 |
-
|
| 107 |
-
# Parse structure method
|
| 108 |
-
if "exptl" in block:
|
| 109 |
-
exptl_cat = block["exptl"]
|
| 110 |
-
if "method" in exptl_cat:
|
| 111 |
-
method = exptl_cat["method"].as_item()
|
| 112 |
-
if method and method != "?":
|
| 113 |
-
self.header.structure_method = method.upper()
|
| 114 |
-
|
| 115 |
-
except Exception:
|
| 116 |
-
# If parsing fails, keep default values
|
| 117 |
-
pass
|
| 118 |
-
|
| 119 |
-
def _parse_entities(self):
|
| 120 |
-
"""Parse entity information and map to chains."""
|
| 121 |
-
if not self.raw:
|
| 122 |
-
return
|
| 123 |
-
|
| 124 |
-
try:
|
| 125 |
-
block = self.raw.block
|
| 126 |
-
|
| 127 |
-
# Parse entity information
|
| 128 |
-
if "entity" in block:
|
| 129 |
-
entity_cat = block["entity"]
|
| 130 |
-
entity_ids = entity_cat["id"].as_array(str)
|
| 131 |
-
entity_types = entity_cat["type"].as_array(str)
|
| 132 |
-
|
| 133 |
-
# Initialize entities dict with all entities (not just polymers)
|
| 134 |
-
for i, (entity_id, entity_type) in enumerate(
|
| 135 |
-
zip(entity_ids, entity_types)
|
| 136 |
-
):
|
| 137 |
-
self.entities[int(entity_id)] = []
|
| 138 |
-
|
| 139 |
-
# Map polymer chains to entities using entity_poly
|
| 140 |
-
if "entity_poly" in block:
|
| 141 |
-
poly_cat = block["entity_poly"]
|
| 142 |
-
entity_ids = poly_cat["entity_id"].as_array(str)
|
| 143 |
-
chain_lists = poly_cat["pdbx_strand_id"].as_array(str)
|
| 144 |
-
|
| 145 |
-
for entity_id, chain_list in zip(entity_ids, chain_lists):
|
| 146 |
-
entity_id = int(entity_id)
|
| 147 |
-
# Chain list is comma-separated
|
| 148 |
-
chains = [c.strip() for c in chain_list.split(",") if c.strip()]
|
| 149 |
-
if entity_id in self.entities:
|
| 150 |
-
self.entities[entity_id] = chains
|
| 151 |
-
|
| 152 |
-
# Map non-polymer chains using struct_asym for entities not covered by entity_poly
|
| 153 |
-
if "struct_asym" in block:
|
| 154 |
-
asym_cat = block["struct_asym"]
|
| 155 |
-
asym_ids = asym_cat["id"].as_array(str)
|
| 156 |
-
entity_ids = asym_cat["entity_id"].as_array(str)
|
| 157 |
-
|
| 158 |
-
for asym_id, entity_id in zip(asym_ids, entity_ids):
|
| 159 |
-
entity_id = int(entity_id)
|
| 160 |
-
# Only add if entity exists but has no chains yet (non-polymer entities)
|
| 161 |
-
if entity_id in self.entities and not self.entities[entity_id]:
|
| 162 |
-
self.entities[entity_id].append(asym_id)
|
| 163 |
-
|
| 164 |
-
except Exception:
|
| 165 |
-
# If parsing fails, try to infer from structure
|
| 166 |
-
if (
|
| 167 |
-
self.structure
|
| 168 |
-
and hasattr(self.structure, "chain_id")
|
| 169 |
-
and self.structure.chain_id is not None
|
| 170 |
-
and hasattr(self.structure.chain_id, "__iter__")
|
| 171 |
-
):
|
| 172 |
-
chain_ids = list(set(self.structure.chain_id))
|
| 173 |
-
self.entities = {1: chain_ids}
|
| 174 |
-
|
| 175 |
-
def _parse_sequences(self):
|
| 176 |
-
"""Parse sequence information from mmCIF."""
|
| 177 |
-
if not self.raw:
|
| 178 |
-
return
|
| 179 |
-
|
| 180 |
-
block = self.raw.block
|
| 181 |
-
|
| 182 |
-
# Parse polymer sequences
|
| 183 |
-
if "entity_poly" in block:
|
| 184 |
-
poly_cat = block["entity_poly"]
|
| 185 |
-
entity_ids = poly_cat["entity_id"].as_array(str)
|
| 186 |
-
sequences = poly_cat["pdbx_seq_one_letter_code_can"].as_array(str)
|
| 187 |
-
chain_lists = poly_cat["pdbx_strand_id"].as_array(str)
|
| 188 |
-
|
| 189 |
-
for entity_id, sequence, chain_list in zip(
|
| 190 |
-
entity_ids, sequences, chain_lists
|
| 191 |
-
):
|
| 192 |
-
# Clean up sequence (remove whitespace and newlines)
|
| 193 |
-
clean_seq = "".join(sequence.split())
|
| 194 |
-
chains = [c.strip() for c in chain_list.split(",") if c.strip()]
|
| 195 |
-
|
| 196 |
-
for chain_id in chains:
|
| 197 |
-
self.chain_to_seqres[chain_id] = clean_seq
|
| 198 |
-
|
| 199 |
-
# Parse sequence to structure mapping
|
| 200 |
-
if "pdbx_poly_seq_scheme" in block:
|
| 201 |
-
seq_cat = block["pdbx_poly_seq_scheme"]
|
| 202 |
-
asym_ids = seq_cat["asym_id"].as_array(str) # Internal chain IDs
|
| 203 |
-
seq_positions = seq_cat["seq_id"].as_array(str)
|
| 204 |
-
auth_seq_nums = seq_cat["auth_seq_num"].as_array(str)
|
| 205 |
-
ins_codes = (
|
| 206 |
-
seq_cat["pdb_ins_code"].as_array(str)
|
| 207 |
-
if "pdb_ins_code" in seq_cat
|
| 208 |
-
else [""] * len(asym_ids)
|
| 209 |
-
)
|
| 210 |
-
hetflags = (
|
| 211 |
-
seq_cat["hetflag"].as_array(str)
|
| 212 |
-
if "hetflag" in seq_cat
|
| 213 |
-
else ["N"] * len(asym_ids)
|
| 214 |
-
)
|
| 215 |
-
|
| 216 |
-
# Get author chain IDs if available
|
| 217 |
-
auth_chain_ids = (
|
| 218 |
-
seq_cat["pdb_strand_id"].as_array(str)
|
| 219 |
-
if "pdb_strand_id" in seq_cat
|
| 220 |
-
else asym_ids # Fallback to internal IDs
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
# Build mapping from internal chain ID to author chain ID
|
| 224 |
-
asym_to_auth_mapping = {}
|
| 225 |
-
for asym_id, auth_id in zip(asym_ids, auth_chain_ids):
|
| 226 |
-
asym_to_auth_mapping[asym_id] = auth_id
|
| 227 |
-
|
| 228 |
-
# Group by internal chain ID first, then map to author chain ID
|
| 229 |
-
chain_data = {}
|
| 230 |
-
for asym_id, seq_pos, auth_seq, ins_code, hetflag in zip(
|
| 231 |
-
asym_ids, seq_positions, auth_seq_nums, ins_codes, hetflags
|
| 232 |
-
):
|
| 233 |
-
if asym_id not in chain_data:
|
| 234 |
-
chain_data[asym_id] = {}
|
| 235 |
-
|
| 236 |
-
try:
|
| 237 |
-
seq_index = int(seq_pos) - 1 # Convert to 0-based indexing
|
| 238 |
-
res_num = int(auth_seq) if auth_seq != "?" else None
|
| 239 |
-
except ValueError:
|
| 240 |
-
continue
|
| 241 |
-
|
| 242 |
-
if res_num is not None:
|
| 243 |
-
# Convert mmCIF "." and "?" to empty string
|
| 244 |
-
clean_ins_code = "" if ins_code in [".", "?"] else ins_code
|
| 245 |
-
else:
|
| 246 |
-
clean_ins_code = ""
|
| 247 |
-
res_num = None
|
| 248 |
-
|
| 249 |
-
is_het = hetflag.upper() == "Y" # type: ignore
|
| 250 |
-
chain_data[asym_id][seq_index] = Residue(
|
| 251 |
-
residue_number=res_num,
|
| 252 |
-
insertion_code=clean_ins_code, # type: ignore
|
| 253 |
-
hetflag=is_het,
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
# Handle cases where multiple residues have the same auth_seq_num
|
| 257 |
-
# by adjusting residue numbers to be unique within each chain
|
| 258 |
-
for asym_id, residue_data in chain_data.items():
|
| 259 |
-
# Check if there are duplicate residue numbers in this chain
|
| 260 |
-
positions_with_same_num = {}
|
| 261 |
-
for seq_idx, res_at_pos in residue_data.items():
|
| 262 |
-
if res_at_pos.residue_number is not None:
|
| 263 |
-
res_num = res_at_pos.residue_number
|
| 264 |
-
if res_num not in positions_with_same_num:
|
| 265 |
-
positions_with_same_num[res_num] = []
|
| 266 |
-
positions_with_same_num[res_num].append(seq_idx)
|
| 267 |
-
|
| 268 |
-
# Fix duplicate residue numbers by making them sequential
|
| 269 |
-
for res_num, seq_indices in positions_with_same_num.items():
|
| 270 |
-
if len(seq_indices) > 1:
|
| 271 |
-
# Multiple residues have the same residue number
|
| 272 |
-
# Make them sequential starting from the original number
|
| 273 |
-
seq_indices.sort() # Ensure consistent ordering
|
| 274 |
-
for i, seq_idx in enumerate(seq_indices):
|
| 275 |
-
original_pos = residue_data[seq_idx]
|
| 276 |
-
new_pos = Residue(
|
| 277 |
-
residue_number=res_num + i,
|
| 278 |
-
insertion_code=original_pos.insertion_code,
|
| 279 |
-
hetflag=original_pos.hetflag,
|
| 280 |
-
)
|
| 281 |
-
residue_data[seq_idx] = new_pos
|
| 282 |
-
|
| 283 |
-
# Create ordered mappings using author chain IDs
|
| 284 |
-
for asym_id in chain_data:
|
| 285 |
-
auth_chain_id = asym_to_auth_mapping.get(asym_id, asym_id)
|
| 286 |
-
if auth_chain_id in self.chain_to_seqres:
|
| 287 |
-
seq_len = len(self.chain_to_seqres[auth_chain_id])
|
| 288 |
-
ordered_mapping = {}
|
| 289 |
-
|
| 290 |
-
for i in range(seq_len):
|
| 291 |
-
if i in chain_data[asym_id]:
|
| 292 |
-
ordered_mapping[i] = chain_data[asym_id][i]
|
| 293 |
-
else:
|
| 294 |
-
# Missing residue - no structure coordinates
|
| 295 |
-
ordered_mapping[i] = Residue(
|
| 296 |
-
residue_number=None, insertion_code="", hetflag=False
|
| 297 |
-
)
|
| 298 |
-
|
| 299 |
-
self.seqres_to_structure[auth_chain_id] = ordered_mapping
|
| 300 |
-
else:
|
| 301 |
-
# Handle case where auth_chain_id is not in chain_to_seqres
|
| 302 |
-
# This can happen if the chain is not a polymer or if there's a parsing issue
|
| 303 |
-
# Create a basic mapping based on the chain_data
|
| 304 |
-
if chain_data[asym_id]:
|
| 305 |
-
# Sort by sequence index to create ordered mapping
|
| 306 |
-
sorted_indices = sorted(chain_data[asym_id].keys())
|
| 307 |
-
ordered_mapping = {}
|
| 308 |
-
for i, seq_idx in enumerate(sorted_indices):
|
| 309 |
-
ordered_mapping[i] = chain_data[asym_id][seq_idx]
|
| 310 |
-
self.seqres_to_structure[auth_chain_id] = ordered_mapping
|
| 311 |
-
|
| 312 |
-
# Ensure all chains have complete mappings
|
| 313 |
-
for chain_id in self.chain_to_seqres:
|
| 314 |
-
if chain_id not in self.seqres_to_structure:
|
| 315 |
-
seq_len = len(self.chain_to_seqres[chain_id])
|
| 316 |
-
self.seqres_to_structure[chain_id] = {
|
| 317 |
-
i: Residue(residue_number=None, insertion_code="", hetflag=False)
|
| 318 |
-
for i in range(seq_len)
|
| 319 |
-
}
|
| 320 |
-
else:
|
| 321 |
-
# Fill in any missing indices
|
| 322 |
-
seq_len = len(self.chain_to_seqres[chain_id])
|
| 323 |
-
mapping = self.seqres_to_structure[chain_id]
|
| 324 |
-
for i in range(seq_len):
|
| 325 |
-
if i not in mapping:
|
| 326 |
-
mapping[i] = Residue(
|
| 327 |
-
residue_number=None, insertion_code="", hetflag=False
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
# Fallback: create basic mappings from structure for missing chains
|
| 331 |
-
if (
|
| 332 |
-
self.structure
|
| 333 |
-
and hasattr(self.structure, "chain_id")
|
| 334 |
-
and self.structure.chain_id is not None
|
| 335 |
-
and hasattr(self.structure.chain_id, "__iter__")
|
| 336 |
-
):
|
| 337 |
-
for chain_id in set(self.structure.chain_id):
|
| 338 |
-
if chain_id not in self.seqres_to_structure:
|
| 339 |
-
chain_structure = self.structure[
|
| 340 |
-
self.structure.chain_id == chain_id
|
| 341 |
-
]
|
| 342 |
-
if (
|
| 343 |
-
hasattr(chain_structure, "res_id")
|
| 344 |
-
and chain_structure.res_id is not None
|
| 345 |
-
and hasattr(chain_structure.res_id, "__iter__")
|
| 346 |
-
):
|
| 347 |
-
residue_ids = list(set(chain_structure.res_id))
|
| 348 |
-
residue_ids.sort()
|
| 349 |
-
|
| 350 |
-
self.seqres_to_structure[chain_id] = {
|
| 351 |
-
i: Residue(
|
| 352 |
-
residue_number=res_id, insertion_code="", hetflag=False
|
| 353 |
-
)
|
| 354 |
-
for i, res_id in enumerate(residue_ids)
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
def _parse_nonpoly_from_mmcif(self) -> dict[tuple, bs.AtomArray]:
|
| 358 |
-
"""Parse non-polymer coordinates from mmCIF block data."""
|
| 359 |
-
nonpoly_coords = {}
|
| 360 |
-
|
| 361 |
-
# Get non-polymer entities from the mmCIF block
|
| 362 |
-
assert self.raw is not None
|
| 363 |
-
block = self.raw.block
|
| 364 |
-
nonpoly_entities = set()
|
| 365 |
-
|
| 366 |
-
# Find non-polymer entities
|
| 367 |
-
if "entity" in block:
|
| 368 |
-
entity_cat = block["entity"]
|
| 369 |
-
entity_ids = entity_cat["id"].as_array(str)
|
| 370 |
-
entity_types = entity_cat["type"].as_array(str)
|
| 371 |
-
|
| 372 |
-
for entity_id, entity_type in zip(entity_ids, entity_types):
|
| 373 |
-
if entity_type.upper() in ["NON-POLYMER", "WATER", "BRANCHED"]:
|
| 374 |
-
nonpoly_entities.add(entity_id)
|
| 375 |
-
|
| 376 |
-
# Map entities to chains for non-polymers
|
| 377 |
-
entity_to_chains = {}
|
| 378 |
-
if "pdbx_entity_nonpoly" in block:
|
| 379 |
-
nonpoly_cat = block["pdbx_entity_nonpoly"]
|
| 380 |
-
entity_ids = nonpoly_cat["entity_id"].as_array(str)
|
| 381 |
-
comp_ids = nonpoly_cat["comp_id"].as_array(str)
|
| 382 |
-
|
| 383 |
-
for entity_id, comp_id in zip(entity_ids, comp_ids):
|
| 384 |
-
if entity_id in nonpoly_entities:
|
| 385 |
-
entity_to_chains[entity_id] = comp_id
|
| 386 |
-
|
| 387 |
-
# Get atom site information for non-polymers
|
| 388 |
-
if "atom_site" in block:
|
| 389 |
-
atom_cat = block["atom_site"]
|
| 390 |
-
atom_chain_ids = atom_cat["label_asym_id"].as_array(str)
|
| 391 |
-
atom_entity_ids = atom_cat["label_entity_id"].as_array(str)
|
| 392 |
-
atom_comp_ids = atom_cat["label_comp_id"].as_array(str)
|
| 393 |
-
|
| 394 |
-
# Group non-polymer atoms by entity and chain
|
| 395 |
-
nonpoly_atom_groups = {}
|
| 396 |
-
for i, (chain_id, entity_id, comp_id) in enumerate(
|
| 397 |
-
zip(atom_chain_ids, atom_entity_ids, atom_comp_ids)
|
| 398 |
-
):
|
| 399 |
-
if entity_id in nonpoly_entities:
|
| 400 |
-
key = (comp_id, chain_id)
|
| 401 |
-
if key not in nonpoly_atom_groups:
|
| 402 |
-
nonpoly_atom_groups[key] = []
|
| 403 |
-
nonpoly_atom_groups[key].append(i)
|
| 404 |
-
|
| 405 |
-
# Extract coordinates for each non-polymer group
|
| 406 |
-
for (comp_id, chain_id), atom_indices in nonpoly_atom_groups.items():
|
| 407 |
-
# Match atoms by comparing chain_id and residue name
|
| 408 |
-
structure_mask = (self.structure.chain_id == chain_id) & (
|
| 409 |
-
self.structure.res_name == comp_id
|
| 410 |
-
)
|
| 411 |
-
|
| 412 |
-
if structure_mask.any():
|
| 413 |
-
nonpoly_array = self.structure[structure_mask]
|
| 414 |
-
if (
|
| 415 |
-
isinstance(nonpoly_array, (bs.AtomArray, bs.AtomArrayStack))
|
| 416 |
-
and len(nonpoly_array) > 0
|
| 417 |
-
):
|
| 418 |
-
nonpoly_coords[(comp_id, chain_id)] = nonpoly_array
|
| 419 |
-
|
| 420 |
-
return nonpoly_coords
|
| 421 |
-
|
| 422 |
-
def _parse_nonpoly_fallback(self) -> dict[tuple, bs.AtomArray]:
|
| 423 |
-
"""Fallback method to extract heteroatoms directly from structure."""
|
| 424 |
-
nonpoly_coords = {}
|
| 425 |
-
|
| 426 |
-
if not (self.structure and hasattr(self.structure, "chain_id")):
|
| 427 |
-
return nonpoly_coords
|
| 428 |
-
|
| 429 |
-
# Create set of standard residues from residue_constants
|
| 430 |
-
standard_residues = set(residue_constants.resnames[:-1]) # Exclude 'UNK'
|
| 431 |
-
standard_residues.update({"A", "C", "G", "T", "U"}) # Add nucleic acids
|
| 432 |
-
|
| 433 |
-
if hasattr(self.structure, "chain_id") and self.structure.chain_id is not None:
|
| 434 |
-
for chain_id in set(self.structure.chain_id):
|
| 435 |
-
chain_structure = self.structure[self.structure.chain_id == chain_id]
|
| 436 |
-
|
| 437 |
-
# Find non-standard residues
|
| 438 |
-
if (
|
| 439 |
-
hasattr(chain_structure, "res_name")
|
| 440 |
-
and chain_structure.res_name is not None
|
| 441 |
-
and hasattr(chain_structure.res_name, "__iter__")
|
| 442 |
-
):
|
| 443 |
-
for res_name in set(chain_structure.res_name):
|
| 444 |
-
if res_name not in standard_residues:
|
| 445 |
-
res_mask = (chain_structure.chain_id == chain_id) & (
|
| 446 |
-
chain_structure.res_name == res_name
|
| 447 |
-
)
|
| 448 |
-
if res_mask.any() and isinstance(
|
| 449 |
-
chain_structure, (bs.AtomArray, bs.AtomArrayStack)
|
| 450 |
-
):
|
| 451 |
-
nonpoly_array = chain_structure[res_mask]
|
| 452 |
-
nonpoly_coords[(res_name, chain_id)] = nonpoly_array
|
| 453 |
-
|
| 454 |
-
return nonpoly_coords
|
| 455 |
-
|
| 456 |
-
@functools.cached_property
|
| 457 |
-
def non_polymer_coords(self) -> dict[tuple, bs.AtomArray]:
|
| 458 |
-
"""
|
| 459 |
-
Extract non-polymer coordinates (ligands, cofactors, etc.) from mmCIF structure.
|
| 460 |
-
|
| 461 |
-
Returns a dictionary mapping (nonpolymer_info, chain_id) tuples to AtomArrays.
|
| 462 |
-
"""
|
| 463 |
-
if not self.structure or not self.raw:
|
| 464 |
-
return {}
|
| 465 |
-
|
| 466 |
-
try:
|
| 467 |
-
return self._parse_nonpoly_from_mmcif()
|
| 468 |
-
except Exception:
|
| 469 |
-
return self._parse_nonpoly_fallback()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import io
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from typing import Union
|
| 9 |
+
|
| 10 |
+
import biotite.structure as bs
|
| 11 |
+
import biotite.structure.io.pdbx as pdbx
|
| 12 |
+
|
| 13 |
+
from . import esmfold2_residue_constants as residue_constants
|
| 14 |
+
|
| 15 |
+
# Define PathOrBuffer for the opensource version
|
| 16 |
+
PathOrBuffer = Union[str, os.PathLike, io.StringIO]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class NoProteinError(Exception):
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class Residue:
|
| 25 |
+
residue_number: int | None = None
|
| 26 |
+
insertion_code: str = ""
|
| 27 |
+
hetflag: bool = False
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class MmcifHeader:
|
| 32 |
+
release_date: datetime | None = None
|
| 33 |
+
resolution: float | None = None
|
| 34 |
+
structure_method: str = "UNKNOWN"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class MmcifWrapper:
|
| 38 |
+
def __init__(self, id: str | None = None):
|
| 39 |
+
self.id: str = id or ""
|
| 40 |
+
self.raw: pdbx.CIFFile | None = None
|
| 41 |
+
self.structure: bs.AtomArray
|
| 42 |
+
self.header: MmcifHeader = MmcifHeader()
|
| 43 |
+
self.entities: dict[int, list[str]] = {}
|
| 44 |
+
self.chain_to_seqres: dict[str, str] = {}
|
| 45 |
+
self.seqres_to_structure: dict[str, dict[int, Residue]] = {}
|
| 46 |
+
|
| 47 |
+
@classmethod
|
| 48 |
+
def read(cls, path: PathOrBuffer, id: str | None = None) -> MmcifWrapper:
|
| 49 |
+
obj = cls(id=id)
|
| 50 |
+
obj._load(path)
|
| 51 |
+
return obj
|
| 52 |
+
|
| 53 |
+
def _load(self, path: PathOrBuffer, fileid: str | None = None):
|
| 54 |
+
"""Load mmCIF data from file."""
|
| 55 |
+
self.raw = pdbx.CIFFile.read(path)
|
| 56 |
+
|
| 57 |
+
self._parse_structure()
|
| 58 |
+
self._parse_header()
|
| 59 |
+
self._parse_entities()
|
| 60 |
+
self._parse_sequences()
|
| 61 |
+
|
| 62 |
+
def _parse_structure(self):
|
| 63 |
+
"""Parse the atomic structure from mmCIF."""
|
| 64 |
+
try:
|
| 65 |
+
structure = pdbx.get_structure(self.raw, model=1)
|
| 66 |
+
if structure is None or not isinstance(structure, bs.AtomArray):
|
| 67 |
+
raise NoProteinError("No structure found in mmCIF file")
|
| 68 |
+
if len(structure) == 0:
|
| 69 |
+
raise NoProteinError("Empty structure in mmCIF file")
|
| 70 |
+
self.structure = structure
|
| 71 |
+
except Exception as e:
|
| 72 |
+
raise ValueError(f"Failed to parse structure: {e}")
|
| 73 |
+
|
| 74 |
+
def _parse_header(self):
|
| 75 |
+
"""Parse header information from mmCIF."""
|
| 76 |
+
if not self.raw:
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
# Get the first (and usually only) block
|
| 81 |
+
block = self.raw.block
|
| 82 |
+
|
| 83 |
+
# Parse release date
|
| 84 |
+
if "pdbx_database_status" in block:
|
| 85 |
+
status_cat = block["pdbx_database_status"]
|
| 86 |
+
if "recvd_initial_deposition_date" in status_cat:
|
| 87 |
+
date_str = status_cat["recvd_initial_deposition_date"].as_item()
|
| 88 |
+
if date_str and date_str != "?":
|
| 89 |
+
try:
|
| 90 |
+
self.header.release_date = datetime.strptime(
|
| 91 |
+
date_str, "%Y-%m-%d"
|
| 92 |
+
)
|
| 93 |
+
except ValueError:
|
| 94 |
+
pass
|
| 95 |
+
|
| 96 |
+
# Parse resolution
|
| 97 |
+
if "refine" in block:
|
| 98 |
+
refine_cat = block["refine"]
|
| 99 |
+
if "ls_d_res_high" in refine_cat:
|
| 100 |
+
res_str = refine_cat["ls_d_res_high"].as_item()
|
| 101 |
+
if res_str and res_str != "?":
|
| 102 |
+
try:
|
| 103 |
+
self.header.resolution = float(res_str)
|
| 104 |
+
except ValueError:
|
| 105 |
+
pass
|
| 106 |
+
|
| 107 |
+
# Parse structure method
|
| 108 |
+
if "exptl" in block:
|
| 109 |
+
exptl_cat = block["exptl"]
|
| 110 |
+
if "method" in exptl_cat:
|
| 111 |
+
method = exptl_cat["method"].as_item()
|
| 112 |
+
if method and method != "?":
|
| 113 |
+
self.header.structure_method = method.upper()
|
| 114 |
+
|
| 115 |
+
except Exception:
|
| 116 |
+
# If parsing fails, keep default values
|
| 117 |
+
pass
|
| 118 |
+
|
| 119 |
+
def _parse_entities(self):
|
| 120 |
+
"""Parse entity information and map to chains."""
|
| 121 |
+
if not self.raw:
|
| 122 |
+
return
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
block = self.raw.block
|
| 126 |
+
|
| 127 |
+
# Parse entity information
|
| 128 |
+
if "entity" in block:
|
| 129 |
+
entity_cat = block["entity"]
|
| 130 |
+
entity_ids = entity_cat["id"].as_array(str)
|
| 131 |
+
entity_types = entity_cat["type"].as_array(str)
|
| 132 |
+
|
| 133 |
+
# Initialize entities dict with all entities (not just polymers)
|
| 134 |
+
for i, (entity_id, entity_type) in enumerate(
|
| 135 |
+
zip(entity_ids, entity_types)
|
| 136 |
+
):
|
| 137 |
+
self.entities[int(entity_id)] = []
|
| 138 |
+
|
| 139 |
+
# Map polymer chains to entities using entity_poly
|
| 140 |
+
if "entity_poly" in block:
|
| 141 |
+
poly_cat = block["entity_poly"]
|
| 142 |
+
entity_ids = poly_cat["entity_id"].as_array(str)
|
| 143 |
+
chain_lists = poly_cat["pdbx_strand_id"].as_array(str)
|
| 144 |
+
|
| 145 |
+
for entity_id, chain_list in zip(entity_ids, chain_lists):
|
| 146 |
+
entity_id = int(entity_id)
|
| 147 |
+
# Chain list is comma-separated
|
| 148 |
+
chains = [c.strip() for c in chain_list.split(",") if c.strip()]
|
| 149 |
+
if entity_id in self.entities:
|
| 150 |
+
self.entities[entity_id] = chains
|
| 151 |
+
|
| 152 |
+
# Map non-polymer chains using struct_asym for entities not covered by entity_poly
|
| 153 |
+
if "struct_asym" in block:
|
| 154 |
+
asym_cat = block["struct_asym"]
|
| 155 |
+
asym_ids = asym_cat["id"].as_array(str)
|
| 156 |
+
entity_ids = asym_cat["entity_id"].as_array(str)
|
| 157 |
+
|
| 158 |
+
for asym_id, entity_id in zip(asym_ids, entity_ids):
|
| 159 |
+
entity_id = int(entity_id)
|
| 160 |
+
# Only add if entity exists but has no chains yet (non-polymer entities)
|
| 161 |
+
if entity_id in self.entities and not self.entities[entity_id]:
|
| 162 |
+
self.entities[entity_id].append(asym_id)
|
| 163 |
+
|
| 164 |
+
except Exception:
|
| 165 |
+
# If parsing fails, try to infer from structure
|
| 166 |
+
if (
|
| 167 |
+
self.structure
|
| 168 |
+
and hasattr(self.structure, "chain_id")
|
| 169 |
+
and self.structure.chain_id is not None
|
| 170 |
+
and hasattr(self.structure.chain_id, "__iter__")
|
| 171 |
+
):
|
| 172 |
+
chain_ids = list(set(self.structure.chain_id))
|
| 173 |
+
self.entities = {1: chain_ids}
|
| 174 |
+
|
| 175 |
+
def _parse_sequences(self):
|
| 176 |
+
"""Parse sequence information from mmCIF."""
|
| 177 |
+
if not self.raw:
|
| 178 |
+
return
|
| 179 |
+
|
| 180 |
+
block = self.raw.block
|
| 181 |
+
|
| 182 |
+
# Parse polymer sequences
|
| 183 |
+
if "entity_poly" in block:
|
| 184 |
+
poly_cat = block["entity_poly"]
|
| 185 |
+
entity_ids = poly_cat["entity_id"].as_array(str)
|
| 186 |
+
sequences = poly_cat["pdbx_seq_one_letter_code_can"].as_array(str)
|
| 187 |
+
chain_lists = poly_cat["pdbx_strand_id"].as_array(str)
|
| 188 |
+
|
| 189 |
+
for entity_id, sequence, chain_list in zip(
|
| 190 |
+
entity_ids, sequences, chain_lists
|
| 191 |
+
):
|
| 192 |
+
# Clean up sequence (remove whitespace and newlines)
|
| 193 |
+
clean_seq = "".join(sequence.split())
|
| 194 |
+
chains = [c.strip() for c in chain_list.split(",") if c.strip()]
|
| 195 |
+
|
| 196 |
+
for chain_id in chains:
|
| 197 |
+
self.chain_to_seqres[chain_id] = clean_seq
|
| 198 |
+
|
| 199 |
+
# Parse sequence to structure mapping
|
| 200 |
+
if "pdbx_poly_seq_scheme" in block:
|
| 201 |
+
seq_cat = block["pdbx_poly_seq_scheme"]
|
| 202 |
+
asym_ids = seq_cat["asym_id"].as_array(str) # Internal chain IDs
|
| 203 |
+
seq_positions = seq_cat["seq_id"].as_array(str)
|
| 204 |
+
auth_seq_nums = seq_cat["auth_seq_num"].as_array(str)
|
| 205 |
+
ins_codes = (
|
| 206 |
+
seq_cat["pdb_ins_code"].as_array(str)
|
| 207 |
+
if "pdb_ins_code" in seq_cat
|
| 208 |
+
else [""] * len(asym_ids)
|
| 209 |
+
)
|
| 210 |
+
hetflags = (
|
| 211 |
+
seq_cat["hetflag"].as_array(str)
|
| 212 |
+
if "hetflag" in seq_cat
|
| 213 |
+
else ["N"] * len(asym_ids)
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Get author chain IDs if available
|
| 217 |
+
auth_chain_ids = (
|
| 218 |
+
seq_cat["pdb_strand_id"].as_array(str)
|
| 219 |
+
if "pdb_strand_id" in seq_cat
|
| 220 |
+
else asym_ids # Fallback to internal IDs
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Build mapping from internal chain ID to author chain ID
|
| 224 |
+
asym_to_auth_mapping = {}
|
| 225 |
+
for asym_id, auth_id in zip(asym_ids, auth_chain_ids):
|
| 226 |
+
asym_to_auth_mapping[asym_id] = auth_id
|
| 227 |
+
|
| 228 |
+
# Group by internal chain ID first, then map to author chain ID
|
| 229 |
+
chain_data = {}
|
| 230 |
+
for asym_id, seq_pos, auth_seq, ins_code, hetflag in zip(
|
| 231 |
+
asym_ids, seq_positions, auth_seq_nums, ins_codes, hetflags
|
| 232 |
+
):
|
| 233 |
+
if asym_id not in chain_data:
|
| 234 |
+
chain_data[asym_id] = {}
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
seq_index = int(seq_pos) - 1 # Convert to 0-based indexing
|
| 238 |
+
res_num = int(auth_seq) if auth_seq != "?" else None
|
| 239 |
+
except ValueError:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
if res_num is not None:
|
| 243 |
+
# Convert mmCIF "." and "?" to empty string
|
| 244 |
+
clean_ins_code = "" if ins_code in [".", "?"] else ins_code
|
| 245 |
+
else:
|
| 246 |
+
clean_ins_code = ""
|
| 247 |
+
res_num = None
|
| 248 |
+
|
| 249 |
+
is_het = hetflag.upper() == "Y" # type: ignore
|
| 250 |
+
chain_data[asym_id][seq_index] = Residue(
|
| 251 |
+
residue_number=res_num,
|
| 252 |
+
insertion_code=clean_ins_code, # type: ignore
|
| 253 |
+
hetflag=is_het,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Handle cases where multiple residues have the same auth_seq_num
|
| 257 |
+
# by adjusting residue numbers to be unique within each chain
|
| 258 |
+
for asym_id, residue_data in chain_data.items():
|
| 259 |
+
# Check if there are duplicate residue numbers in this chain
|
| 260 |
+
positions_with_same_num = {}
|
| 261 |
+
for seq_idx, res_at_pos in residue_data.items():
|
| 262 |
+
if res_at_pos.residue_number is not None:
|
| 263 |
+
res_num = res_at_pos.residue_number
|
| 264 |
+
if res_num not in positions_with_same_num:
|
| 265 |
+
positions_with_same_num[res_num] = []
|
| 266 |
+
positions_with_same_num[res_num].append(seq_idx)
|
| 267 |
+
|
| 268 |
+
# Fix duplicate residue numbers by making them sequential
|
| 269 |
+
for res_num, seq_indices in positions_with_same_num.items():
|
| 270 |
+
if len(seq_indices) > 1:
|
| 271 |
+
# Multiple residues have the same residue number
|
| 272 |
+
# Make them sequential starting from the original number
|
| 273 |
+
seq_indices.sort() # Ensure consistent ordering
|
| 274 |
+
for i, seq_idx in enumerate(seq_indices):
|
| 275 |
+
original_pos = residue_data[seq_idx]
|
| 276 |
+
new_pos = Residue(
|
| 277 |
+
residue_number=res_num + i,
|
| 278 |
+
insertion_code=original_pos.insertion_code,
|
| 279 |
+
hetflag=original_pos.hetflag,
|
| 280 |
+
)
|
| 281 |
+
residue_data[seq_idx] = new_pos
|
| 282 |
+
|
| 283 |
+
# Create ordered mappings using author chain IDs
|
| 284 |
+
for asym_id in chain_data:
|
| 285 |
+
auth_chain_id = asym_to_auth_mapping.get(asym_id, asym_id)
|
| 286 |
+
if auth_chain_id in self.chain_to_seqres:
|
| 287 |
+
seq_len = len(self.chain_to_seqres[auth_chain_id])
|
| 288 |
+
ordered_mapping = {}
|
| 289 |
+
|
| 290 |
+
for i in range(seq_len):
|
| 291 |
+
if i in chain_data[asym_id]:
|
| 292 |
+
ordered_mapping[i] = chain_data[asym_id][i]
|
| 293 |
+
else:
|
| 294 |
+
# Missing residue - no structure coordinates
|
| 295 |
+
ordered_mapping[i] = Residue(
|
| 296 |
+
residue_number=None, insertion_code="", hetflag=False
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
self.seqres_to_structure[auth_chain_id] = ordered_mapping
|
| 300 |
+
else:
|
| 301 |
+
# Handle case where auth_chain_id is not in chain_to_seqres
|
| 302 |
+
# This can happen if the chain is not a polymer or if there's a parsing issue
|
| 303 |
+
# Create a basic mapping based on the chain_data
|
| 304 |
+
if chain_data[asym_id]:
|
| 305 |
+
# Sort by sequence index to create ordered mapping
|
| 306 |
+
sorted_indices = sorted(chain_data[asym_id].keys())
|
| 307 |
+
ordered_mapping = {}
|
| 308 |
+
for i, seq_idx in enumerate(sorted_indices):
|
| 309 |
+
ordered_mapping[i] = chain_data[asym_id][seq_idx]
|
| 310 |
+
self.seqres_to_structure[auth_chain_id] = ordered_mapping
|
| 311 |
+
|
| 312 |
+
# Ensure all chains have complete mappings
|
| 313 |
+
for chain_id in self.chain_to_seqres:
|
| 314 |
+
if chain_id not in self.seqres_to_structure:
|
| 315 |
+
seq_len = len(self.chain_to_seqres[chain_id])
|
| 316 |
+
self.seqres_to_structure[chain_id] = {
|
| 317 |
+
i: Residue(residue_number=None, insertion_code="", hetflag=False)
|
| 318 |
+
for i in range(seq_len)
|
| 319 |
+
}
|
| 320 |
+
else:
|
| 321 |
+
# Fill in any missing indices
|
| 322 |
+
seq_len = len(self.chain_to_seqres[chain_id])
|
| 323 |
+
mapping = self.seqres_to_structure[chain_id]
|
| 324 |
+
for i in range(seq_len):
|
| 325 |
+
if i not in mapping:
|
| 326 |
+
mapping[i] = Residue(
|
| 327 |
+
residue_number=None, insertion_code="", hetflag=False
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# Fallback: create basic mappings from structure for missing chains
|
| 331 |
+
if (
|
| 332 |
+
self.structure
|
| 333 |
+
and hasattr(self.structure, "chain_id")
|
| 334 |
+
and self.structure.chain_id is not None
|
| 335 |
+
and hasattr(self.structure.chain_id, "__iter__")
|
| 336 |
+
):
|
| 337 |
+
for chain_id in set(self.structure.chain_id):
|
| 338 |
+
if chain_id not in self.seqres_to_structure:
|
| 339 |
+
chain_structure = self.structure[
|
| 340 |
+
self.structure.chain_id == chain_id
|
| 341 |
+
]
|
| 342 |
+
if (
|
| 343 |
+
hasattr(chain_structure, "res_id")
|
| 344 |
+
and chain_structure.res_id is not None
|
| 345 |
+
and hasattr(chain_structure.res_id, "__iter__")
|
| 346 |
+
):
|
| 347 |
+
residue_ids = list(set(chain_structure.res_id))
|
| 348 |
+
residue_ids.sort()
|
| 349 |
+
|
| 350 |
+
self.seqres_to_structure[chain_id] = {
|
| 351 |
+
i: Residue(
|
| 352 |
+
residue_number=res_id, insertion_code="", hetflag=False
|
| 353 |
+
)
|
| 354 |
+
for i, res_id in enumerate(residue_ids)
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
def _parse_nonpoly_from_mmcif(self) -> dict[tuple, bs.AtomArray]:
|
| 358 |
+
"""Parse non-polymer coordinates from mmCIF block data."""
|
| 359 |
+
nonpoly_coords = {}
|
| 360 |
+
|
| 361 |
+
# Get non-polymer entities from the mmCIF block
|
| 362 |
+
assert self.raw is not None
|
| 363 |
+
block = self.raw.block
|
| 364 |
+
nonpoly_entities = set()
|
| 365 |
+
|
| 366 |
+
# Find non-polymer entities
|
| 367 |
+
if "entity" in block:
|
| 368 |
+
entity_cat = block["entity"]
|
| 369 |
+
entity_ids = entity_cat["id"].as_array(str)
|
| 370 |
+
entity_types = entity_cat["type"].as_array(str)
|
| 371 |
+
|
| 372 |
+
for entity_id, entity_type in zip(entity_ids, entity_types):
|
| 373 |
+
if entity_type.upper() in ["NON-POLYMER", "WATER", "BRANCHED"]:
|
| 374 |
+
nonpoly_entities.add(entity_id)
|
| 375 |
+
|
| 376 |
+
# Map entities to chains for non-polymers
|
| 377 |
+
entity_to_chains = {}
|
| 378 |
+
if "pdbx_entity_nonpoly" in block:
|
| 379 |
+
nonpoly_cat = block["pdbx_entity_nonpoly"]
|
| 380 |
+
entity_ids = nonpoly_cat["entity_id"].as_array(str)
|
| 381 |
+
comp_ids = nonpoly_cat["comp_id"].as_array(str)
|
| 382 |
+
|
| 383 |
+
for entity_id, comp_id in zip(entity_ids, comp_ids):
|
| 384 |
+
if entity_id in nonpoly_entities:
|
| 385 |
+
entity_to_chains[entity_id] = comp_id
|
| 386 |
+
|
| 387 |
+
# Get atom site information for non-polymers
|
| 388 |
+
if "atom_site" in block:
|
| 389 |
+
atom_cat = block["atom_site"]
|
| 390 |
+
atom_chain_ids = atom_cat["label_asym_id"].as_array(str)
|
| 391 |
+
atom_entity_ids = atom_cat["label_entity_id"].as_array(str)
|
| 392 |
+
atom_comp_ids = atom_cat["label_comp_id"].as_array(str)
|
| 393 |
+
|
| 394 |
+
# Group non-polymer atoms by entity and chain
|
| 395 |
+
nonpoly_atom_groups = {}
|
| 396 |
+
for i, (chain_id, entity_id, comp_id) in enumerate(
|
| 397 |
+
zip(atom_chain_ids, atom_entity_ids, atom_comp_ids)
|
| 398 |
+
):
|
| 399 |
+
if entity_id in nonpoly_entities:
|
| 400 |
+
key = (comp_id, chain_id)
|
| 401 |
+
if key not in nonpoly_atom_groups:
|
| 402 |
+
nonpoly_atom_groups[key] = []
|
| 403 |
+
nonpoly_atom_groups[key].append(i)
|
| 404 |
+
|
| 405 |
+
# Extract coordinates for each non-polymer group
|
| 406 |
+
for (comp_id, chain_id), atom_indices in nonpoly_atom_groups.items():
|
| 407 |
+
# Match atoms by comparing chain_id and residue name
|
| 408 |
+
structure_mask = (self.structure.chain_id == chain_id) & (
|
| 409 |
+
self.structure.res_name == comp_id
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
if structure_mask.any():
|
| 413 |
+
nonpoly_array = self.structure[structure_mask]
|
| 414 |
+
if (
|
| 415 |
+
isinstance(nonpoly_array, (bs.AtomArray, bs.AtomArrayStack))
|
| 416 |
+
and len(nonpoly_array) > 0
|
| 417 |
+
):
|
| 418 |
+
nonpoly_coords[(comp_id, chain_id)] = nonpoly_array
|
| 419 |
+
|
| 420 |
+
return nonpoly_coords
|
| 421 |
+
|
| 422 |
+
def _parse_nonpoly_fallback(self) -> dict[tuple, bs.AtomArray]:
|
| 423 |
+
"""Fallback method to extract heteroatoms directly from structure."""
|
| 424 |
+
nonpoly_coords = {}
|
| 425 |
+
|
| 426 |
+
if not (self.structure and hasattr(self.structure, "chain_id")):
|
| 427 |
+
return nonpoly_coords
|
| 428 |
+
|
| 429 |
+
# Create set of standard residues from residue_constants
|
| 430 |
+
standard_residues = set(residue_constants.resnames[:-1]) # Exclude 'UNK'
|
| 431 |
+
standard_residues.update({"A", "C", "G", "T", "U"}) # Add nucleic acids
|
| 432 |
+
|
| 433 |
+
if hasattr(self.structure, "chain_id") and self.structure.chain_id is not None:
|
| 434 |
+
for chain_id in set(self.structure.chain_id):
|
| 435 |
+
chain_structure = self.structure[self.structure.chain_id == chain_id]
|
| 436 |
+
|
| 437 |
+
# Find non-standard residues
|
| 438 |
+
if (
|
| 439 |
+
hasattr(chain_structure, "res_name")
|
| 440 |
+
and chain_structure.res_name is not None
|
| 441 |
+
and hasattr(chain_structure.res_name, "__iter__")
|
| 442 |
+
):
|
| 443 |
+
for res_name in set(chain_structure.res_name):
|
| 444 |
+
if res_name not in standard_residues:
|
| 445 |
+
res_mask = (chain_structure.chain_id == chain_id) & (
|
| 446 |
+
chain_structure.res_name == res_name
|
| 447 |
+
)
|
| 448 |
+
if res_mask.any() and isinstance(
|
| 449 |
+
chain_structure, (bs.AtomArray, bs.AtomArrayStack)
|
| 450 |
+
):
|
| 451 |
+
nonpoly_array = chain_structure[res_mask]
|
| 452 |
+
nonpoly_coords[(res_name, chain_id)] = nonpoly_array
|
| 453 |
+
|
| 454 |
+
return nonpoly_coords
|
| 455 |
+
|
| 456 |
+
@functools.cached_property
|
| 457 |
+
def non_polymer_coords(self) -> dict[tuple, bs.AtomArray]:
|
| 458 |
+
"""
|
| 459 |
+
Extract non-polymer coordinates (ligands, cofactors, etc.) from mmCIF structure.
|
| 460 |
+
|
| 461 |
+
Returns a dictionary mapping (nonpolymer_info, chain_id) tuples to AtomArrays.
|
| 462 |
+
"""
|
| 463 |
+
if not self.structure or not self.raw:
|
| 464 |
+
return {}
|
| 465 |
+
|
| 466 |
+
try:
|
| 467 |
+
return self._parse_nonpoly_from_mmcif()
|
| 468 |
+
except Exception:
|
| 469 |
+
return self._parse_nonpoly_fallback()
|
esmfold2_molecular_complex.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
esmfold2_msa.py
CHANGED
|
@@ -1,506 +1,506 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import dataclasses
|
| 4 |
-
import string
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
from functools import cached_property
|
| 7 |
-
from itertools import islice
|
| 8 |
-
from typing import Sequence
|
| 9 |
-
|
| 10 |
-
import numpy as np
|
| 11 |
-
from Bio import SeqIO
|
| 12 |
-
from scipy.spatial.distance import cdist
|
| 13 |
-
|
| 14 |
-
from .esmfold2_misc import slice_any_object
|
| 15 |
-
from .esmfold2_msa_filter_sequences import greedy_select_indices, hhfilter
|
| 16 |
-
from .esmfold2_parsing import FastaEntry, read_sequences, write_sequences
|
| 17 |
-
from .esmfold2_sequential_dataclass import SequentialDataclass
|
| 18 |
-
from .esmfold2_system import PathOrBuffer
|
| 19 |
-
|
| 20 |
-
REMOVE_LOWERCASE_TRANSLATION = str.maketrans(dict.fromkeys(string.ascii_lowercase))
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def remove_insertions_from_sequence(seq: str) -> str:
|
| 24 |
-
return seq.translate(REMOVE_LOWERCASE_TRANSLATION)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
@dataclass(frozen=True)
|
| 28 |
-
class MSA(SequentialDataclass):
|
| 29 |
-
"""Object-oriented interface to an MSA.
|
| 30 |
-
|
| 31 |
-
Args:
|
| 32 |
-
sequences (list[str]): List of protein sequences
|
| 33 |
-
headers (list[str]): List of headers describing the sequences
|
| 34 |
-
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
entries: list[FastaEntry]
|
| 38 |
-
|
| 39 |
-
@cached_property
|
| 40 |
-
def sequences(self) -> list[str]:
|
| 41 |
-
return [entry.sequence for entry in self.entries]
|
| 42 |
-
|
| 43 |
-
@cached_property
|
| 44 |
-
def headers(self) -> list[str]:
|
| 45 |
-
return [entry.header for entry in self.entries]
|
| 46 |
-
|
| 47 |
-
def __repr__(self):
|
| 48 |
-
return (
|
| 49 |
-
f"MSA({self.entries[0].header}: Depth={self.depth}, Length={self.seqlen})"
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
def to_fast_msa(self) -> FastMSA:
|
| 53 |
-
return FastMSA(self.array, self.headers)
|
| 54 |
-
|
| 55 |
-
@classmethod
|
| 56 |
-
def from_a3m(
|
| 57 |
-
cls,
|
| 58 |
-
path: PathOrBuffer,
|
| 59 |
-
remove_insertions: bool = True,
|
| 60 |
-
max_sequences: int | None = None,
|
| 61 |
-
) -> MSA:
|
| 62 |
-
entries = []
|
| 63 |
-
for header, seq in islice(read_sequences(path), max_sequences):
|
| 64 |
-
if remove_insertions:
|
| 65 |
-
seq = remove_insertions_from_sequence(seq)
|
| 66 |
-
if entries:
|
| 67 |
-
assert (
|
| 68 |
-
len(seq) == len(entries[0].sequence)
|
| 69 |
-
), f"Sequence length mismatch. Expected: {len(entries[0].sequence)}, Received: {len(seq)}"
|
| 70 |
-
entries.append(FastaEntry(header, seq))
|
| 71 |
-
return cls(entries)
|
| 72 |
-
|
| 73 |
-
def to_a3m(self, path: PathOrBuffer) -> None:
|
| 74 |
-
write_sequences(self.entries, path)
|
| 75 |
-
|
| 76 |
-
@classmethod
|
| 77 |
-
def from_stockholm(
|
| 78 |
-
cls,
|
| 79 |
-
path: PathOrBuffer,
|
| 80 |
-
remove_insertions: bool = True,
|
| 81 |
-
max_sequences: int | None = None,
|
| 82 |
-
) -> MSA:
|
| 83 |
-
entries = []
|
| 84 |
-
for record in islice(SeqIO.parse(path, "stockholm"), max_sequences):
|
| 85 |
-
header = f"{record.id} {record.description}"
|
| 86 |
-
seq = str(record.seq)
|
| 87 |
-
if entries:
|
| 88 |
-
assert (
|
| 89 |
-
len(seq) == len(entries[0].sequence)
|
| 90 |
-
), f"Sequence length mismatch. Expected: {len(entries[0].sequence)}, Received: {len(seq)}"
|
| 91 |
-
entries.append(FastaEntry(header, seq))
|
| 92 |
-
msa = cls(entries)
|
| 93 |
-
if remove_insertions:
|
| 94 |
-
keep_inds = [i for i, aa in enumerate(msa.query) if aa != "-"]
|
| 95 |
-
msa = msa.select_positions(keep_inds)
|
| 96 |
-
return msa
|
| 97 |
-
|
| 98 |
-
def to_bytes(self) -> bytes:
|
| 99 |
-
version = 1
|
| 100 |
-
version_bytes = version.to_bytes(1, "little")
|
| 101 |
-
seqlen_bytes = self.seqlen.to_bytes(4, "little")
|
| 102 |
-
depth_bytes = self.depth.to_bytes(4, "little")
|
| 103 |
-
array_bytes = self.array.tobytes()
|
| 104 |
-
header_bytes = "\n".join(entry.header for entry in self.entries).encode()
|
| 105 |
-
all_bytes = (
|
| 106 |
-
version_bytes + seqlen_bytes + depth_bytes + array_bytes + header_bytes
|
| 107 |
-
)
|
| 108 |
-
return all_bytes
|
| 109 |
-
|
| 110 |
-
@classmethod
|
| 111 |
-
def from_bytes(cls, data: bytes) -> MSA:
|
| 112 |
-
version_bytes, seqlen_bytes, depth_bytes, data = (
|
| 113 |
-
data[:1],
|
| 114 |
-
data[1:5],
|
| 115 |
-
data[5:9],
|
| 116 |
-
data[9:],
|
| 117 |
-
)
|
| 118 |
-
version = int.from_bytes(version_bytes, "little")
|
| 119 |
-
if version != 1:
|
| 120 |
-
raise ValueError(f"Unsupported version: {version}")
|
| 121 |
-
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 122 |
-
depth = int.from_bytes(depth_bytes, "little")
|
| 123 |
-
array_bytes, header_bytes = data[: seqlen * depth], data[seqlen * depth :]
|
| 124 |
-
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 125 |
-
array = array.reshape(depth, seqlen)
|
| 126 |
-
headers = header_bytes.decode().split("\n")
|
| 127 |
-
# Sometimes the separation is two newlines, which results in an empty header.
|
| 128 |
-
headers = [header for header in headers if header]
|
| 129 |
-
# If all headers were empty (e.g., saved from from_sequences), use empty headers
|
| 130 |
-
if len(headers) == 0 and depth > 0:
|
| 131 |
-
headers = [""] * depth
|
| 132 |
-
entries = [
|
| 133 |
-
FastaEntry(header, b"".join(row).decode())
|
| 134 |
-
for header, row in zip(headers, array)
|
| 135 |
-
]
|
| 136 |
-
return cls(entries)
|
| 137 |
-
|
| 138 |
-
# TODO(jmaccarl): set remove_insertions to True by default here to match other utils
|
| 139 |
-
@classmethod
|
| 140 |
-
def from_sequences(
|
| 141 |
-
cls, sequences: list[str], remove_insertions: bool = False
|
| 142 |
-
) -> MSA:
|
| 143 |
-
if remove_insertions:
|
| 144 |
-
entries = [
|
| 145 |
-
FastaEntry("", remove_insertions_from_sequence(seq))
|
| 146 |
-
for seq in sequences
|
| 147 |
-
]
|
| 148 |
-
else:
|
| 149 |
-
entries = [FastaEntry("", seq) for seq in sequences]
|
| 150 |
-
return cls(entries)
|
| 151 |
-
|
| 152 |
-
def to_sequence_bytes(self) -> bytes:
|
| 153 |
-
"""Stores ONLY SEQUENCES in array format as bytes. Header information will be lost."""
|
| 154 |
-
seqlen_bytes = self.seqlen.to_bytes(4, "little")
|
| 155 |
-
array_bytes = self.array.tobytes()
|
| 156 |
-
all_bytes = seqlen_bytes + array_bytes
|
| 157 |
-
return all_bytes
|
| 158 |
-
|
| 159 |
-
@classmethod
|
| 160 |
-
def from_sequence_bytes(cls, data: bytes) -> MSA:
|
| 161 |
-
seqlen_bytes, array_bytes = data[:4], data[4:]
|
| 162 |
-
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 163 |
-
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 164 |
-
array = array.reshape(-1, seqlen)
|
| 165 |
-
entries = [FastaEntry("", b"".join(row).decode()) for row in array]
|
| 166 |
-
return cls(entries)
|
| 167 |
-
|
| 168 |
-
@property
|
| 169 |
-
def depth(self) -> int:
|
| 170 |
-
return len(self.entries)
|
| 171 |
-
|
| 172 |
-
@property
|
| 173 |
-
def seqlen(self) -> int:
|
| 174 |
-
return len(self.entries[0].sequence)
|
| 175 |
-
|
| 176 |
-
@cached_property
|
| 177 |
-
def array(self) -> np.ndarray:
|
| 178 |
-
return np.array([list(seq) for seq in self.sequences], dtype="|S1")
|
| 179 |
-
|
| 180 |
-
@property
|
| 181 |
-
def query(self) -> str:
|
| 182 |
-
return self.entries[0].sequence
|
| 183 |
-
|
| 184 |
-
def select_sequences(self, indices: Sequence[int] | np.ndarray) -> MSA:
|
| 185 |
-
"""Subselect rows of the MSA."""
|
| 186 |
-
entries = [self.entries[idx] for idx in indices]
|
| 187 |
-
return dataclasses.replace(self, entries=entries)
|
| 188 |
-
|
| 189 |
-
def select_positions(self, indices: Sequence[int] | np.ndarray) -> MSA:
|
| 190 |
-
"""Subselect columns of the MSA."""
|
| 191 |
-
entries = [
|
| 192 |
-
FastaEntry(header, "".join(seq[idx] for idx in indices))
|
| 193 |
-
for header, seq in self.entries
|
| 194 |
-
]
|
| 195 |
-
return dataclasses.replace(self, entries=entries)
|
| 196 |
-
|
| 197 |
-
def __getitem__(self, indices: int | list[int] | slice | np.ndarray):
|
| 198 |
-
if isinstance(indices, int):
|
| 199 |
-
indices = [indices]
|
| 200 |
-
|
| 201 |
-
entries = [
|
| 202 |
-
FastaEntry(header, slice_any_object(seq, indices))
|
| 203 |
-
for header, seq in self.entries
|
| 204 |
-
]
|
| 205 |
-
return dataclasses.replace(self, entries=entries)
|
| 206 |
-
|
| 207 |
-
def __len__(self):
|
| 208 |
-
return self.seqlen
|
| 209 |
-
|
| 210 |
-
def greedy_select(self, num_seqs: int, mode: str = "max") -> MSA:
|
| 211 |
-
"""Greedily select sequences that either maximize or minimize hamming distance.
|
| 212 |
-
|
| 213 |
-
Algorithm proposed in the MSA Transformer paper. Starting from the query sequence,
|
| 214 |
-
iteratively add sequences to the list with the maximum (minimum) average Hamming
|
| 215 |
-
distance to the existing set of sequences.
|
| 216 |
-
|
| 217 |
-
Args:
|
| 218 |
-
num_seqs (int): Number of sequences to select.
|
| 219 |
-
mode (str): Whether to maximize or minimize diversity. DO NOT pick 'min' unless
|
| 220 |
-
you're doing it to prove a point for a paper.
|
| 221 |
-
|
| 222 |
-
Returns:
|
| 223 |
-
MSA object w/ subselected sequences.
|
| 224 |
-
"""
|
| 225 |
-
assert mode in ("max", "min")
|
| 226 |
-
if self.depth <= num_seqs:
|
| 227 |
-
return self
|
| 228 |
-
|
| 229 |
-
indices = greedy_select_indices(self.array, num_seqs, mode)
|
| 230 |
-
return self.select_sequences(indices)
|
| 231 |
-
|
| 232 |
-
def hhfilter(
|
| 233 |
-
self,
|
| 234 |
-
seqid: int = 90,
|
| 235 |
-
diff: int = 0,
|
| 236 |
-
cov: int = 0,
|
| 237 |
-
qid: int = 0,
|
| 238 |
-
qsc: float = -20.0,
|
| 239 |
-
binary: str = "hhfilter",
|
| 240 |
-
) -> MSA:
|
| 241 |
-
"""Apply hhfilter to the sequences in the MSA and return a filtered MSA."""
|
| 242 |
-
|
| 243 |
-
indices = hhfilter(
|
| 244 |
-
self.sequences,
|
| 245 |
-
seqid=seqid,
|
| 246 |
-
diff=diff,
|
| 247 |
-
cov=cov,
|
| 248 |
-
qid=qid,
|
| 249 |
-
qsc=qsc,
|
| 250 |
-
binary=binary,
|
| 251 |
-
)
|
| 252 |
-
return self.select_sequences(indices)
|
| 253 |
-
|
| 254 |
-
def select_random_sequences(self, num_seqs: int) -> MSA:
|
| 255 |
-
"""Uses random sampling to subselect sequences from the MSA. Always
|
| 256 |
-
keeps the query sequence.
|
| 257 |
-
"""
|
| 258 |
-
if num_seqs >= self.depth:
|
| 259 |
-
return self
|
| 260 |
-
|
| 261 |
-
# Subselect random, always keeping the query sequence.
|
| 262 |
-
indices = np.sort(
|
| 263 |
-
np.append(
|
| 264 |
-
0, np.random.choice(self.depth - 1, num_seqs - 1, replace=False) + 1
|
| 265 |
-
)
|
| 266 |
-
)
|
| 267 |
-
msa = self.select_sequences(indices) # type: ignore
|
| 268 |
-
return msa
|
| 269 |
-
|
| 270 |
-
def select_diverse_sequences(self, num_seqs: int) -> MSA:
|
| 271 |
-
"""Applies hhfilter to select ~num_seqs sequences, then uses random sampling
|
| 272 |
-
to subselect if necessary.
|
| 273 |
-
"""
|
| 274 |
-
if num_seqs >= self.depth:
|
| 275 |
-
return self
|
| 276 |
-
|
| 277 |
-
msa = self.hhfilter(diff=num_seqs)
|
| 278 |
-
if num_seqs < msa.depth:
|
| 279 |
-
msa = msa.select_random_sequences(num_seqs)
|
| 280 |
-
return msa
|
| 281 |
-
|
| 282 |
-
def pad_to_depth(self, depth: int) -> MSA:
|
| 283 |
-
if depth < self.depth:
|
| 284 |
-
raise ValueError(f"Cannot pad to depth {depth} when depth is {self.depth}")
|
| 285 |
-
elif depth == self.depth:
|
| 286 |
-
return self
|
| 287 |
-
|
| 288 |
-
num_to_add = depth - self.depth
|
| 289 |
-
extra_entries = [FastaEntry("", "-" * self.seqlen) for _ in range(num_to_add)]
|
| 290 |
-
return dataclasses.replace(self, entries=self.entries + extra_entries)
|
| 291 |
-
|
| 292 |
-
@classmethod
|
| 293 |
-
def stack(
|
| 294 |
-
cls, msas: Sequence[MSA], remove_query_from_later_msas: bool = True
|
| 295 |
-
) -> MSA:
|
| 296 |
-
"""Stack a series of MSAs. Optionally remove the query from msas after the first."""
|
| 297 |
-
all_entries = []
|
| 298 |
-
for i, msa in enumerate(msas):
|
| 299 |
-
entries = msa.entries
|
| 300 |
-
if i > 0 and remove_query_from_later_msas:
|
| 301 |
-
entries = entries[1:]
|
| 302 |
-
all_entries.extend(entries)
|
| 303 |
-
return cls(entries=all_entries)
|
| 304 |
-
|
| 305 |
-
@cached_property
|
| 306 |
-
def seqid(self) -> np.ndarray:
|
| 307 |
-
array = self.array.view(np.uint8)
|
| 308 |
-
seqid = 1 - cdist(array[0][None], array, "hamming")
|
| 309 |
-
return seqid[0]
|
| 310 |
-
|
| 311 |
-
@classmethod
|
| 312 |
-
def concat(
|
| 313 |
-
cls,
|
| 314 |
-
msas: Sequence[MSA],
|
| 315 |
-
join_token: str | None = "|",
|
| 316 |
-
allow_depth_mismatch: bool = False,
|
| 317 |
-
) -> MSA:
|
| 318 |
-
"""Concatenate a series of MSAs horizontally, along the sequence dimension."""
|
| 319 |
-
if not msas:
|
| 320 |
-
raise ValueError("Cannot concatenate an empty list of MSAs")
|
| 321 |
-
msa_depths = [msa.depth for msa in msas]
|
| 322 |
-
if len(set(msa_depths)) != 1:
|
| 323 |
-
if not allow_depth_mismatch:
|
| 324 |
-
raise ValueError("Depth mismatch in concatenating MSAs")
|
| 325 |
-
else:
|
| 326 |
-
max_depth = max(msa_depths)
|
| 327 |
-
msas = [msa.pad_to_depth(max_depth) for msa in msas]
|
| 328 |
-
headers = [
|
| 329 |
-
"|".join([str(h) for h in headers])
|
| 330 |
-
for headers in zip(*(msa.headers for msa in msas))
|
| 331 |
-
]
|
| 332 |
-
|
| 333 |
-
if join_token is None:
|
| 334 |
-
join_token = ""
|
| 335 |
-
|
| 336 |
-
seqs = [join_token.join(vals) for vals in zip(*(msa.sequences for msa in msas))]
|
| 337 |
-
entries = [FastaEntry(header, seq) for header, seq in zip(headers, seqs)]
|
| 338 |
-
return cls(entries)
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
@dataclass(frozen=True)
|
| 342 |
-
class FastMSA(SequentialDataclass):
|
| 343 |
-
"""Object-oriented interface to an MSA stored as a numpy uint8 array."""
|
| 344 |
-
|
| 345 |
-
array: np.ndarray
|
| 346 |
-
headers: list[str] | None = None
|
| 347 |
-
|
| 348 |
-
def __post_init__(self):
|
| 349 |
-
if self.headers is not None:
|
| 350 |
-
assert (
|
| 351 |
-
len(self.headers) == self.depth
|
| 352 |
-
), "Number of headers must match depth."
|
| 353 |
-
|
| 354 |
-
@classmethod
|
| 355 |
-
def from_bytes(cls, data: bytes) -> FastMSA:
|
| 356 |
-
version_bytes, seqlen_bytes, depth_bytes, data = (
|
| 357 |
-
data[:1],
|
| 358 |
-
data[1:5],
|
| 359 |
-
data[5:9],
|
| 360 |
-
data[9:],
|
| 361 |
-
)
|
| 362 |
-
version = int.from_bytes(version_bytes, "little")
|
| 363 |
-
if version != 1:
|
| 364 |
-
raise ValueError(f"Unsupported version: {version}")
|
| 365 |
-
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 366 |
-
depth = int.from_bytes(depth_bytes, "little")
|
| 367 |
-
array_bytes, header_bytes = data[: seqlen * depth], data[seqlen * depth :]
|
| 368 |
-
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 369 |
-
array = array.reshape(depth, seqlen)
|
| 370 |
-
headers = header_bytes.decode().split("\n")
|
| 371 |
-
# Sometimes the separation is two newlines, which results in an empty header.
|
| 372 |
-
headers = [header for header in headers if header]
|
| 373 |
-
# If all headers were empty (e.g., saved from from_sequences), use empty headers
|
| 374 |
-
if len(headers) == 0 and depth > 0:
|
| 375 |
-
headers = [""] * depth
|
| 376 |
-
return cls(array, headers)
|
| 377 |
-
|
| 378 |
-
@classmethod
|
| 379 |
-
def from_sequence_bytes(cls, data: bytes) -> FastMSA:
|
| 380 |
-
seqlen_bytes, array_bytes = data[:4], data[4:]
|
| 381 |
-
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 382 |
-
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 383 |
-
array = array.reshape(-1, seqlen)
|
| 384 |
-
return cls(array)
|
| 385 |
-
|
| 386 |
-
@property
|
| 387 |
-
def depth(self) -> int:
|
| 388 |
-
return self.array.shape[0]
|
| 389 |
-
|
| 390 |
-
@property
|
| 391 |
-
def seqlen(self) -> int:
|
| 392 |
-
return self.array.shape[1]
|
| 393 |
-
|
| 394 |
-
def __len__(self):
|
| 395 |
-
return self.seqlen
|
| 396 |
-
|
| 397 |
-
def __getitem__(self, indices: int | list[int] | slice | np.ndarray):
|
| 398 |
-
if isinstance(indices, int):
|
| 399 |
-
indices = [indices]
|
| 400 |
-
|
| 401 |
-
return dataclasses.replace(self, array=self.array[:, indices])
|
| 402 |
-
|
| 403 |
-
def select_sequences(self, indices: Sequence[int] | np.ndarray) -> FastMSA:
|
| 404 |
-
"""Subselect rows of the MSA."""
|
| 405 |
-
array = self.array[indices]
|
| 406 |
-
headers = (
|
| 407 |
-
[self.headers[idx] for idx in indices] if self.headers is not None else None
|
| 408 |
-
)
|
| 409 |
-
return dataclasses.replace(self, array=array, headers=headers)
|
| 410 |
-
|
| 411 |
-
def select_random_sequences(self, num_seqs: int) -> FastMSA:
|
| 412 |
-
"""Uses random sampling to subselect sequences from the MSA. Always
|
| 413 |
-
keeps the query sequence.
|
| 414 |
-
"""
|
| 415 |
-
if num_seqs >= self.depth:
|
| 416 |
-
return self
|
| 417 |
-
|
| 418 |
-
# Subselect random, always keeping the query sequence.
|
| 419 |
-
indices = np.sort(
|
| 420 |
-
np.append(
|
| 421 |
-
0, np.random.choice(self.depth - 1, num_seqs - 1, replace=False) + 1
|
| 422 |
-
)
|
| 423 |
-
)
|
| 424 |
-
msa = self.select_sequences(indices) # type: ignore
|
| 425 |
-
return msa
|
| 426 |
-
|
| 427 |
-
def pad_to_depth(self, depth: int) -> FastMSA:
|
| 428 |
-
if depth < self.depth:
|
| 429 |
-
raise ValueError(f"Cannot pad to depth {depth} when depth is {self.depth}")
|
| 430 |
-
elif depth == self.depth:
|
| 431 |
-
return self
|
| 432 |
-
|
| 433 |
-
num_to_add = depth - self.depth
|
| 434 |
-
array = np.pad(
|
| 435 |
-
self.array,
|
| 436 |
-
[(0, num_to_add), (0, 0)],
|
| 437 |
-
constant_values=ord("-") if self.array.dtype == np.uint8 else b"-",
|
| 438 |
-
)
|
| 439 |
-
headers = self.headers
|
| 440 |
-
if headers is not None:
|
| 441 |
-
headers = headers + [""] * num_to_add
|
| 442 |
-
return dataclasses.replace(self, array=array, headers=headers)
|
| 443 |
-
|
| 444 |
-
@classmethod
|
| 445 |
-
def concat(
|
| 446 |
-
cls,
|
| 447 |
-
msas: Sequence[FastMSA],
|
| 448 |
-
join_token: str | None = None,
|
| 449 |
-
allow_depth_mismatch: bool = False,
|
| 450 |
-
) -> FastMSA:
|
| 451 |
-
"""Concatenate a series of MSAs horizontally, along the sequence dimension."""
|
| 452 |
-
if not msas:
|
| 453 |
-
raise ValueError("Cannot concatenate an empty list of MSAs")
|
| 454 |
-
if join_token is not None and join_token != "":
|
| 455 |
-
raise NotImplementedError("join_token is not supported for FastMSA")
|
| 456 |
-
|
| 457 |
-
msa_depths = [msa.depth for msa in msas]
|
| 458 |
-
if len(set(msa_depths)) != 1:
|
| 459 |
-
if not allow_depth_mismatch:
|
| 460 |
-
raise ValueError("Depth mismatch in concatenating MSAs")
|
| 461 |
-
else:
|
| 462 |
-
max_depth = max(msa_depths)
|
| 463 |
-
msas = [msa.pad_to_depth(max_depth) for msa in msas]
|
| 464 |
-
headers = [
|
| 465 |
-
"|".join([str(h) for h in headers])
|
| 466 |
-
for headers in zip(
|
| 467 |
-
*(
|
| 468 |
-
msa.headers if msa.headers is not None else [""] * msa.depth
|
| 469 |
-
for msa in msas
|
| 470 |
-
)
|
| 471 |
-
)
|
| 472 |
-
]
|
| 473 |
-
|
| 474 |
-
array = np.concatenate([msa.array for msa in msas], axis=1)
|
| 475 |
-
return cls(array, headers)
|
| 476 |
-
|
| 477 |
-
def to_msa(self) -> MSA:
|
| 478 |
-
headers = (
|
| 479 |
-
self.headers
|
| 480 |
-
if self.headers is not None
|
| 481 |
-
else [f"seq{i}" for i in range(self.depth)]
|
| 482 |
-
)
|
| 483 |
-
entries = [
|
| 484 |
-
FastaEntry(header, b"".join(row).decode())
|
| 485 |
-
for header, row in zip(headers, self.array)
|
| 486 |
-
]
|
| 487 |
-
return MSA(entries)
|
| 488 |
-
|
| 489 |
-
@classmethod
|
| 490 |
-
def stack(
|
| 491 |
-
cls, msas: Sequence[FastMSA], remove_query_from_later_msas: bool = True
|
| 492 |
-
) -> FastMSA:
|
| 493 |
-
"""Stack a series of MSAs. Optionally remove the query from msas after the first."""
|
| 494 |
-
arrays = []
|
| 495 |
-
all_headers = []
|
| 496 |
-
for i, msa in enumerate(msas):
|
| 497 |
-
array = msa.array
|
| 498 |
-
headers = msa.headers
|
| 499 |
-
if i > 0 and remove_query_from_later_msas:
|
| 500 |
-
array = array[1:]
|
| 501 |
-
if headers is not None:
|
| 502 |
-
headers = headers[1:]
|
| 503 |
-
arrays.append(array)
|
| 504 |
-
if headers is not None:
|
| 505 |
-
all_headers.extend(headers)
|
| 506 |
-
return cls(np.concatenate(arrays, axis=0), all_headers)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import dataclasses
|
| 4 |
+
import string
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from functools import cached_property
|
| 7 |
+
from itertools import islice
|
| 8 |
+
from typing import Sequence
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
from Bio import SeqIO
|
| 12 |
+
from scipy.spatial.distance import cdist
|
| 13 |
+
|
| 14 |
+
from .esmfold2_misc import slice_any_object
|
| 15 |
+
from .esmfold2_msa_filter_sequences import greedy_select_indices, hhfilter
|
| 16 |
+
from .esmfold2_parsing import FastaEntry, read_sequences, write_sequences
|
| 17 |
+
from .esmfold2_sequential_dataclass import SequentialDataclass
|
| 18 |
+
from .esmfold2_system import PathOrBuffer
|
| 19 |
+
|
| 20 |
+
REMOVE_LOWERCASE_TRANSLATION = str.maketrans(dict.fromkeys(string.ascii_lowercase))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def remove_insertions_from_sequence(seq: str) -> str:
|
| 24 |
+
return seq.translate(REMOVE_LOWERCASE_TRANSLATION)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass(frozen=True)
|
| 28 |
+
class MSA(SequentialDataclass):
|
| 29 |
+
"""Object-oriented interface to an MSA.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
sequences (list[str]): List of protein sequences
|
| 33 |
+
headers (list[str]): List of headers describing the sequences
|
| 34 |
+
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
entries: list[FastaEntry]
|
| 38 |
+
|
| 39 |
+
@cached_property
|
| 40 |
+
def sequences(self) -> list[str]:
|
| 41 |
+
return [entry.sequence for entry in self.entries]
|
| 42 |
+
|
| 43 |
+
@cached_property
|
| 44 |
+
def headers(self) -> list[str]:
|
| 45 |
+
return [entry.header for entry in self.entries]
|
| 46 |
+
|
| 47 |
+
def __repr__(self):
|
| 48 |
+
return (
|
| 49 |
+
f"MSA({self.entries[0].header}: Depth={self.depth}, Length={self.seqlen})"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def to_fast_msa(self) -> FastMSA:
|
| 53 |
+
return FastMSA(self.array, self.headers)
|
| 54 |
+
|
| 55 |
+
@classmethod
|
| 56 |
+
def from_a3m(
|
| 57 |
+
cls,
|
| 58 |
+
path: PathOrBuffer,
|
| 59 |
+
remove_insertions: bool = True,
|
| 60 |
+
max_sequences: int | None = None,
|
| 61 |
+
) -> MSA:
|
| 62 |
+
entries = []
|
| 63 |
+
for header, seq in islice(read_sequences(path), max_sequences):
|
| 64 |
+
if remove_insertions:
|
| 65 |
+
seq = remove_insertions_from_sequence(seq)
|
| 66 |
+
if entries:
|
| 67 |
+
assert (
|
| 68 |
+
len(seq) == len(entries[0].sequence)
|
| 69 |
+
), f"Sequence length mismatch. Expected: {len(entries[0].sequence)}, Received: {len(seq)}"
|
| 70 |
+
entries.append(FastaEntry(header, seq))
|
| 71 |
+
return cls(entries)
|
| 72 |
+
|
| 73 |
+
def to_a3m(self, path: PathOrBuffer) -> None:
|
| 74 |
+
write_sequences(self.entries, path)
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def from_stockholm(
|
| 78 |
+
cls,
|
| 79 |
+
path: PathOrBuffer,
|
| 80 |
+
remove_insertions: bool = True,
|
| 81 |
+
max_sequences: int | None = None,
|
| 82 |
+
) -> MSA:
|
| 83 |
+
entries = []
|
| 84 |
+
for record in islice(SeqIO.parse(path, "stockholm"), max_sequences):
|
| 85 |
+
header = f"{record.id} {record.description}"
|
| 86 |
+
seq = str(record.seq)
|
| 87 |
+
if entries:
|
| 88 |
+
assert (
|
| 89 |
+
len(seq) == len(entries[0].sequence)
|
| 90 |
+
), f"Sequence length mismatch. Expected: {len(entries[0].sequence)}, Received: {len(seq)}"
|
| 91 |
+
entries.append(FastaEntry(header, seq))
|
| 92 |
+
msa = cls(entries)
|
| 93 |
+
if remove_insertions:
|
| 94 |
+
keep_inds = [i for i, aa in enumerate(msa.query) if aa != "-"]
|
| 95 |
+
msa = msa.select_positions(keep_inds)
|
| 96 |
+
return msa
|
| 97 |
+
|
| 98 |
+
def to_bytes(self) -> bytes:
|
| 99 |
+
version = 1
|
| 100 |
+
version_bytes = version.to_bytes(1, "little")
|
| 101 |
+
seqlen_bytes = self.seqlen.to_bytes(4, "little")
|
| 102 |
+
depth_bytes = self.depth.to_bytes(4, "little")
|
| 103 |
+
array_bytes = self.array.tobytes()
|
| 104 |
+
header_bytes = "\n".join(entry.header for entry in self.entries).encode()
|
| 105 |
+
all_bytes = (
|
| 106 |
+
version_bytes + seqlen_bytes + depth_bytes + array_bytes + header_bytes
|
| 107 |
+
)
|
| 108 |
+
return all_bytes
|
| 109 |
+
|
| 110 |
+
@classmethod
|
| 111 |
+
def from_bytes(cls, data: bytes) -> MSA:
|
| 112 |
+
version_bytes, seqlen_bytes, depth_bytes, data = (
|
| 113 |
+
data[:1],
|
| 114 |
+
data[1:5],
|
| 115 |
+
data[5:9],
|
| 116 |
+
data[9:],
|
| 117 |
+
)
|
| 118 |
+
version = int.from_bytes(version_bytes, "little")
|
| 119 |
+
if version != 1:
|
| 120 |
+
raise ValueError(f"Unsupported version: {version}")
|
| 121 |
+
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 122 |
+
depth = int.from_bytes(depth_bytes, "little")
|
| 123 |
+
array_bytes, header_bytes = data[: seqlen * depth], data[seqlen * depth :]
|
| 124 |
+
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 125 |
+
array = array.reshape(depth, seqlen)
|
| 126 |
+
headers = header_bytes.decode().split("\n")
|
| 127 |
+
# Sometimes the separation is two newlines, which results in an empty header.
|
| 128 |
+
headers = [header for header in headers if header]
|
| 129 |
+
# If all headers were empty (e.g., saved from from_sequences), use empty headers
|
| 130 |
+
if len(headers) == 0 and depth > 0:
|
| 131 |
+
headers = [""] * depth
|
| 132 |
+
entries = [
|
| 133 |
+
FastaEntry(header, b"".join(row).decode())
|
| 134 |
+
for header, row in zip(headers, array)
|
| 135 |
+
]
|
| 136 |
+
return cls(entries)
|
| 137 |
+
|
| 138 |
+
# TODO(jmaccarl): set remove_insertions to True by default here to match other utils
|
| 139 |
+
@classmethod
|
| 140 |
+
def from_sequences(
|
| 141 |
+
cls, sequences: list[str], remove_insertions: bool = False
|
| 142 |
+
) -> MSA:
|
| 143 |
+
if remove_insertions:
|
| 144 |
+
entries = [
|
| 145 |
+
FastaEntry("", remove_insertions_from_sequence(seq))
|
| 146 |
+
for seq in sequences
|
| 147 |
+
]
|
| 148 |
+
else:
|
| 149 |
+
entries = [FastaEntry("", seq) for seq in sequences]
|
| 150 |
+
return cls(entries)
|
| 151 |
+
|
| 152 |
+
def to_sequence_bytes(self) -> bytes:
|
| 153 |
+
"""Stores ONLY SEQUENCES in array format as bytes. Header information will be lost."""
|
| 154 |
+
seqlen_bytes = self.seqlen.to_bytes(4, "little")
|
| 155 |
+
array_bytes = self.array.tobytes()
|
| 156 |
+
all_bytes = seqlen_bytes + array_bytes
|
| 157 |
+
return all_bytes
|
| 158 |
+
|
| 159 |
+
@classmethod
|
| 160 |
+
def from_sequence_bytes(cls, data: bytes) -> MSA:
|
| 161 |
+
seqlen_bytes, array_bytes = data[:4], data[4:]
|
| 162 |
+
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 163 |
+
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 164 |
+
array = array.reshape(-1, seqlen)
|
| 165 |
+
entries = [FastaEntry("", b"".join(row).decode()) for row in array]
|
| 166 |
+
return cls(entries)
|
| 167 |
+
|
| 168 |
+
@property
|
| 169 |
+
def depth(self) -> int:
|
| 170 |
+
return len(self.entries)
|
| 171 |
+
|
| 172 |
+
@property
|
| 173 |
+
def seqlen(self) -> int:
|
| 174 |
+
return len(self.entries[0].sequence)
|
| 175 |
+
|
| 176 |
+
@cached_property
|
| 177 |
+
def array(self) -> np.ndarray:
|
| 178 |
+
return np.array([list(seq) for seq in self.sequences], dtype="|S1")
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def query(self) -> str:
|
| 182 |
+
return self.entries[0].sequence
|
| 183 |
+
|
| 184 |
+
def select_sequences(self, indices: Sequence[int] | np.ndarray) -> MSA:
|
| 185 |
+
"""Subselect rows of the MSA."""
|
| 186 |
+
entries = [self.entries[idx] for idx in indices]
|
| 187 |
+
return dataclasses.replace(self, entries=entries)
|
| 188 |
+
|
| 189 |
+
def select_positions(self, indices: Sequence[int] | np.ndarray) -> MSA:
|
| 190 |
+
"""Subselect columns of the MSA."""
|
| 191 |
+
entries = [
|
| 192 |
+
FastaEntry(header, "".join(seq[idx] for idx in indices))
|
| 193 |
+
for header, seq in self.entries
|
| 194 |
+
]
|
| 195 |
+
return dataclasses.replace(self, entries=entries)
|
| 196 |
+
|
| 197 |
+
def __getitem__(self, indices: int | list[int] | slice | np.ndarray):
|
| 198 |
+
if isinstance(indices, int):
|
| 199 |
+
indices = [indices]
|
| 200 |
+
|
| 201 |
+
entries = [
|
| 202 |
+
FastaEntry(header, slice_any_object(seq, indices))
|
| 203 |
+
for header, seq in self.entries
|
| 204 |
+
]
|
| 205 |
+
return dataclasses.replace(self, entries=entries)
|
| 206 |
+
|
| 207 |
+
def __len__(self):
|
| 208 |
+
return self.seqlen
|
| 209 |
+
|
| 210 |
+
def greedy_select(self, num_seqs: int, mode: str = "max") -> MSA:
|
| 211 |
+
"""Greedily select sequences that either maximize or minimize hamming distance.
|
| 212 |
+
|
| 213 |
+
Algorithm proposed in the MSA Transformer paper. Starting from the query sequence,
|
| 214 |
+
iteratively add sequences to the list with the maximum (minimum) average Hamming
|
| 215 |
+
distance to the existing set of sequences.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
num_seqs (int): Number of sequences to select.
|
| 219 |
+
mode (str): Whether to maximize or minimize diversity. DO NOT pick 'min' unless
|
| 220 |
+
you're doing it to prove a point for a paper.
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
MSA object w/ subselected sequences.
|
| 224 |
+
"""
|
| 225 |
+
assert mode in ("max", "min")
|
| 226 |
+
if self.depth <= num_seqs:
|
| 227 |
+
return self
|
| 228 |
+
|
| 229 |
+
indices = greedy_select_indices(self.array, num_seqs, mode)
|
| 230 |
+
return self.select_sequences(indices)
|
| 231 |
+
|
| 232 |
+
def hhfilter(
|
| 233 |
+
self,
|
| 234 |
+
seqid: int = 90,
|
| 235 |
+
diff: int = 0,
|
| 236 |
+
cov: int = 0,
|
| 237 |
+
qid: int = 0,
|
| 238 |
+
qsc: float = -20.0,
|
| 239 |
+
binary: str = "hhfilter",
|
| 240 |
+
) -> MSA:
|
| 241 |
+
"""Apply hhfilter to the sequences in the MSA and return a filtered MSA."""
|
| 242 |
+
|
| 243 |
+
indices = hhfilter(
|
| 244 |
+
self.sequences,
|
| 245 |
+
seqid=seqid,
|
| 246 |
+
diff=diff,
|
| 247 |
+
cov=cov,
|
| 248 |
+
qid=qid,
|
| 249 |
+
qsc=qsc,
|
| 250 |
+
binary=binary,
|
| 251 |
+
)
|
| 252 |
+
return self.select_sequences(indices)
|
| 253 |
+
|
| 254 |
+
def select_random_sequences(self, num_seqs: int) -> MSA:
|
| 255 |
+
"""Uses random sampling to subselect sequences from the MSA. Always
|
| 256 |
+
keeps the query sequence.
|
| 257 |
+
"""
|
| 258 |
+
if num_seqs >= self.depth:
|
| 259 |
+
return self
|
| 260 |
+
|
| 261 |
+
# Subselect random, always keeping the query sequence.
|
| 262 |
+
indices = np.sort(
|
| 263 |
+
np.append(
|
| 264 |
+
0, np.random.choice(self.depth - 1, num_seqs - 1, replace=False) + 1
|
| 265 |
+
)
|
| 266 |
+
)
|
| 267 |
+
msa = self.select_sequences(indices) # type: ignore
|
| 268 |
+
return msa
|
| 269 |
+
|
| 270 |
+
def select_diverse_sequences(self, num_seqs: int) -> MSA:
|
| 271 |
+
"""Applies hhfilter to select ~num_seqs sequences, then uses random sampling
|
| 272 |
+
to subselect if necessary.
|
| 273 |
+
"""
|
| 274 |
+
if num_seqs >= self.depth:
|
| 275 |
+
return self
|
| 276 |
+
|
| 277 |
+
msa = self.hhfilter(diff=num_seqs)
|
| 278 |
+
if num_seqs < msa.depth:
|
| 279 |
+
msa = msa.select_random_sequences(num_seqs)
|
| 280 |
+
return msa
|
| 281 |
+
|
| 282 |
+
def pad_to_depth(self, depth: int) -> MSA:
|
| 283 |
+
if depth < self.depth:
|
| 284 |
+
raise ValueError(f"Cannot pad to depth {depth} when depth is {self.depth}")
|
| 285 |
+
elif depth == self.depth:
|
| 286 |
+
return self
|
| 287 |
+
|
| 288 |
+
num_to_add = depth - self.depth
|
| 289 |
+
extra_entries = [FastaEntry("", "-" * self.seqlen) for _ in range(num_to_add)]
|
| 290 |
+
return dataclasses.replace(self, entries=self.entries + extra_entries)
|
| 291 |
+
|
| 292 |
+
@classmethod
|
| 293 |
+
def stack(
|
| 294 |
+
cls, msas: Sequence[MSA], remove_query_from_later_msas: bool = True
|
| 295 |
+
) -> MSA:
|
| 296 |
+
"""Stack a series of MSAs. Optionally remove the query from msas after the first."""
|
| 297 |
+
all_entries = []
|
| 298 |
+
for i, msa in enumerate(msas):
|
| 299 |
+
entries = msa.entries
|
| 300 |
+
if i > 0 and remove_query_from_later_msas:
|
| 301 |
+
entries = entries[1:]
|
| 302 |
+
all_entries.extend(entries)
|
| 303 |
+
return cls(entries=all_entries)
|
| 304 |
+
|
| 305 |
+
@cached_property
|
| 306 |
+
def seqid(self) -> np.ndarray:
|
| 307 |
+
array = self.array.view(np.uint8)
|
| 308 |
+
seqid = 1 - cdist(array[0][None], array, "hamming")
|
| 309 |
+
return seqid[0]
|
| 310 |
+
|
| 311 |
+
@classmethod
|
| 312 |
+
def concat(
|
| 313 |
+
cls,
|
| 314 |
+
msas: Sequence[MSA],
|
| 315 |
+
join_token: str | None = "|",
|
| 316 |
+
allow_depth_mismatch: bool = False,
|
| 317 |
+
) -> MSA:
|
| 318 |
+
"""Concatenate a series of MSAs horizontally, along the sequence dimension."""
|
| 319 |
+
if not msas:
|
| 320 |
+
raise ValueError("Cannot concatenate an empty list of MSAs")
|
| 321 |
+
msa_depths = [msa.depth for msa in msas]
|
| 322 |
+
if len(set(msa_depths)) != 1:
|
| 323 |
+
if not allow_depth_mismatch:
|
| 324 |
+
raise ValueError("Depth mismatch in concatenating MSAs")
|
| 325 |
+
else:
|
| 326 |
+
max_depth = max(msa_depths)
|
| 327 |
+
msas = [msa.pad_to_depth(max_depth) for msa in msas]
|
| 328 |
+
headers = [
|
| 329 |
+
"|".join([str(h) for h in headers])
|
| 330 |
+
for headers in zip(*(msa.headers for msa in msas))
|
| 331 |
+
]
|
| 332 |
+
|
| 333 |
+
if join_token is None:
|
| 334 |
+
join_token = ""
|
| 335 |
+
|
| 336 |
+
seqs = [join_token.join(vals) for vals in zip(*(msa.sequences for msa in msas))]
|
| 337 |
+
entries = [FastaEntry(header, seq) for header, seq in zip(headers, seqs)]
|
| 338 |
+
return cls(entries)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
@dataclass(frozen=True)
|
| 342 |
+
class FastMSA(SequentialDataclass):
|
| 343 |
+
"""Object-oriented interface to an MSA stored as a numpy uint8 array."""
|
| 344 |
+
|
| 345 |
+
array: np.ndarray
|
| 346 |
+
headers: list[str] | None = None
|
| 347 |
+
|
| 348 |
+
def __post_init__(self):
|
| 349 |
+
if self.headers is not None:
|
| 350 |
+
assert (
|
| 351 |
+
len(self.headers) == self.depth
|
| 352 |
+
), "Number of headers must match depth."
|
| 353 |
+
|
| 354 |
+
@classmethod
|
| 355 |
+
def from_bytes(cls, data: bytes) -> FastMSA:
|
| 356 |
+
version_bytes, seqlen_bytes, depth_bytes, data = (
|
| 357 |
+
data[:1],
|
| 358 |
+
data[1:5],
|
| 359 |
+
data[5:9],
|
| 360 |
+
data[9:],
|
| 361 |
+
)
|
| 362 |
+
version = int.from_bytes(version_bytes, "little")
|
| 363 |
+
if version != 1:
|
| 364 |
+
raise ValueError(f"Unsupported version: {version}")
|
| 365 |
+
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 366 |
+
depth = int.from_bytes(depth_bytes, "little")
|
| 367 |
+
array_bytes, header_bytes = data[: seqlen * depth], data[seqlen * depth :]
|
| 368 |
+
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 369 |
+
array = array.reshape(depth, seqlen)
|
| 370 |
+
headers = header_bytes.decode().split("\n")
|
| 371 |
+
# Sometimes the separation is two newlines, which results in an empty header.
|
| 372 |
+
headers = [header for header in headers if header]
|
| 373 |
+
# If all headers were empty (e.g., saved from from_sequences), use empty headers
|
| 374 |
+
if len(headers) == 0 and depth > 0:
|
| 375 |
+
headers = [""] * depth
|
| 376 |
+
return cls(array, headers)
|
| 377 |
+
|
| 378 |
+
@classmethod
|
| 379 |
+
def from_sequence_bytes(cls, data: bytes) -> FastMSA:
|
| 380 |
+
seqlen_bytes, array_bytes = data[:4], data[4:]
|
| 381 |
+
seqlen = int.from_bytes(seqlen_bytes, "little")
|
| 382 |
+
array = np.frombuffer(array_bytes, dtype="|S1")
|
| 383 |
+
array = array.reshape(-1, seqlen)
|
| 384 |
+
return cls(array)
|
| 385 |
+
|
| 386 |
+
@property
|
| 387 |
+
def depth(self) -> int:
|
| 388 |
+
return self.array.shape[0]
|
| 389 |
+
|
| 390 |
+
@property
|
| 391 |
+
def seqlen(self) -> int:
|
| 392 |
+
return self.array.shape[1]
|
| 393 |
+
|
| 394 |
+
def __len__(self):
|
| 395 |
+
return self.seqlen
|
| 396 |
+
|
| 397 |
+
def __getitem__(self, indices: int | list[int] | slice | np.ndarray):
|
| 398 |
+
if isinstance(indices, int):
|
| 399 |
+
indices = [indices]
|
| 400 |
+
|
| 401 |
+
return dataclasses.replace(self, array=self.array[:, indices])
|
| 402 |
+
|
| 403 |
+
def select_sequences(self, indices: Sequence[int] | np.ndarray) -> FastMSA:
|
| 404 |
+
"""Subselect rows of the MSA."""
|
| 405 |
+
array = self.array[indices]
|
| 406 |
+
headers = (
|
| 407 |
+
[self.headers[idx] for idx in indices] if self.headers is not None else None
|
| 408 |
+
)
|
| 409 |
+
return dataclasses.replace(self, array=array, headers=headers)
|
| 410 |
+
|
| 411 |
+
def select_random_sequences(self, num_seqs: int) -> FastMSA:
|
| 412 |
+
"""Uses random sampling to subselect sequences from the MSA. Always
|
| 413 |
+
keeps the query sequence.
|
| 414 |
+
"""
|
| 415 |
+
if num_seqs >= self.depth:
|
| 416 |
+
return self
|
| 417 |
+
|
| 418 |
+
# Subselect random, always keeping the query sequence.
|
| 419 |
+
indices = np.sort(
|
| 420 |
+
np.append(
|
| 421 |
+
0, np.random.choice(self.depth - 1, num_seqs - 1, replace=False) + 1
|
| 422 |
+
)
|
| 423 |
+
)
|
| 424 |
+
msa = self.select_sequences(indices) # type: ignore
|
| 425 |
+
return msa
|
| 426 |
+
|
| 427 |
+
def pad_to_depth(self, depth: int) -> FastMSA:
|
| 428 |
+
if depth < self.depth:
|
| 429 |
+
raise ValueError(f"Cannot pad to depth {depth} when depth is {self.depth}")
|
| 430 |
+
elif depth == self.depth:
|
| 431 |
+
return self
|
| 432 |
+
|
| 433 |
+
num_to_add = depth - self.depth
|
| 434 |
+
array = np.pad(
|
| 435 |
+
self.array,
|
| 436 |
+
[(0, num_to_add), (0, 0)],
|
| 437 |
+
constant_values=ord("-") if self.array.dtype == np.uint8 else b"-",
|
| 438 |
+
)
|
| 439 |
+
headers = self.headers
|
| 440 |
+
if headers is not None:
|
| 441 |
+
headers = headers + [""] * num_to_add
|
| 442 |
+
return dataclasses.replace(self, array=array, headers=headers)
|
| 443 |
+
|
| 444 |
+
@classmethod
|
| 445 |
+
def concat(
|
| 446 |
+
cls,
|
| 447 |
+
msas: Sequence[FastMSA],
|
| 448 |
+
join_token: str | None = None,
|
| 449 |
+
allow_depth_mismatch: bool = False,
|
| 450 |
+
) -> FastMSA:
|
| 451 |
+
"""Concatenate a series of MSAs horizontally, along the sequence dimension."""
|
| 452 |
+
if not msas:
|
| 453 |
+
raise ValueError("Cannot concatenate an empty list of MSAs")
|
| 454 |
+
if join_token is not None and join_token != "":
|
| 455 |
+
raise NotImplementedError("join_token is not supported for FastMSA")
|
| 456 |
+
|
| 457 |
+
msa_depths = [msa.depth for msa in msas]
|
| 458 |
+
if len(set(msa_depths)) != 1:
|
| 459 |
+
if not allow_depth_mismatch:
|
| 460 |
+
raise ValueError("Depth mismatch in concatenating MSAs")
|
| 461 |
+
else:
|
| 462 |
+
max_depth = max(msa_depths)
|
| 463 |
+
msas = [msa.pad_to_depth(max_depth) for msa in msas]
|
| 464 |
+
headers = [
|
| 465 |
+
"|".join([str(h) for h in headers])
|
| 466 |
+
for headers in zip(
|
| 467 |
+
*(
|
| 468 |
+
msa.headers if msa.headers is not None else [""] * msa.depth
|
| 469 |
+
for msa in msas
|
| 470 |
+
)
|
| 471 |
+
)
|
| 472 |
+
]
|
| 473 |
+
|
| 474 |
+
array = np.concatenate([msa.array for msa in msas], axis=1)
|
| 475 |
+
return cls(array, headers)
|
| 476 |
+
|
| 477 |
+
def to_msa(self) -> MSA:
|
| 478 |
+
headers = (
|
| 479 |
+
self.headers
|
| 480 |
+
if self.headers is not None
|
| 481 |
+
else [f"seq{i}" for i in range(self.depth)]
|
| 482 |
+
)
|
| 483 |
+
entries = [
|
| 484 |
+
FastaEntry(header, b"".join(row).decode())
|
| 485 |
+
for header, row in zip(headers, self.array)
|
| 486 |
+
]
|
| 487 |
+
return MSA(entries)
|
| 488 |
+
|
| 489 |
+
@classmethod
|
| 490 |
+
def stack(
|
| 491 |
+
cls, msas: Sequence[FastMSA], remove_query_from_later_msas: bool = True
|
| 492 |
+
) -> FastMSA:
|
| 493 |
+
"""Stack a series of MSAs. Optionally remove the query from msas after the first."""
|
| 494 |
+
arrays = []
|
| 495 |
+
all_headers = []
|
| 496 |
+
for i, msa in enumerate(msas):
|
| 497 |
+
array = msa.array
|
| 498 |
+
headers = msa.headers
|
| 499 |
+
if i > 0 and remove_query_from_later_msas:
|
| 500 |
+
array = array[1:]
|
| 501 |
+
if headers is not None:
|
| 502 |
+
headers = headers[1:]
|
| 503 |
+
arrays.append(array)
|
| 504 |
+
if headers is not None:
|
| 505 |
+
all_headers.extend(headers)
|
| 506 |
+
return cls(np.concatenate(arrays, axis=0), all_headers)
|
esmfold2_msa_filter_sequences.py
CHANGED
|
@@ -1,82 +1,82 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import tempfile
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
|
| 5 |
-
import numpy as np
|
| 6 |
-
from scipy.spatial.distance import cdist
|
| 7 |
-
|
| 8 |
-
from .esmfold2_system import run_subprocess_with_errorcheck
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def greedy_select_indices(array, num_seqs: int, mode: str = "max") -> list[int]:
|
| 12 |
-
"""Greedily select sequences that either maximize or minimize hamming distance.
|
| 13 |
-
|
| 14 |
-
Algorithm proposed in the MSA Transformer paper. Starting from the query sequence,
|
| 15 |
-
iteratively add sequences to the list with the maximum (minimum) average Hamming
|
| 16 |
-
distance to the existing set of sequences.
|
| 17 |
-
|
| 18 |
-
Args:
|
| 19 |
-
array (np.ndarray): Character array representing the sequences in the MSA
|
| 20 |
-
num_seqs (int): Number of sequences to select.
|
| 21 |
-
mode (str): Whether to maximize or minimize diversity. DO NOT pick 'min' unless
|
| 22 |
-
you're doing it to prove a point for a paper.
|
| 23 |
-
|
| 24 |
-
Returns:
|
| 25 |
-
list[int]: List of indices to select from the array
|
| 26 |
-
"""
|
| 27 |
-
assert mode in ("max", "min")
|
| 28 |
-
depth = array.shape[0]
|
| 29 |
-
if depth <= num_seqs:
|
| 30 |
-
return list(range(depth))
|
| 31 |
-
array = array.view(np.uint8)
|
| 32 |
-
|
| 33 |
-
optfunc = np.argmax if mode == "max" else np.argmin
|
| 34 |
-
all_indices = np.arange(depth)
|
| 35 |
-
indices = [0]
|
| 36 |
-
pairwise_distances = np.zeros((0, depth))
|
| 37 |
-
for _ in range(num_seqs - 1):
|
| 38 |
-
dist = cdist(array[indices[-1:]], array, "hamming")
|
| 39 |
-
pairwise_distances = np.concatenate([pairwise_distances, dist])
|
| 40 |
-
shifted_distance = np.delete(pairwise_distances, indices, axis=1).mean(0)
|
| 41 |
-
shifted_index = optfunc(shifted_distance)
|
| 42 |
-
index = np.delete(all_indices, indices)[shifted_index]
|
| 43 |
-
indices.append(index)
|
| 44 |
-
indices = sorted(indices)
|
| 45 |
-
return indices
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def hhfilter(
|
| 49 |
-
sequences: list[str],
|
| 50 |
-
seqid: int = 90,
|
| 51 |
-
diff: int = 0,
|
| 52 |
-
cov: int = 0,
|
| 53 |
-
qid: int = 0,
|
| 54 |
-
qsc: float = -20.0,
|
| 55 |
-
binary: str = "hhfilter",
|
| 56 |
-
) -> list[int]:
|
| 57 |
-
with tempfile.TemporaryDirectory(
|
| 58 |
-
dir="/dev/shm" if os.path.exists("/dev/shm") else None
|
| 59 |
-
) as tempdirname:
|
| 60 |
-
tempdir = Path(tempdirname)
|
| 61 |
-
fasta_file = tempdir / "input.fasta"
|
| 62 |
-
fasta_file.write_text(
|
| 63 |
-
"\n".join(f">{i}\n{seq}" for i, seq in enumerate(sequences))
|
| 64 |
-
)
|
| 65 |
-
output_file = tempdir / "output.fasta"
|
| 66 |
-
command = " ".join(
|
| 67 |
-
[
|
| 68 |
-
f"{binary}",
|
| 69 |
-
f"-i {fasta_file}",
|
| 70 |
-
"-M a3m",
|
| 71 |
-
f"-o {output_file}",
|
| 72 |
-
f"-id {seqid}",
|
| 73 |
-
f"-diff {diff}",
|
| 74 |
-
f"-cov {cov}",
|
| 75 |
-
f"-qid {qid}",
|
| 76 |
-
f"-qsc {qsc}",
|
| 77 |
-
]
|
| 78 |
-
).split(" ")
|
| 79 |
-
run_subprocess_with_errorcheck(command, capture_output=True)
|
| 80 |
-
with output_file.open() as f:
|
| 81 |
-
indices = [int(line[1:].strip()) for line in f if line.startswith(">")]
|
| 82 |
-
return indices
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
from scipy.spatial.distance import cdist
|
| 7 |
+
|
| 8 |
+
from .esmfold2_system import run_subprocess_with_errorcheck
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def greedy_select_indices(array, num_seqs: int, mode: str = "max") -> list[int]:
|
| 12 |
+
"""Greedily select sequences that either maximize or minimize hamming distance.
|
| 13 |
+
|
| 14 |
+
Algorithm proposed in the MSA Transformer paper. Starting from the query sequence,
|
| 15 |
+
iteratively add sequences to the list with the maximum (minimum) average Hamming
|
| 16 |
+
distance to the existing set of sequences.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
array (np.ndarray): Character array representing the sequences in the MSA
|
| 20 |
+
num_seqs (int): Number of sequences to select.
|
| 21 |
+
mode (str): Whether to maximize or minimize diversity. DO NOT pick 'min' unless
|
| 22 |
+
you're doing it to prove a point for a paper.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
list[int]: List of indices to select from the array
|
| 26 |
+
"""
|
| 27 |
+
assert mode in ("max", "min")
|
| 28 |
+
depth = array.shape[0]
|
| 29 |
+
if depth <= num_seqs:
|
| 30 |
+
return list(range(depth))
|
| 31 |
+
array = array.view(np.uint8)
|
| 32 |
+
|
| 33 |
+
optfunc = np.argmax if mode == "max" else np.argmin
|
| 34 |
+
all_indices = np.arange(depth)
|
| 35 |
+
indices = [0]
|
| 36 |
+
pairwise_distances = np.zeros((0, depth))
|
| 37 |
+
for _ in range(num_seqs - 1):
|
| 38 |
+
dist = cdist(array[indices[-1:]], array, "hamming")
|
| 39 |
+
pairwise_distances = np.concatenate([pairwise_distances, dist])
|
| 40 |
+
shifted_distance = np.delete(pairwise_distances, indices, axis=1).mean(0)
|
| 41 |
+
shifted_index = optfunc(shifted_distance)
|
| 42 |
+
index = np.delete(all_indices, indices)[shifted_index]
|
| 43 |
+
indices.append(index)
|
| 44 |
+
indices = sorted(indices)
|
| 45 |
+
return indices
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def hhfilter(
|
| 49 |
+
sequences: list[str],
|
| 50 |
+
seqid: int = 90,
|
| 51 |
+
diff: int = 0,
|
| 52 |
+
cov: int = 0,
|
| 53 |
+
qid: int = 0,
|
| 54 |
+
qsc: float = -20.0,
|
| 55 |
+
binary: str = "hhfilter",
|
| 56 |
+
) -> list[int]:
|
| 57 |
+
with tempfile.TemporaryDirectory(
|
| 58 |
+
dir="/dev/shm" if os.path.exists("/dev/shm") else None
|
| 59 |
+
) as tempdirname:
|
| 60 |
+
tempdir = Path(tempdirname)
|
| 61 |
+
fasta_file = tempdir / "input.fasta"
|
| 62 |
+
fasta_file.write_text(
|
| 63 |
+
"\n".join(f">{i}\n{seq}" for i, seq in enumerate(sequences))
|
| 64 |
+
)
|
| 65 |
+
output_file = tempdir / "output.fasta"
|
| 66 |
+
command = " ".join(
|
| 67 |
+
[
|
| 68 |
+
f"{binary}",
|
| 69 |
+
f"-i {fasta_file}",
|
| 70 |
+
"-M a3m",
|
| 71 |
+
f"-o {output_file}",
|
| 72 |
+
f"-id {seqid}",
|
| 73 |
+
f"-diff {diff}",
|
| 74 |
+
f"-cov {cov}",
|
| 75 |
+
f"-qid {qid}",
|
| 76 |
+
f"-qsc {qsc}",
|
| 77 |
+
]
|
| 78 |
+
).split(" ")
|
| 79 |
+
run_subprocess_with_errorcheck(command, capture_output=True)
|
| 80 |
+
with output_file.open() as f:
|
| 81 |
+
indices = [int(line[1:].strip()) for line in f if line.startswith(">")]
|
| 82 |
+
return indices
|
esmfold2_normalize_coordinates.py
CHANGED
|
@@ -1,79 +1,79 @@
|
|
| 1 |
-
from typing import TypeVar
|
| 2 |
-
|
| 3 |
-
import numpy as np
|
| 4 |
-
import torch
|
| 5 |
-
from torch import Tensor
|
| 6 |
-
|
| 7 |
-
from . import esmfold2_residue_constants as RC
|
| 8 |
-
from .esmfold2_affine3d import Affine3D
|
| 9 |
-
|
| 10 |
-
ArrayOrTensor = TypeVar("ArrayOrTensor", np.ndarray, Tensor)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def atom3_to_backbone_frames(bb_positions: torch.Tensor) -> Affine3D:
|
| 14 |
-
N, CA, C = bb_positions.unbind(dim=-2)
|
| 15 |
-
return Affine3D.from_graham_schmidt(C, CA, N)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def index_by_atom_name(
|
| 19 |
-
atom37: ArrayOrTensor, atom_names: str | list[str], dim: int = -2
|
| 20 |
-
) -> ArrayOrTensor:
|
| 21 |
-
squeeze = False
|
| 22 |
-
if isinstance(atom_names, str):
|
| 23 |
-
atom_names = [atom_names]
|
| 24 |
-
squeeze = True
|
| 25 |
-
indices = [RC.atom_order[atom_name] for atom_name in atom_names]
|
| 26 |
-
dim = dim % atom37.ndim
|
| 27 |
-
index = tuple(slice(None) if dim != i else indices for i in range(atom37.ndim))
|
| 28 |
-
result = atom37[index] # type: ignore
|
| 29 |
-
if squeeze:
|
| 30 |
-
result = result.squeeze(dim)
|
| 31 |
-
return result
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def get_protein_normalization_frame(coords: Tensor) -> Affine3D:
|
| 35 |
-
"""Given a set of coordinates for a protein, compute a single frame that can be used to normalize the coordinates.
|
| 36 |
-
Specifically, we compute the average position of the N, CA, and C atoms use those 3 points to construct a frame
|
| 37 |
-
using the Gram-Schmidt algorithm. The average CA position is used as the origin of the frame.
|
| 38 |
-
|
| 39 |
-
Args:
|
| 40 |
-
coords (torch.FloatTensor): [L, 37, 3] tensor of coordinates
|
| 41 |
-
|
| 42 |
-
Returns:
|
| 43 |
-
Affine3D: tensor of Affine3D frame
|
| 44 |
-
"""
|
| 45 |
-
bb_coords = index_by_atom_name(coords, ["N", "CA", "C"], dim=-2)
|
| 46 |
-
coord_mask = torch.all(torch.all(torch.isfinite(bb_coords), dim=-1), dim=-1)
|
| 47 |
-
|
| 48 |
-
average_position_per_n_ca_c = bb_coords.masked_fill(
|
| 49 |
-
~coord_mask[..., None, None], 0
|
| 50 |
-
).sum(-3) / (coord_mask.sum(-1)[..., None, None] + 1e-8)
|
| 51 |
-
frame = atom3_to_backbone_frames(average_position_per_n_ca_c.float())
|
| 52 |
-
|
| 53 |
-
return frame
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def apply_frame_to_coords(coords: Tensor, frame: Affine3D) -> Tensor:
|
| 57 |
-
"""Given a set of coordinates and a single frame, apply the frame to the coordinates.
|
| 58 |
-
|
| 59 |
-
Args:
|
| 60 |
-
coords (torch.FloatTensor): [L, 37, 3] tensor of coordinates
|
| 61 |
-
frame (Affine3D): Affine3D frame
|
| 62 |
-
|
| 63 |
-
Returns:
|
| 64 |
-
torch.FloatTensor: [L, 37, 3] tensor of transformed coordinates
|
| 65 |
-
"""
|
| 66 |
-
coords_trans_rot = frame[..., None, None].invert().apply(coords)
|
| 67 |
-
|
| 68 |
-
# only transform coordinates with frame that have a valid rotation
|
| 69 |
-
valid_frame = frame.trans.norm(dim=-1) > 0
|
| 70 |
-
|
| 71 |
-
is_inf = torch.isinf(coords)
|
| 72 |
-
coords = coords_trans_rot.where(valid_frame[..., None, None, None], coords)
|
| 73 |
-
coords.masked_fill_(is_inf, torch.inf)
|
| 74 |
-
|
| 75 |
-
return coords
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def normalize_coordinates(coords: Tensor) -> Tensor:
|
| 79 |
-
return apply_frame_to_coords(coords, get_protein_normalization_frame(coords))
|
|
|
|
| 1 |
+
from typing import TypeVar
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from torch import Tensor
|
| 6 |
+
|
| 7 |
+
from . import esmfold2_residue_constants as RC
|
| 8 |
+
from .esmfold2_affine3d import Affine3D
|
| 9 |
+
|
| 10 |
+
ArrayOrTensor = TypeVar("ArrayOrTensor", np.ndarray, Tensor)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def atom3_to_backbone_frames(bb_positions: torch.Tensor) -> Affine3D:
|
| 14 |
+
N, CA, C = bb_positions.unbind(dim=-2)
|
| 15 |
+
return Affine3D.from_graham_schmidt(C, CA, N)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def index_by_atom_name(
|
| 19 |
+
atom37: ArrayOrTensor, atom_names: str | list[str], dim: int = -2
|
| 20 |
+
) -> ArrayOrTensor:
|
| 21 |
+
squeeze = False
|
| 22 |
+
if isinstance(atom_names, str):
|
| 23 |
+
atom_names = [atom_names]
|
| 24 |
+
squeeze = True
|
| 25 |
+
indices = [RC.atom_order[atom_name] for atom_name in atom_names]
|
| 26 |
+
dim = dim % atom37.ndim
|
| 27 |
+
index = tuple(slice(None) if dim != i else indices for i in range(atom37.ndim))
|
| 28 |
+
result = atom37[index] # type: ignore
|
| 29 |
+
if squeeze:
|
| 30 |
+
result = result.squeeze(dim)
|
| 31 |
+
return result
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_protein_normalization_frame(coords: Tensor) -> Affine3D:
|
| 35 |
+
"""Given a set of coordinates for a protein, compute a single frame that can be used to normalize the coordinates.
|
| 36 |
+
Specifically, we compute the average position of the N, CA, and C atoms use those 3 points to construct a frame
|
| 37 |
+
using the Gram-Schmidt algorithm. The average CA position is used as the origin of the frame.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
coords (torch.FloatTensor): [L, 37, 3] tensor of coordinates
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
Affine3D: tensor of Affine3D frame
|
| 44 |
+
"""
|
| 45 |
+
bb_coords = index_by_atom_name(coords, ["N", "CA", "C"], dim=-2)
|
| 46 |
+
coord_mask = torch.all(torch.all(torch.isfinite(bb_coords), dim=-1), dim=-1)
|
| 47 |
+
|
| 48 |
+
average_position_per_n_ca_c = bb_coords.masked_fill(
|
| 49 |
+
~coord_mask[..., None, None], 0
|
| 50 |
+
).sum(-3) / (coord_mask.sum(-1)[..., None, None] + 1e-8)
|
| 51 |
+
frame = atom3_to_backbone_frames(average_position_per_n_ca_c.float())
|
| 52 |
+
|
| 53 |
+
return frame
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def apply_frame_to_coords(coords: Tensor, frame: Affine3D) -> Tensor:
|
| 57 |
+
"""Given a set of coordinates and a single frame, apply the frame to the coordinates.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
coords (torch.FloatTensor): [L, 37, 3] tensor of coordinates
|
| 61 |
+
frame (Affine3D): Affine3D frame
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
torch.FloatTensor: [L, 37, 3] tensor of transformed coordinates
|
| 65 |
+
"""
|
| 66 |
+
coords_trans_rot = frame[..., None, None].invert().apply(coords)
|
| 67 |
+
|
| 68 |
+
# only transform coordinates with frame that have a valid rotation
|
| 69 |
+
valid_frame = frame.trans.norm(dim=-1) > 0
|
| 70 |
+
|
| 71 |
+
is_inf = torch.isinf(coords)
|
| 72 |
+
coords = coords_trans_rot.where(valid_frame[..., None, None, None], coords)
|
| 73 |
+
coords.masked_fill_(is_inf, torch.inf)
|
| 74 |
+
|
| 75 |
+
return coords
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def normalize_coordinates(coords: Tensor) -> Tensor:
|
| 79 |
+
return apply_frame_to_coords(coords, get_protein_normalization_frame(coords))
|
esmfold2_output.py
CHANGED
|
@@ -1,224 +1,224 @@
|
|
| 1 |
-
from itertools import groupby
|
| 2 |
-
from typing import Any
|
| 3 |
-
|
| 4 |
-
import numpy as np
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from .esmfold2_constants import ELEMENT_NUMBER_TO_SYMBOL, MOL_TYPE_NONPOLYMER
|
| 8 |
-
from .esmfold2_molecular_complex import (
|
| 9 |
-
MolecularComplex,
|
| 10 |
-
MolecularComplexMetadata,
|
| 11 |
-
)
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def get_element_symbol(atomic_num: int) -> str:
|
| 15 |
-
return ELEMENT_NUMBER_TO_SYMBOL.get(atomic_num, "X")
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def build_molecular_complex_from_features(
|
| 19 |
-
coords: torch.Tensor,
|
| 20 |
-
plddt: torch.Tensor,
|
| 21 |
-
atom_mask: torch.Tensor,
|
| 22 |
-
ref_element: torch.Tensor,
|
| 23 |
-
ref_atom_name_chars: torch.Tensor,
|
| 24 |
-
chain_infos: list,
|
| 25 |
-
complex_id: str,
|
| 26 |
-
) -> MolecularComplex:
|
| 27 |
-
"""Construct a MolecularComplex from feature-dict tensors and chain metadata.
|
| 28 |
-
|
| 29 |
-
Non-polymer chains (ligands) collapse all per-atom tokens into a single
|
| 30 |
-
residue token whose pLDDT is the per-token average and whose hetero flag
|
| 31 |
-
is True.
|
| 32 |
-
"""
|
| 33 |
-
mask_np = atom_mask.bool().cpu().numpy()
|
| 34 |
-
coords_np = coords.float().cpu().numpy()
|
| 35 |
-
name_chars_np = ref_atom_name_chars.cpu().numpy()
|
| 36 |
-
elements_np = ref_element.cpu().numpy()
|
| 37 |
-
plddt_np = plddt.float().cpu().numpy()
|
| 38 |
-
|
| 39 |
-
sequence_tokens: list[str] = []
|
| 40 |
-
chain_ids_per_token: list[int] = []
|
| 41 |
-
token_to_atoms: list[list[int]] = []
|
| 42 |
-
confidence: list[float] = []
|
| 43 |
-
flat_positions: list[list[float]] = []
|
| 44 |
-
flat_elements: list[str] = []
|
| 45 |
-
flat_names: list[str] = []
|
| 46 |
-
flat_hetero: list[bool] = []
|
| 47 |
-
|
| 48 |
-
chain_lookup: dict[int, str] = {}
|
| 49 |
-
entity_info: dict[int, str] = {}
|
| 50 |
-
out_atom_cursor = 0
|
| 51 |
-
|
| 52 |
-
for ci in chain_infos:
|
| 53 |
-
chain_lookup[ci.asym_id] = ci.chain_id
|
| 54 |
-
is_nonpolymer = ci.mol_type == MOL_TYPE_NONPOLYMER
|
| 55 |
-
entity_info[ci.entity_id] = "non-polymer" if is_nonpolymer else "polymer"
|
| 56 |
-
|
| 57 |
-
if is_nonpolymer:
|
| 58 |
-
residue_name = ci.tokens[0].residue_name if ci.tokens else "LIG"
|
| 59 |
-
sequence_tokens.append(residue_name)
|
| 60 |
-
chain_ids_per_token.append(ci.asym_id)
|
| 61 |
-
avg_plddt = (
|
| 62 |
-
float(np.mean([plddt_np[ti.token_index] for ti in ci.tokens]))
|
| 63 |
-
if ci.tokens
|
| 64 |
-
else 0.0
|
| 65 |
-
)
|
| 66 |
-
confidence.append(avg_plddt)
|
| 67 |
-
token_atom_start = out_atom_cursor
|
| 68 |
-
for ti in ci.tokens:
|
| 69 |
-
for atom_idx in range(ti.atom_start, ti.atom_start + ti.atom_count):
|
| 70 |
-
if not mask_np[atom_idx]:
|
| 71 |
-
continue
|
| 72 |
-
flat_positions.append(coords_np[atom_idx].tolist())
|
| 73 |
-
flat_elements.append(get_element_symbol(int(elements_np[atom_idx])))
|
| 74 |
-
chars = name_chars_np[atom_idx]
|
| 75 |
-
name = "".join(
|
| 76 |
-
chr(int(c) + 32) for c in chars if int(c) != 0
|
| 77 |
-
).strip()
|
| 78 |
-
flat_names.append(name)
|
| 79 |
-
flat_hetero.append(True)
|
| 80 |
-
out_atom_cursor += 1
|
| 81 |
-
token_to_atoms.append([token_atom_start, out_atom_cursor])
|
| 82 |
-
continue
|
| 83 |
-
|
| 84 |
-
# Atom-tokenized modified residues (HYP, MSE, ...) span multiple
|
| 85 |
-
# tokens per residue; collapse them back to one mmCIF residue.
|
| 86 |
-
for _residue_index, ti_iter in groupby(
|
| 87 |
-
ci.tokens, key=lambda t: t.residue_index
|
| 88 |
-
):
|
| 89 |
-
ti_group = list(ti_iter)
|
| 90 |
-
sequence_tokens.append(ti_group[0].residue_name)
|
| 91 |
-
chain_ids_per_token.append(ci.asym_id)
|
| 92 |
-
confidence.append(
|
| 93 |
-
float(np.mean([plddt_np[ti.token_index] for ti in ti_group]))
|
| 94 |
-
)
|
| 95 |
-
token_atom_start = out_atom_cursor
|
| 96 |
-
for ti in ti_group:
|
| 97 |
-
for atom_idx in range(ti.atom_start, ti.atom_start + ti.atom_count):
|
| 98 |
-
if not mask_np[atom_idx]:
|
| 99 |
-
continue
|
| 100 |
-
flat_positions.append(coords_np[atom_idx].tolist())
|
| 101 |
-
flat_elements.append(get_element_symbol(int(elements_np[atom_idx])))
|
| 102 |
-
chars = name_chars_np[atom_idx]
|
| 103 |
-
name = "".join(
|
| 104 |
-
chr(int(c) + 32) for c in chars if int(c) != 0
|
| 105 |
-
).strip()
|
| 106 |
-
flat_names.append(name)
|
| 107 |
-
flat_hetero.append(False)
|
| 108 |
-
out_atom_cursor += 1
|
| 109 |
-
token_to_atoms.append([token_atom_start, out_atom_cursor])
|
| 110 |
-
|
| 111 |
-
return MolecularComplex(
|
| 112 |
-
id=complex_id,
|
| 113 |
-
sequence=sequence_tokens,
|
| 114 |
-
atom_positions=np.array(flat_positions, dtype=np.float32).reshape(-1, 3),
|
| 115 |
-
atom_elements=np.array(flat_elements, dtype=object),
|
| 116 |
-
token_to_atoms=np.array(token_to_atoms, dtype=np.int32).reshape(-1, 2),
|
| 117 |
-
chain_id=np.array(chain_ids_per_token, dtype=np.int64),
|
| 118 |
-
plddt=np.array(confidence, dtype=np.float32),
|
| 119 |
-
atom_names=np.array(flat_names, dtype=object),
|
| 120 |
-
atom_hetero=np.array(flat_hetero, dtype=bool),
|
| 121 |
-
metadata=MolecularComplexMetadata(
|
| 122 |
-
entity_lookup=entity_info,
|
| 123 |
-
chain_lookup=chain_lookup,
|
| 124 |
-
assembly_composition=None,
|
| 125 |
-
),
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def build_molecular_complex(
|
| 130 |
-
structure: Any, coords: torch.Tensor, plddt: torch.Tensor, complex_id: str
|
| 131 |
-
) -> MolecularComplex:
|
| 132 |
-
"""Directly constructs a MolecularComplex from model outputs without intermediate files.
|
| 133 |
-
|
| 134 |
-
Args:
|
| 135 |
-
structure: Object with .chains, .residues, .atoms numpy structured arrays.
|
| 136 |
-
coords: [N_atoms, 3] predicted atom coordinates.
|
| 137 |
-
plddt: [N_residues] per-residue confidence scores.
|
| 138 |
-
complex_id: Identifier string for the resulting complex.
|
| 139 |
-
"""
|
| 140 |
-
flat_positions = []
|
| 141 |
-
flat_elements = []
|
| 142 |
-
flat_names = []
|
| 143 |
-
flat_hetero = []
|
| 144 |
-
|
| 145 |
-
sequence_tokens = []
|
| 146 |
-
token_to_atoms = []
|
| 147 |
-
chain_ids_per_token = []
|
| 148 |
-
confidence_scores = []
|
| 149 |
-
|
| 150 |
-
chain_lookup = {}
|
| 151 |
-
entity_info = {}
|
| 152 |
-
|
| 153 |
-
global_atom_cursor = 0
|
| 154 |
-
global_res_cursor = 0
|
| 155 |
-
atom_array_idx = 0
|
| 156 |
-
|
| 157 |
-
for chain in structure.chains:
|
| 158 |
-
chain_idx_numeric = chain["asym_id"]
|
| 159 |
-
chain_name_str = str(chain["name"])
|
| 160 |
-
mol_type = chain["mol_type"]
|
| 161 |
-
|
| 162 |
-
chain_lookup[chain_idx_numeric] = chain_name_str
|
| 163 |
-
entity_info[chain["entity_id"]] = (
|
| 164 |
-
"polymer" if mol_type != MOL_TYPE_NONPOLYMER else "non-polymer"
|
| 165 |
-
)
|
| 166 |
-
|
| 167 |
-
res_start = chain["res_idx"]
|
| 168 |
-
res_end = chain["res_idx"] + chain["res_num"]
|
| 169 |
-
residues = structure.residues[res_start:res_end]
|
| 170 |
-
|
| 171 |
-
for residue in residues:
|
| 172 |
-
res_name = str(residue["name"])
|
| 173 |
-
|
| 174 |
-
sequence_tokens.append(res_name)
|
| 175 |
-
chain_ids_per_token.append(chain_idx_numeric)
|
| 176 |
-
|
| 177 |
-
score = plddt[global_res_cursor].item()
|
| 178 |
-
confidence_scores.append(score)
|
| 179 |
-
token_start_idx = atom_array_idx
|
| 180 |
-
|
| 181 |
-
atom_start = residue["atom_idx"]
|
| 182 |
-
atom_end = residue["atom_idx"] + residue["atom_num"]
|
| 183 |
-
atoms = structure.atoms[atom_start:atom_end]
|
| 184 |
-
|
| 185 |
-
for atom in atoms:
|
| 186 |
-
if not atom["is_present"]:
|
| 187 |
-
continue
|
| 188 |
-
|
| 189 |
-
pos = coords[global_atom_cursor].tolist()
|
| 190 |
-
flat_positions.append(pos)
|
| 191 |
-
|
| 192 |
-
elem = get_element_symbol(atom["element"].item())
|
| 193 |
-
flat_elements.append(elem)
|
| 194 |
-
|
| 195 |
-
raw_name = atom["name"]
|
| 196 |
-
if hasattr(raw_name, "tolist"):
|
| 197 |
-
raw_name = raw_name.tolist()
|
| 198 |
-
name_str = "".join([chr(c + 32) for c in raw_name if c != 0])
|
| 199 |
-
flat_names.append(name_str)
|
| 200 |
-
|
| 201 |
-
flat_hetero.append(mol_type == MOL_TYPE_NONPOLYMER)
|
| 202 |
-
|
| 203 |
-
global_atom_cursor += 1
|
| 204 |
-
atom_array_idx += 1
|
| 205 |
-
|
| 206 |
-
token_to_atoms.append([token_start_idx, atom_array_idx])
|
| 207 |
-
global_res_cursor += 1
|
| 208 |
-
|
| 209 |
-
return MolecularComplex(
|
| 210 |
-
id=complex_id,
|
| 211 |
-
sequence=sequence_tokens,
|
| 212 |
-
atom_positions=np.array(flat_positions, dtype=np.float32),
|
| 213 |
-
atom_elements=np.array(flat_elements, dtype=object),
|
| 214 |
-
token_to_atoms=np.array(token_to_atoms, dtype=np.int32),
|
| 215 |
-
chain_id=np.array(chain_ids_per_token, dtype=np.int64),
|
| 216 |
-
plddt=np.array(confidence_scores, dtype=np.float32),
|
| 217 |
-
atom_names=np.array(flat_names, dtype=object),
|
| 218 |
-
atom_hetero=np.array(flat_hetero, dtype=bool),
|
| 219 |
-
metadata=MolecularComplexMetadata(
|
| 220 |
-
entity_lookup=entity_info,
|
| 221 |
-
chain_lookup=chain_lookup,
|
| 222 |
-
assembly_composition=None,
|
| 223 |
-
),
|
| 224 |
-
)
|
|
|
|
| 1 |
+
from itertools import groupby
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from .esmfold2_constants import ELEMENT_NUMBER_TO_SYMBOL, MOL_TYPE_NONPOLYMER
|
| 8 |
+
from .esmfold2_molecular_complex import (
|
| 9 |
+
MolecularComplex,
|
| 10 |
+
MolecularComplexMetadata,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_element_symbol(atomic_num: int) -> str:
|
| 15 |
+
return ELEMENT_NUMBER_TO_SYMBOL.get(atomic_num, "X")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def build_molecular_complex_from_features(
|
| 19 |
+
coords: torch.Tensor,
|
| 20 |
+
plddt: torch.Tensor,
|
| 21 |
+
atom_mask: torch.Tensor,
|
| 22 |
+
ref_element: torch.Tensor,
|
| 23 |
+
ref_atom_name_chars: torch.Tensor,
|
| 24 |
+
chain_infos: list,
|
| 25 |
+
complex_id: str,
|
| 26 |
+
) -> MolecularComplex:
|
| 27 |
+
"""Construct a MolecularComplex from feature-dict tensors and chain metadata.
|
| 28 |
+
|
| 29 |
+
Non-polymer chains (ligands) collapse all per-atom tokens into a single
|
| 30 |
+
residue token whose pLDDT is the per-token average and whose hetero flag
|
| 31 |
+
is True.
|
| 32 |
+
"""
|
| 33 |
+
mask_np = atom_mask.bool().cpu().numpy()
|
| 34 |
+
coords_np = coords.float().cpu().numpy()
|
| 35 |
+
name_chars_np = ref_atom_name_chars.cpu().numpy()
|
| 36 |
+
elements_np = ref_element.cpu().numpy()
|
| 37 |
+
plddt_np = plddt.float().cpu().numpy()
|
| 38 |
+
|
| 39 |
+
sequence_tokens: list[str] = []
|
| 40 |
+
chain_ids_per_token: list[int] = []
|
| 41 |
+
token_to_atoms: list[list[int]] = []
|
| 42 |
+
confidence: list[float] = []
|
| 43 |
+
flat_positions: list[list[float]] = []
|
| 44 |
+
flat_elements: list[str] = []
|
| 45 |
+
flat_names: list[str] = []
|
| 46 |
+
flat_hetero: list[bool] = []
|
| 47 |
+
|
| 48 |
+
chain_lookup: dict[int, str] = {}
|
| 49 |
+
entity_info: dict[int, str] = {}
|
| 50 |
+
out_atom_cursor = 0
|
| 51 |
+
|
| 52 |
+
for ci in chain_infos:
|
| 53 |
+
chain_lookup[ci.asym_id] = ci.chain_id
|
| 54 |
+
is_nonpolymer = ci.mol_type == MOL_TYPE_NONPOLYMER
|
| 55 |
+
entity_info[ci.entity_id] = "non-polymer" if is_nonpolymer else "polymer"
|
| 56 |
+
|
| 57 |
+
if is_nonpolymer:
|
| 58 |
+
residue_name = ci.tokens[0].residue_name if ci.tokens else "LIG"
|
| 59 |
+
sequence_tokens.append(residue_name)
|
| 60 |
+
chain_ids_per_token.append(ci.asym_id)
|
| 61 |
+
avg_plddt = (
|
| 62 |
+
float(np.mean([plddt_np[ti.token_index] for ti in ci.tokens]))
|
| 63 |
+
if ci.tokens
|
| 64 |
+
else 0.0
|
| 65 |
+
)
|
| 66 |
+
confidence.append(avg_plddt)
|
| 67 |
+
token_atom_start = out_atom_cursor
|
| 68 |
+
for ti in ci.tokens:
|
| 69 |
+
for atom_idx in range(ti.atom_start, ti.atom_start + ti.atom_count):
|
| 70 |
+
if not mask_np[atom_idx]:
|
| 71 |
+
continue
|
| 72 |
+
flat_positions.append(coords_np[atom_idx].tolist())
|
| 73 |
+
flat_elements.append(get_element_symbol(int(elements_np[atom_idx])))
|
| 74 |
+
chars = name_chars_np[atom_idx]
|
| 75 |
+
name = "".join(
|
| 76 |
+
chr(int(c) + 32) for c in chars if int(c) != 0
|
| 77 |
+
).strip()
|
| 78 |
+
flat_names.append(name)
|
| 79 |
+
flat_hetero.append(True)
|
| 80 |
+
out_atom_cursor += 1
|
| 81 |
+
token_to_atoms.append([token_atom_start, out_atom_cursor])
|
| 82 |
+
continue
|
| 83 |
+
|
| 84 |
+
# Atom-tokenized modified residues (HYP, MSE, ...) span multiple
|
| 85 |
+
# tokens per residue; collapse them back to one mmCIF residue.
|
| 86 |
+
for _residue_index, ti_iter in groupby(
|
| 87 |
+
ci.tokens, key=lambda t: t.residue_index
|
| 88 |
+
):
|
| 89 |
+
ti_group = list(ti_iter)
|
| 90 |
+
sequence_tokens.append(ti_group[0].residue_name)
|
| 91 |
+
chain_ids_per_token.append(ci.asym_id)
|
| 92 |
+
confidence.append(
|
| 93 |
+
float(np.mean([plddt_np[ti.token_index] for ti in ti_group]))
|
| 94 |
+
)
|
| 95 |
+
token_atom_start = out_atom_cursor
|
| 96 |
+
for ti in ti_group:
|
| 97 |
+
for atom_idx in range(ti.atom_start, ti.atom_start + ti.atom_count):
|
| 98 |
+
if not mask_np[atom_idx]:
|
| 99 |
+
continue
|
| 100 |
+
flat_positions.append(coords_np[atom_idx].tolist())
|
| 101 |
+
flat_elements.append(get_element_symbol(int(elements_np[atom_idx])))
|
| 102 |
+
chars = name_chars_np[atom_idx]
|
| 103 |
+
name = "".join(
|
| 104 |
+
chr(int(c) + 32) for c in chars if int(c) != 0
|
| 105 |
+
).strip()
|
| 106 |
+
flat_names.append(name)
|
| 107 |
+
flat_hetero.append(False)
|
| 108 |
+
out_atom_cursor += 1
|
| 109 |
+
token_to_atoms.append([token_atom_start, out_atom_cursor])
|
| 110 |
+
|
| 111 |
+
return MolecularComplex(
|
| 112 |
+
id=complex_id,
|
| 113 |
+
sequence=sequence_tokens,
|
| 114 |
+
atom_positions=np.array(flat_positions, dtype=np.float32).reshape(-1, 3),
|
| 115 |
+
atom_elements=np.array(flat_elements, dtype=object),
|
| 116 |
+
token_to_atoms=np.array(token_to_atoms, dtype=np.int32).reshape(-1, 2),
|
| 117 |
+
chain_id=np.array(chain_ids_per_token, dtype=np.int64),
|
| 118 |
+
plddt=np.array(confidence, dtype=np.float32),
|
| 119 |
+
atom_names=np.array(flat_names, dtype=object),
|
| 120 |
+
atom_hetero=np.array(flat_hetero, dtype=bool),
|
| 121 |
+
metadata=MolecularComplexMetadata(
|
| 122 |
+
entity_lookup=entity_info,
|
| 123 |
+
chain_lookup=chain_lookup,
|
| 124 |
+
assembly_composition=None,
|
| 125 |
+
),
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def build_molecular_complex(
|
| 130 |
+
structure: Any, coords: torch.Tensor, plddt: torch.Tensor, complex_id: str
|
| 131 |
+
) -> MolecularComplex:
|
| 132 |
+
"""Directly constructs a MolecularComplex from model outputs without intermediate files.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
structure: Object with .chains, .residues, .atoms numpy structured arrays.
|
| 136 |
+
coords: [N_atoms, 3] predicted atom coordinates.
|
| 137 |
+
plddt: [N_residues] per-residue confidence scores.
|
| 138 |
+
complex_id: Identifier string for the resulting complex.
|
| 139 |
+
"""
|
| 140 |
+
flat_positions = []
|
| 141 |
+
flat_elements = []
|
| 142 |
+
flat_names = []
|
| 143 |
+
flat_hetero = []
|
| 144 |
+
|
| 145 |
+
sequence_tokens = []
|
| 146 |
+
token_to_atoms = []
|
| 147 |
+
chain_ids_per_token = []
|
| 148 |
+
confidence_scores = []
|
| 149 |
+
|
| 150 |
+
chain_lookup = {}
|
| 151 |
+
entity_info = {}
|
| 152 |
+
|
| 153 |
+
global_atom_cursor = 0
|
| 154 |
+
global_res_cursor = 0
|
| 155 |
+
atom_array_idx = 0
|
| 156 |
+
|
| 157 |
+
for chain in structure.chains:
|
| 158 |
+
chain_idx_numeric = chain["asym_id"]
|
| 159 |
+
chain_name_str = str(chain["name"])
|
| 160 |
+
mol_type = chain["mol_type"]
|
| 161 |
+
|
| 162 |
+
chain_lookup[chain_idx_numeric] = chain_name_str
|
| 163 |
+
entity_info[chain["entity_id"]] = (
|
| 164 |
+
"polymer" if mol_type != MOL_TYPE_NONPOLYMER else "non-polymer"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
res_start = chain["res_idx"]
|
| 168 |
+
res_end = chain["res_idx"] + chain["res_num"]
|
| 169 |
+
residues = structure.residues[res_start:res_end]
|
| 170 |
+
|
| 171 |
+
for residue in residues:
|
| 172 |
+
res_name = str(residue["name"])
|
| 173 |
+
|
| 174 |
+
sequence_tokens.append(res_name)
|
| 175 |
+
chain_ids_per_token.append(chain_idx_numeric)
|
| 176 |
+
|
| 177 |
+
score = plddt[global_res_cursor].item()
|
| 178 |
+
confidence_scores.append(score)
|
| 179 |
+
token_start_idx = atom_array_idx
|
| 180 |
+
|
| 181 |
+
atom_start = residue["atom_idx"]
|
| 182 |
+
atom_end = residue["atom_idx"] + residue["atom_num"]
|
| 183 |
+
atoms = structure.atoms[atom_start:atom_end]
|
| 184 |
+
|
| 185 |
+
for atom in atoms:
|
| 186 |
+
if not atom["is_present"]:
|
| 187 |
+
continue
|
| 188 |
+
|
| 189 |
+
pos = coords[global_atom_cursor].tolist()
|
| 190 |
+
flat_positions.append(pos)
|
| 191 |
+
|
| 192 |
+
elem = get_element_symbol(atom["element"].item())
|
| 193 |
+
flat_elements.append(elem)
|
| 194 |
+
|
| 195 |
+
raw_name = atom["name"]
|
| 196 |
+
if hasattr(raw_name, "tolist"):
|
| 197 |
+
raw_name = raw_name.tolist()
|
| 198 |
+
name_str = "".join([chr(c + 32) for c in raw_name if c != 0])
|
| 199 |
+
flat_names.append(name_str)
|
| 200 |
+
|
| 201 |
+
flat_hetero.append(mol_type == MOL_TYPE_NONPOLYMER)
|
| 202 |
+
|
| 203 |
+
global_atom_cursor += 1
|
| 204 |
+
atom_array_idx += 1
|
| 205 |
+
|
| 206 |
+
token_to_atoms.append([token_start_idx, atom_array_idx])
|
| 207 |
+
global_res_cursor += 1
|
| 208 |
+
|
| 209 |
+
return MolecularComplex(
|
| 210 |
+
id=complex_id,
|
| 211 |
+
sequence=sequence_tokens,
|
| 212 |
+
atom_positions=np.array(flat_positions, dtype=np.float32),
|
| 213 |
+
atom_elements=np.array(flat_elements, dtype=object),
|
| 214 |
+
token_to_atoms=np.array(token_to_atoms, dtype=np.int32),
|
| 215 |
+
chain_id=np.array(chain_ids_per_token, dtype=np.int64),
|
| 216 |
+
plddt=np.array(confidence_scores, dtype=np.float32),
|
| 217 |
+
atom_names=np.array(flat_names, dtype=object),
|
| 218 |
+
atom_hetero=np.array(flat_hetero, dtype=bool),
|
| 219 |
+
metadata=MolecularComplexMetadata(
|
| 220 |
+
entity_lookup=entity_info,
|
| 221 |
+
chain_lookup=chain_lookup,
|
| 222 |
+
assembly_composition=None,
|
| 223 |
+
),
|
| 224 |
+
)
|
esmfold2_paired_msa.py
CHANGED
|
@@ -1,245 +1,245 @@
|
|
| 1 |
-
"""Taxonomy-paired MSA construction for ESMFold2 inference.
|
| 2 |
-
|
| 3 |
-
Taxonomy IDs are read from FASTA headers as ``key=N`` tokens. Rows
|
| 4 |
-
where any chain has ``key=-1`` (or no ``key=`` at all) are treated as
|
| 5 |
-
unpaired and assigned to that chain's block-diagonal section after
|
| 6 |
-
the paired rows.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import re
|
| 10 |
-
|
| 11 |
-
import numpy as np
|
| 12 |
-
|
| 13 |
-
from .esmfold2_constants import (
|
| 14 |
-
MSA_GAP_TOKEN_ID,
|
| 15 |
-
PROTEIN_3TO1,
|
| 16 |
-
PROTEIN_RESIDUE_TO_RES_TYPE,
|
| 17 |
-
PROTEIN_UNK_RES_TYPE,
|
| 18 |
-
)
|
| 19 |
-
from .esmfold2_msa import MSA
|
| 20 |
-
|
| 21 |
-
_KEY_RE = re.compile(r"key=(-?\d+)")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def protein_letter_to_res_type() -> dict[str, int]:
|
| 25 |
-
"""Return the protein 1-letter → res_type mapping used by the MSA encoder."""
|
| 26 |
-
mapping: dict[str, int] = {}
|
| 27 |
-
for three, one in PROTEIN_3TO1.items():
|
| 28 |
-
if three in PROTEIN_RESIDUE_TO_RES_TYPE:
|
| 29 |
-
mapping[one] = PROTEIN_RESIDUE_TO_RES_TYPE[three]
|
| 30 |
-
mapping["-"] = MSA_GAP_TOKEN_ID
|
| 31 |
-
mapping["X"] = PROTEIN_UNK_RES_TYPE
|
| 32 |
-
return mapping
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _taxonomy_from_header(header: str) -> int:
|
| 36 |
-
if not header:
|
| 37 |
-
return -1
|
| 38 |
-
m = _KEY_RE.search(header)
|
| 39 |
-
return int(m.group(1)) if m else -1
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def msa_to_res_type_and_deletions(
|
| 43 |
-
msa: MSA, letter_to_res_type: dict[str, int]
|
| 44 |
-
) -> tuple[np.ndarray, np.ndarray]:
|
| 45 |
-
"""Convert an :class:`MSA` to ``(res_type[M, L], deletion_count[M, L])``.
|
| 46 |
-
|
| 47 |
-
Handles a3m insertion convention: lowercase letters and ``.`` are
|
| 48 |
-
insertions and are not emitted; their count is accumulated into the
|
| 49 |
-
next non-insertion position's deletion value. ``L`` is the query
|
| 50 |
-
length after stripping insertions from row 0.
|
| 51 |
-
"""
|
| 52 |
-
query = msa.entries[0].sequence
|
| 53 |
-
L = sum(1 for ch in query if not (ch.islower() or ch == "."))
|
| 54 |
-
M = msa.depth
|
| 55 |
-
|
| 56 |
-
res_type = np.full((M, L), MSA_GAP_TOKEN_ID, dtype=np.int64)
|
| 57 |
-
deletions = np.zeros((M, L), dtype=np.float32)
|
| 58 |
-
|
| 59 |
-
for r, entry in enumerate(msa.entries):
|
| 60 |
-
col = 0
|
| 61 |
-
ins = 0
|
| 62 |
-
for ch in entry.sequence:
|
| 63 |
-
if ch == "." or (ch.islower() and ch != "-"):
|
| 64 |
-
ins += 1
|
| 65 |
-
continue
|
| 66 |
-
if col >= L:
|
| 67 |
-
break
|
| 68 |
-
if ch == "-":
|
| 69 |
-
res_type[r, col] = MSA_GAP_TOKEN_ID
|
| 70 |
-
else:
|
| 71 |
-
res_type[r, col] = letter_to_res_type.get(
|
| 72 |
-
ch.upper(), PROTEIN_UNK_RES_TYPE
|
| 73 |
-
)
|
| 74 |
-
if ins > 0:
|
| 75 |
-
deletions[r, col] = float(ins)
|
| 76 |
-
ins = 0
|
| 77 |
-
col += 1
|
| 78 |
-
return res_type, deletions
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def _dummy_msa_residues(query_res_types: np.ndarray) -> np.ndarray:
|
| 82 |
-
"""Single-row 'MSA' for chains without one — just the query."""
|
| 83 |
-
return query_res_types[None, :] # [1, L]
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def construct_paired_msa(
|
| 87 |
-
chain_msas: dict[int, MSA | None],
|
| 88 |
-
chain_query_res_types: dict[int, np.ndarray],
|
| 89 |
-
token_asym_ids: np.ndarray,
|
| 90 |
-
token_res_ids: np.ndarray,
|
| 91 |
-
letter_to_res_type: dict[str, int] | None = None,
|
| 92 |
-
*,
|
| 93 |
-
max_pairs: int = 8192,
|
| 94 |
-
max_total: int = 16384,
|
| 95 |
-
max_seqs: int = 16384,
|
| 96 |
-
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 97 |
-
"""Build paired MSA features.
|
| 98 |
-
|
| 99 |
-
Parameters
|
| 100 |
-
----------
|
| 101 |
-
chain_msas
|
| 102 |
-
``asym_id -> MSA`` (or ``None`` for chains without an MSA).
|
| 103 |
-
chain_query_res_types
|
| 104 |
-
``asym_id -> np.ndarray[L_c]`` of res-type ids for the chain's
|
| 105 |
-
query. Used to build dummy MSAs when a chain has no MSA.
|
| 106 |
-
token_asym_ids
|
| 107 |
-
Per-token asym_id, length ``T``. Must be non-decreasing.
|
| 108 |
-
token_res_ids
|
| 109 |
-
Per-token residue index within chain, length ``T``.
|
| 110 |
-
letter_to_res_type
|
| 111 |
-
1-letter → res-type mapping. Defaults to
|
| 112 |
-
:func:`protein_letter_to_res_type`.
|
| 113 |
-
|
| 114 |
-
Returns
|
| 115 |
-
-------
|
| 116 |
-
msa_residues : ``np.ndarray[M, T]`` int64
|
| 117 |
-
deletion_value : ``np.ndarray[M, T]`` float32 (raw deletion counts; the
|
| 118 |
-
``arctan(/3) * pi/2`` transform is applied by the caller)
|
| 119 |
-
is_paired : ``np.ndarray[M, T]`` float32 broadcast of per-row,
|
| 120 |
-
per-chain paired flags.
|
| 121 |
-
"""
|
| 122 |
-
if letter_to_res_type is None:
|
| 123 |
-
letter_to_res_type = protein_letter_to_res_type()
|
| 124 |
-
|
| 125 |
-
chain_ids: list[int] = sorted(chain_msas.keys())
|
| 126 |
-
|
| 127 |
-
# Build per-chain (res_type, deletions, taxonomy) tables.
|
| 128 |
-
chain_res_type: dict[int, np.ndarray] = {}
|
| 129 |
-
chain_deletions: dict[int, np.ndarray] = {}
|
| 130 |
-
chain_taxonomies: dict[int, list[int]] = {}
|
| 131 |
-
for c in chain_ids:
|
| 132 |
-
m = chain_msas.get(c)
|
| 133 |
-
if m is None or m.depth == 0:
|
| 134 |
-
qres = chain_query_res_types[c]
|
| 135 |
-
chain_res_type[c] = _dummy_msa_residues(qres)
|
| 136 |
-
chain_deletions[c] = np.zeros((1, qres.shape[0]), dtype=np.float32)
|
| 137 |
-
chain_taxonomies[c] = [-1]
|
| 138 |
-
continue
|
| 139 |
-
rt, dl = msa_to_res_type_and_deletions(m, letter_to_res_type)
|
| 140 |
-
chain_res_type[c] = rt
|
| 141 |
-
chain_deletions[c] = dl
|
| 142 |
-
chain_taxonomies[c] = [_taxonomy_from_header(e.header) for e in m.entries]
|
| 143 |
-
|
| 144 |
-
# Group by taxonomy, skip query row and unpaired (-1) entries.
|
| 145 |
-
taxonomy_map: dict[int, list[tuple[int, int]]] = {}
|
| 146 |
-
for c in chain_ids:
|
| 147 |
-
for seq_idx, taxon in enumerate(chain_taxonomies[c]):
|
| 148 |
-
if seq_idx == 0 or taxon == -1:
|
| 149 |
-
continue
|
| 150 |
-
taxonomy_map.setdefault(taxon, []).append((c, seq_idx))
|
| 151 |
-
taxonomy_map = {k: v for k, v in taxonomy_map.items() if len(v) > 1}
|
| 152 |
-
# Order taxonomies by number of distinct chains, descending.
|
| 153 |
-
sorted_taxa = sorted(
|
| 154 |
-
taxonomy_map.items(), key=lambda kv: len({c for c, _ in kv[1]}), reverse=True
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
visited = {s for _, items in taxonomy_map.items() for s in items}
|
| 158 |
-
available: dict[int, list[int]] = {
|
| 159 |
-
c: [i for i in range(1, len(chain_taxonomies[c])) if (c, i) not in visited]
|
| 160 |
-
for c in chain_ids
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
pairing: list[dict[int, int]] = [{c: 0 for c in chain_ids}]
|
| 164 |
-
is_paired: list[dict[int, int]] = [{c: 1 for c in chain_ids}]
|
| 165 |
-
|
| 166 |
-
for _, pairs in sorted_taxa:
|
| 167 |
-
per_chain: dict[int, list[int]] = {}
|
| 168 |
-
for c, seq_idx in pairs:
|
| 169 |
-
per_chain.setdefault(c, []).append(seq_idx)
|
| 170 |
-
max_occ = max(len(v) for v in per_chain.values())
|
| 171 |
-
for i in range(max_occ):
|
| 172 |
-
row_pairing: dict[int, int] = {}
|
| 173 |
-
row_is_paired: dict[int, int] = {}
|
| 174 |
-
for c, seq_idxs in per_chain.items():
|
| 175 |
-
row_pairing[c] = seq_idxs[i % len(seq_idxs)]
|
| 176 |
-
row_is_paired[c] = 1
|
| 177 |
-
for c in chain_ids:
|
| 178 |
-
if c in row_pairing:
|
| 179 |
-
continue
|
| 180 |
-
row_is_paired[c] = 0
|
| 181 |
-
if available[c]:
|
| 182 |
-
row_pairing[c] = available[c].pop(0)
|
| 183 |
-
else:
|
| 184 |
-
row_pairing[c] = -1
|
| 185 |
-
pairing.append(row_pairing)
|
| 186 |
-
is_paired.append(row_is_paired)
|
| 187 |
-
if len(pairing) >= max_pairs:
|
| 188 |
-
break
|
| 189 |
-
if len(pairing) >= max_pairs:
|
| 190 |
-
break
|
| 191 |
-
|
| 192 |
-
max_left = max((len(v) for v in available.values()), default=0)
|
| 193 |
-
for _ in range(min(max_total - len(pairing), max_left)):
|
| 194 |
-
row_pairing = {}
|
| 195 |
-
row_is_paired = {}
|
| 196 |
-
for c in chain_ids:
|
| 197 |
-
row_is_paired[c] = 0
|
| 198 |
-
if available[c]:
|
| 199 |
-
row_pairing[c] = available[c].pop(0)
|
| 200 |
-
else:
|
| 201 |
-
row_pairing[c] = -1
|
| 202 |
-
pairing.append(row_pairing)
|
| 203 |
-
is_paired.append(row_is_paired)
|
| 204 |
-
if len(pairing) >= max_total:
|
| 205 |
-
break
|
| 206 |
-
|
| 207 |
-
pairing = pairing[:max_seqs]
|
| 208 |
-
is_paired = is_paired[:max_seqs]
|
| 209 |
-
M = len(pairing)
|
| 210 |
-
T = len(token_asym_ids)
|
| 211 |
-
|
| 212 |
-
msa_residues = np.full((M, T), MSA_GAP_TOKEN_ID, dtype=np.int64)
|
| 213 |
-
deletion_value = np.zeros((M, T), dtype=np.float32)
|
| 214 |
-
paired_mask = np.zeros((M, T), dtype=np.float32)
|
| 215 |
-
|
| 216 |
-
# Vectorize per chain: gather chain rows according to pairing[c], then
|
| 217 |
-
# index into them by the chain's token residue ids.
|
| 218 |
-
for c in chain_ids:
|
| 219 |
-
rt = chain_res_type[c]
|
| 220 |
-
dl = chain_deletions[c]
|
| 221 |
-
Lc = rt.shape[1]
|
| 222 |
-
chain_pairing = np.array([row[c] for row in pairing], dtype=np.int64)
|
| 223 |
-
chain_paired = np.array([row[c] for row in is_paired], dtype=np.float32)
|
| 224 |
-
|
| 225 |
-
token_mask = token_asym_ids == c
|
| 226 |
-
if not token_mask.any():
|
| 227 |
-
continue
|
| 228 |
-
token_res_in_chain = token_res_ids[token_mask]
|
| 229 |
-
# Clamp residue indices to the MSA's column range. Modified-residue
|
| 230 |
-
# tokens that exceed the query length fall back to the last column.
|
| 231 |
-
cols = np.minimum(token_res_in_chain, Lc - 1)
|
| 232 |
-
|
| 233 |
-
# Rows where pairing == -1 fall back to gap (already initialized).
|
| 234 |
-
valid_rows = chain_pairing >= 0
|
| 235 |
-
if valid_rows.any():
|
| 236 |
-
gathered_rt = rt[chain_pairing[valid_rows]][:, cols]
|
| 237 |
-
gathered_dl = dl[chain_pairing[valid_rows]][:, cols]
|
| 238 |
-
valid_idx = np.where(valid_rows)[0]
|
| 239 |
-
token_idx = np.where(token_mask)[0]
|
| 240 |
-
msa_residues[np.ix_(valid_idx, token_idx)] = gathered_rt
|
| 241 |
-
deletion_value[np.ix_(valid_idx, token_idx)] = gathered_dl
|
| 242 |
-
|
| 243 |
-
paired_mask[:, token_mask] = chain_paired[:, None]
|
| 244 |
-
|
| 245 |
-
return msa_residues, deletion_value, paired_mask
|
|
|
|
| 1 |
+
"""Taxonomy-paired MSA construction for ESMFold2 inference.
|
| 2 |
+
|
| 3 |
+
Taxonomy IDs are read from FASTA headers as ``key=N`` tokens. Rows
|
| 4 |
+
where any chain has ``key=-1`` (or no ``key=`` at all) are treated as
|
| 5 |
+
unpaired and assigned to that chain's block-diagonal section after
|
| 6 |
+
the paired rows.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
from .esmfold2_constants import (
|
| 14 |
+
MSA_GAP_TOKEN_ID,
|
| 15 |
+
PROTEIN_3TO1,
|
| 16 |
+
PROTEIN_RESIDUE_TO_RES_TYPE,
|
| 17 |
+
PROTEIN_UNK_RES_TYPE,
|
| 18 |
+
)
|
| 19 |
+
from .esmfold2_msa import MSA
|
| 20 |
+
|
| 21 |
+
_KEY_RE = re.compile(r"key=(-?\d+)")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def protein_letter_to_res_type() -> dict[str, int]:
|
| 25 |
+
"""Return the protein 1-letter → res_type mapping used by the MSA encoder."""
|
| 26 |
+
mapping: dict[str, int] = {}
|
| 27 |
+
for three, one in PROTEIN_3TO1.items():
|
| 28 |
+
if three in PROTEIN_RESIDUE_TO_RES_TYPE:
|
| 29 |
+
mapping[one] = PROTEIN_RESIDUE_TO_RES_TYPE[three]
|
| 30 |
+
mapping["-"] = MSA_GAP_TOKEN_ID
|
| 31 |
+
mapping["X"] = PROTEIN_UNK_RES_TYPE
|
| 32 |
+
return mapping
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _taxonomy_from_header(header: str) -> int:
|
| 36 |
+
if not header:
|
| 37 |
+
return -1
|
| 38 |
+
m = _KEY_RE.search(header)
|
| 39 |
+
return int(m.group(1)) if m else -1
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def msa_to_res_type_and_deletions(
|
| 43 |
+
msa: MSA, letter_to_res_type: dict[str, int]
|
| 44 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 45 |
+
"""Convert an :class:`MSA` to ``(res_type[M, L], deletion_count[M, L])``.
|
| 46 |
+
|
| 47 |
+
Handles a3m insertion convention: lowercase letters and ``.`` are
|
| 48 |
+
insertions and are not emitted; their count is accumulated into the
|
| 49 |
+
next non-insertion position's deletion value. ``L`` is the query
|
| 50 |
+
length after stripping insertions from row 0.
|
| 51 |
+
"""
|
| 52 |
+
query = msa.entries[0].sequence
|
| 53 |
+
L = sum(1 for ch in query if not (ch.islower() or ch == "."))
|
| 54 |
+
M = msa.depth
|
| 55 |
+
|
| 56 |
+
res_type = np.full((M, L), MSA_GAP_TOKEN_ID, dtype=np.int64)
|
| 57 |
+
deletions = np.zeros((M, L), dtype=np.float32)
|
| 58 |
+
|
| 59 |
+
for r, entry in enumerate(msa.entries):
|
| 60 |
+
col = 0
|
| 61 |
+
ins = 0
|
| 62 |
+
for ch in entry.sequence:
|
| 63 |
+
if ch == "." or (ch.islower() and ch != "-"):
|
| 64 |
+
ins += 1
|
| 65 |
+
continue
|
| 66 |
+
if col >= L:
|
| 67 |
+
break
|
| 68 |
+
if ch == "-":
|
| 69 |
+
res_type[r, col] = MSA_GAP_TOKEN_ID
|
| 70 |
+
else:
|
| 71 |
+
res_type[r, col] = letter_to_res_type.get(
|
| 72 |
+
ch.upper(), PROTEIN_UNK_RES_TYPE
|
| 73 |
+
)
|
| 74 |
+
if ins > 0:
|
| 75 |
+
deletions[r, col] = float(ins)
|
| 76 |
+
ins = 0
|
| 77 |
+
col += 1
|
| 78 |
+
return res_type, deletions
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _dummy_msa_residues(query_res_types: np.ndarray) -> np.ndarray:
|
| 82 |
+
"""Single-row 'MSA' for chains without one — just the query."""
|
| 83 |
+
return query_res_types[None, :] # [1, L]
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def construct_paired_msa(
|
| 87 |
+
chain_msas: dict[int, MSA | None],
|
| 88 |
+
chain_query_res_types: dict[int, np.ndarray],
|
| 89 |
+
token_asym_ids: np.ndarray,
|
| 90 |
+
token_res_ids: np.ndarray,
|
| 91 |
+
letter_to_res_type: dict[str, int] | None = None,
|
| 92 |
+
*,
|
| 93 |
+
max_pairs: int = 8192,
|
| 94 |
+
max_total: int = 16384,
|
| 95 |
+
max_seqs: int = 16384,
|
| 96 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 97 |
+
"""Build paired MSA features.
|
| 98 |
+
|
| 99 |
+
Parameters
|
| 100 |
+
----------
|
| 101 |
+
chain_msas
|
| 102 |
+
``asym_id -> MSA`` (or ``None`` for chains without an MSA).
|
| 103 |
+
chain_query_res_types
|
| 104 |
+
``asym_id -> np.ndarray[L_c]`` of res-type ids for the chain's
|
| 105 |
+
query. Used to build dummy MSAs when a chain has no MSA.
|
| 106 |
+
token_asym_ids
|
| 107 |
+
Per-token asym_id, length ``T``. Must be non-decreasing.
|
| 108 |
+
token_res_ids
|
| 109 |
+
Per-token residue index within chain, length ``T``.
|
| 110 |
+
letter_to_res_type
|
| 111 |
+
1-letter → res-type mapping. Defaults to
|
| 112 |
+
:func:`protein_letter_to_res_type`.
|
| 113 |
+
|
| 114 |
+
Returns
|
| 115 |
+
-------
|
| 116 |
+
msa_residues : ``np.ndarray[M, T]`` int64
|
| 117 |
+
deletion_value : ``np.ndarray[M, T]`` float32 (raw deletion counts; the
|
| 118 |
+
``arctan(/3) * pi/2`` transform is applied by the caller)
|
| 119 |
+
is_paired : ``np.ndarray[M, T]`` float32 broadcast of per-row,
|
| 120 |
+
per-chain paired flags.
|
| 121 |
+
"""
|
| 122 |
+
if letter_to_res_type is None:
|
| 123 |
+
letter_to_res_type = protein_letter_to_res_type()
|
| 124 |
+
|
| 125 |
+
chain_ids: list[int] = sorted(chain_msas.keys())
|
| 126 |
+
|
| 127 |
+
# Build per-chain (res_type, deletions, taxonomy) tables.
|
| 128 |
+
chain_res_type: dict[int, np.ndarray] = {}
|
| 129 |
+
chain_deletions: dict[int, np.ndarray] = {}
|
| 130 |
+
chain_taxonomies: dict[int, list[int]] = {}
|
| 131 |
+
for c in chain_ids:
|
| 132 |
+
m = chain_msas.get(c)
|
| 133 |
+
if m is None or m.depth == 0:
|
| 134 |
+
qres = chain_query_res_types[c]
|
| 135 |
+
chain_res_type[c] = _dummy_msa_residues(qres)
|
| 136 |
+
chain_deletions[c] = np.zeros((1, qres.shape[0]), dtype=np.float32)
|
| 137 |
+
chain_taxonomies[c] = [-1]
|
| 138 |
+
continue
|
| 139 |
+
rt, dl = msa_to_res_type_and_deletions(m, letter_to_res_type)
|
| 140 |
+
chain_res_type[c] = rt
|
| 141 |
+
chain_deletions[c] = dl
|
| 142 |
+
chain_taxonomies[c] = [_taxonomy_from_header(e.header) for e in m.entries]
|
| 143 |
+
|
| 144 |
+
# Group by taxonomy, skip query row and unpaired (-1) entries.
|
| 145 |
+
taxonomy_map: dict[int, list[tuple[int, int]]] = {}
|
| 146 |
+
for c in chain_ids:
|
| 147 |
+
for seq_idx, taxon in enumerate(chain_taxonomies[c]):
|
| 148 |
+
if seq_idx == 0 or taxon == -1:
|
| 149 |
+
continue
|
| 150 |
+
taxonomy_map.setdefault(taxon, []).append((c, seq_idx))
|
| 151 |
+
taxonomy_map = {k: v for k, v in taxonomy_map.items() if len(v) > 1}
|
| 152 |
+
# Order taxonomies by number of distinct chains, descending.
|
| 153 |
+
sorted_taxa = sorted(
|
| 154 |
+
taxonomy_map.items(), key=lambda kv: len({c for c, _ in kv[1]}), reverse=True
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
visited = {s for _, items in taxonomy_map.items() for s in items}
|
| 158 |
+
available: dict[int, list[int]] = {
|
| 159 |
+
c: [i for i in range(1, len(chain_taxonomies[c])) if (c, i) not in visited]
|
| 160 |
+
for c in chain_ids
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
pairing: list[dict[int, int]] = [{c: 0 for c in chain_ids}]
|
| 164 |
+
is_paired: list[dict[int, int]] = [{c: 1 for c in chain_ids}]
|
| 165 |
+
|
| 166 |
+
for _, pairs in sorted_taxa:
|
| 167 |
+
per_chain: dict[int, list[int]] = {}
|
| 168 |
+
for c, seq_idx in pairs:
|
| 169 |
+
per_chain.setdefault(c, []).append(seq_idx)
|
| 170 |
+
max_occ = max(len(v) for v in per_chain.values())
|
| 171 |
+
for i in range(max_occ):
|
| 172 |
+
row_pairing: dict[int, int] = {}
|
| 173 |
+
row_is_paired: dict[int, int] = {}
|
| 174 |
+
for c, seq_idxs in per_chain.items():
|
| 175 |
+
row_pairing[c] = seq_idxs[i % len(seq_idxs)]
|
| 176 |
+
row_is_paired[c] = 1
|
| 177 |
+
for c in chain_ids:
|
| 178 |
+
if c in row_pairing:
|
| 179 |
+
continue
|
| 180 |
+
row_is_paired[c] = 0
|
| 181 |
+
if available[c]:
|
| 182 |
+
row_pairing[c] = available[c].pop(0)
|
| 183 |
+
else:
|
| 184 |
+
row_pairing[c] = -1
|
| 185 |
+
pairing.append(row_pairing)
|
| 186 |
+
is_paired.append(row_is_paired)
|
| 187 |
+
if len(pairing) >= max_pairs:
|
| 188 |
+
break
|
| 189 |
+
if len(pairing) >= max_pairs:
|
| 190 |
+
break
|
| 191 |
+
|
| 192 |
+
max_left = max((len(v) for v in available.values()), default=0)
|
| 193 |
+
for _ in range(min(max_total - len(pairing), max_left)):
|
| 194 |
+
row_pairing = {}
|
| 195 |
+
row_is_paired = {}
|
| 196 |
+
for c in chain_ids:
|
| 197 |
+
row_is_paired[c] = 0
|
| 198 |
+
if available[c]:
|
| 199 |
+
row_pairing[c] = available[c].pop(0)
|
| 200 |
+
else:
|
| 201 |
+
row_pairing[c] = -1
|
| 202 |
+
pairing.append(row_pairing)
|
| 203 |
+
is_paired.append(row_is_paired)
|
| 204 |
+
if len(pairing) >= max_total:
|
| 205 |
+
break
|
| 206 |
+
|
| 207 |
+
pairing = pairing[:max_seqs]
|
| 208 |
+
is_paired = is_paired[:max_seqs]
|
| 209 |
+
M = len(pairing)
|
| 210 |
+
T = len(token_asym_ids)
|
| 211 |
+
|
| 212 |
+
msa_residues = np.full((M, T), MSA_GAP_TOKEN_ID, dtype=np.int64)
|
| 213 |
+
deletion_value = np.zeros((M, T), dtype=np.float32)
|
| 214 |
+
paired_mask = np.zeros((M, T), dtype=np.float32)
|
| 215 |
+
|
| 216 |
+
# Vectorize per chain: gather chain rows according to pairing[c], then
|
| 217 |
+
# index into them by the chain's token residue ids.
|
| 218 |
+
for c in chain_ids:
|
| 219 |
+
rt = chain_res_type[c]
|
| 220 |
+
dl = chain_deletions[c]
|
| 221 |
+
Lc = rt.shape[1]
|
| 222 |
+
chain_pairing = np.array([row[c] for row in pairing], dtype=np.int64)
|
| 223 |
+
chain_paired = np.array([row[c] for row in is_paired], dtype=np.float32)
|
| 224 |
+
|
| 225 |
+
token_mask = token_asym_ids == c
|
| 226 |
+
if not token_mask.any():
|
| 227 |
+
continue
|
| 228 |
+
token_res_in_chain = token_res_ids[token_mask]
|
| 229 |
+
# Clamp residue indices to the MSA's column range. Modified-residue
|
| 230 |
+
# tokens that exceed the query length fall back to the last column.
|
| 231 |
+
cols = np.minimum(token_res_in_chain, Lc - 1)
|
| 232 |
+
|
| 233 |
+
# Rows where pairing == -1 fall back to gap (already initialized).
|
| 234 |
+
valid_rows = chain_pairing >= 0
|
| 235 |
+
if valid_rows.any():
|
| 236 |
+
gathered_rt = rt[chain_pairing[valid_rows]][:, cols]
|
| 237 |
+
gathered_dl = dl[chain_pairing[valid_rows]][:, cols]
|
| 238 |
+
valid_idx = np.where(valid_rows)[0]
|
| 239 |
+
token_idx = np.where(token_mask)[0]
|
| 240 |
+
msa_residues[np.ix_(valid_idx, token_idx)] = gathered_rt
|
| 241 |
+
deletion_value[np.ix_(valid_idx, token_idx)] = gathered_dl
|
| 242 |
+
|
| 243 |
+
paired_mask[:, token_mask] = chain_paired[:, None]
|
| 244 |
+
|
| 245 |
+
return msa_residues, deletion_value, paired_mask
|
esmfold2_parsing.py
CHANGED
|
@@ -1,112 +1,112 @@
|
|
| 1 |
-
import io
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
from typing import Generator, Iterable, NamedTuple
|
| 4 |
-
|
| 5 |
-
PathOrBuffer = str | Path | io.TextIOBase
|
| 6 |
-
FastaEntry = NamedTuple("FastaEntry", [("header", str), ("sequence", str)])
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def parse_fasta(fasta_string: str) -> Generator[FastaEntry, None, None]:
|
| 10 |
-
"""
|
| 11 |
-
Parses a fasta file and yields FastaEntry objects
|
| 12 |
-
|
| 13 |
-
Args:
|
| 14 |
-
fasta_string: The fasta file as a string
|
| 15 |
-
Returns:
|
| 16 |
-
A generator of FastaEntry objects
|
| 17 |
-
"""
|
| 18 |
-
header = None
|
| 19 |
-
seq = []
|
| 20 |
-
num_sequences = 0
|
| 21 |
-
for line in fasta_string.splitlines():
|
| 22 |
-
if not line or line[0] == "#":
|
| 23 |
-
continue
|
| 24 |
-
if line.startswith(">"):
|
| 25 |
-
if header is not None:
|
| 26 |
-
yield FastaEntry(header, "".join(seq))
|
| 27 |
-
seq = []
|
| 28 |
-
header = line[1:].strip()
|
| 29 |
-
else:
|
| 30 |
-
seq.append(line)
|
| 31 |
-
if header is not None:
|
| 32 |
-
num_sequences += 1
|
| 33 |
-
yield FastaEntry(header, "".join(seq))
|
| 34 |
-
|
| 35 |
-
if num_sequences == 0:
|
| 36 |
-
raise ValueError("Found no sequences in input")
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def read_sequences(path: PathOrBuffer) -> Generator[FastaEntry, None, None]:
|
| 40 |
-
# Uses duck typing to try and call the right method
|
| 41 |
-
# Doesn't use explicit isinstance check to support
|
| 42 |
-
# inputs that are not explicitly str/Path/TextIOBase but
|
| 43 |
-
# may support similar functionality
|
| 44 |
-
data = None # type: ignore
|
| 45 |
-
try:
|
| 46 |
-
if str(path).endswith(".gz"):
|
| 47 |
-
import gzip
|
| 48 |
-
|
| 49 |
-
data = gzip.open(path, "rt") # type: ignore
|
| 50 |
-
else:
|
| 51 |
-
try:
|
| 52 |
-
data = open(path) # type: ignore
|
| 53 |
-
except TypeError:
|
| 54 |
-
data: io.TextIOBase = path # type: ignore
|
| 55 |
-
|
| 56 |
-
yield from parse_fasta(data.read())
|
| 57 |
-
finally:
|
| 58 |
-
if data is not None:
|
| 59 |
-
data.close()
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def read_first_sequence(path: PathOrBuffer) -> FastaEntry:
|
| 63 |
-
return next(iter(read_sequences(path)))
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def count_fasta_sequences(path: str | Path) -> int:
|
| 67 |
-
"""Count sequences in a FASTA file by counting header lines.
|
| 68 |
-
|
| 69 |
-
Faster than parsing the full file — only scans for '>' prefixes.
|
| 70 |
-
Returns 0 if the file does not exist.
|
| 71 |
-
"""
|
| 72 |
-
path = Path(path)
|
| 73 |
-
if not path.exists():
|
| 74 |
-
return 0
|
| 75 |
-
with open(path) as f:
|
| 76 |
-
return sum(1 for line in f if line.startswith(">"))
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def append_fasta_sequence(header: str, sequence: str, path: str | Path) -> None:
|
| 80 |
-
"""Append a single sequence to a FASTA file (creating it if needed)."""
|
| 81 |
-
path = Path(path)
|
| 82 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
-
# The existing file may not end with a newline (e.g., write_sequences()
|
| 84 |
-
# explicitly avoids writing a newline at the end), so we insert one before
|
| 85 |
-
# appending to avoid merging with the last line.
|
| 86 |
-
needs_newline = (
|
| 87 |
-
path.exists() and path.stat().st_size > 0 and path.read_bytes()[-1:] != b"\n"
|
| 88 |
-
)
|
| 89 |
-
with open(path, "a") as f:
|
| 90 |
-
if needs_newline:
|
| 91 |
-
f.write("\n")
|
| 92 |
-
f.write(f">{header}\n{sequence}\n")
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def write_sequences(sequences: Iterable[tuple[str, str]], path: PathOrBuffer) -> None:
|
| 96 |
-
needs_closing = False
|
| 97 |
-
handle = None
|
| 98 |
-
try:
|
| 99 |
-
try:
|
| 100 |
-
handle = open(path, "w") # type: ignore
|
| 101 |
-
needs_closing = True
|
| 102 |
-
except TypeError:
|
| 103 |
-
handle = path
|
| 104 |
-
has_prev = False
|
| 105 |
-
for header, seq in sequences:
|
| 106 |
-
if has_prev:
|
| 107 |
-
handle.write("\n") # type: ignore
|
| 108 |
-
handle.write(f">{header}\n{seq}") # type: ignore
|
| 109 |
-
has_prev = True
|
| 110 |
-
finally:
|
| 111 |
-
if needs_closing:
|
| 112 |
-
handle.close() # type: ignore
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Generator, Iterable, NamedTuple
|
| 4 |
+
|
| 5 |
+
PathOrBuffer = str | Path | io.TextIOBase
|
| 6 |
+
FastaEntry = NamedTuple("FastaEntry", [("header", str), ("sequence", str)])
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def parse_fasta(fasta_string: str) -> Generator[FastaEntry, None, None]:
|
| 10 |
+
"""
|
| 11 |
+
Parses a fasta file and yields FastaEntry objects
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
fasta_string: The fasta file as a string
|
| 15 |
+
Returns:
|
| 16 |
+
A generator of FastaEntry objects
|
| 17 |
+
"""
|
| 18 |
+
header = None
|
| 19 |
+
seq = []
|
| 20 |
+
num_sequences = 0
|
| 21 |
+
for line in fasta_string.splitlines():
|
| 22 |
+
if not line or line[0] == "#":
|
| 23 |
+
continue
|
| 24 |
+
if line.startswith(">"):
|
| 25 |
+
if header is not None:
|
| 26 |
+
yield FastaEntry(header, "".join(seq))
|
| 27 |
+
seq = []
|
| 28 |
+
header = line[1:].strip()
|
| 29 |
+
else:
|
| 30 |
+
seq.append(line)
|
| 31 |
+
if header is not None:
|
| 32 |
+
num_sequences += 1
|
| 33 |
+
yield FastaEntry(header, "".join(seq))
|
| 34 |
+
|
| 35 |
+
if num_sequences == 0:
|
| 36 |
+
raise ValueError("Found no sequences in input")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def read_sequences(path: PathOrBuffer) -> Generator[FastaEntry, None, None]:
|
| 40 |
+
# Uses duck typing to try and call the right method
|
| 41 |
+
# Doesn't use explicit isinstance check to support
|
| 42 |
+
# inputs that are not explicitly str/Path/TextIOBase but
|
| 43 |
+
# may support similar functionality
|
| 44 |
+
data = None # type: ignore
|
| 45 |
+
try:
|
| 46 |
+
if str(path).endswith(".gz"):
|
| 47 |
+
import gzip
|
| 48 |
+
|
| 49 |
+
data = gzip.open(path, "rt") # type: ignore
|
| 50 |
+
else:
|
| 51 |
+
try:
|
| 52 |
+
data = open(path) # type: ignore
|
| 53 |
+
except TypeError:
|
| 54 |
+
data: io.TextIOBase = path # type: ignore
|
| 55 |
+
|
| 56 |
+
yield from parse_fasta(data.read())
|
| 57 |
+
finally:
|
| 58 |
+
if data is not None:
|
| 59 |
+
data.close()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def read_first_sequence(path: PathOrBuffer) -> FastaEntry:
|
| 63 |
+
return next(iter(read_sequences(path)))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def count_fasta_sequences(path: str | Path) -> int:
|
| 67 |
+
"""Count sequences in a FASTA file by counting header lines.
|
| 68 |
+
|
| 69 |
+
Faster than parsing the full file — only scans for '>' prefixes.
|
| 70 |
+
Returns 0 if the file does not exist.
|
| 71 |
+
"""
|
| 72 |
+
path = Path(path)
|
| 73 |
+
if not path.exists():
|
| 74 |
+
return 0
|
| 75 |
+
with open(path) as f:
|
| 76 |
+
return sum(1 for line in f if line.startswith(">"))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def append_fasta_sequence(header: str, sequence: str, path: str | Path) -> None:
|
| 80 |
+
"""Append a single sequence to a FASTA file (creating it if needed)."""
|
| 81 |
+
path = Path(path)
|
| 82 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
# The existing file may not end with a newline (e.g., write_sequences()
|
| 84 |
+
# explicitly avoids writing a newline at the end), so we insert one before
|
| 85 |
+
# appending to avoid merging with the last line.
|
| 86 |
+
needs_newline = (
|
| 87 |
+
path.exists() and path.stat().st_size > 0 and path.read_bytes()[-1:] != b"\n"
|
| 88 |
+
)
|
| 89 |
+
with open(path, "a") as f:
|
| 90 |
+
if needs_newline:
|
| 91 |
+
f.write("\n")
|
| 92 |
+
f.write(f">{header}\n{sequence}\n")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def write_sequences(sequences: Iterable[tuple[str, str]], path: PathOrBuffer) -> None:
|
| 96 |
+
needs_closing = False
|
| 97 |
+
handle = None
|
| 98 |
+
try:
|
| 99 |
+
try:
|
| 100 |
+
handle = open(path, "w") # type: ignore
|
| 101 |
+
needs_closing = True
|
| 102 |
+
except TypeError:
|
| 103 |
+
handle = path
|
| 104 |
+
has_prev = False
|
| 105 |
+
for header, seq in sequences:
|
| 106 |
+
if has_prev:
|
| 107 |
+
handle.write("\n") # type: ignore
|
| 108 |
+
handle.write(f">{header}\n{seq}") # type: ignore
|
| 109 |
+
has_prev = True
|
| 110 |
+
finally:
|
| 111 |
+
if needs_closing:
|
| 112 |
+
handle.close() # type: ignore
|
esmfold2_predicted_aligned_error.py
CHANGED
|
@@ -1,104 +1,104 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import torch.nn.functional as F
|
| 3 |
-
|
| 4 |
-
from .esmfold2_affine3d import Affine3D
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
def masked_mean(
|
| 8 |
-
mask: torch.Tensor,
|
| 9 |
-
value: torch.Tensor,
|
| 10 |
-
dim: int | None | tuple[int, ...] = None,
|
| 11 |
-
eps=1e-10,
|
| 12 |
-
) -> torch.Tensor:
|
| 13 |
-
"""Compute the mean of `value` where only positions where `mask == true` are
|
| 14 |
-
counted.
|
| 15 |
-
"""
|
| 16 |
-
mask = mask.expand(*value.shape)
|
| 17 |
-
return torch.sum(mask * value, dim=dim) / (eps + torch.sum(mask, dim=dim))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def _pae_bins(
|
| 21 |
-
max_bin: float = 31, num_bins: int = 64, device: torch.device = torch.device("cpu")
|
| 22 |
-
):
|
| 23 |
-
bins = torch.linspace(0, max_bin, steps=(num_bins - 1), device=device)
|
| 24 |
-
step = max_bin / (num_bins - 2)
|
| 25 |
-
bin_centers = bins + step / 2
|
| 26 |
-
bin_centers = torch.cat(
|
| 27 |
-
[bin_centers, (bin_centers[-1] + step).unsqueeze(-1)], dim=0
|
| 28 |
-
)
|
| 29 |
-
return bin_centers
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def _compute_pae_masks(mask: torch.Tensor):
|
| 33 |
-
square_mask = (mask.unsqueeze(-1) * mask.unsqueeze(-2)).bool()
|
| 34 |
-
return square_mask
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def compute_predicted_aligned_error(
|
| 38 |
-
logits: torch.Tensor,
|
| 39 |
-
aa_mask: torch.Tensor,
|
| 40 |
-
sequence_id: torch.Tensor | None = None,
|
| 41 |
-
max_bin: float = 31,
|
| 42 |
-
) -> torch.Tensor:
|
| 43 |
-
bins = _pae_bins(max_bin, logits.shape[-1], logits.device)
|
| 44 |
-
square_mask = _compute_pae_masks(aa_mask)
|
| 45 |
-
min_v = torch.finfo(logits.dtype).min
|
| 46 |
-
probs = logits.masked_fill(~square_mask.unsqueeze(-1), min_v).softmax(dim=-1)
|
| 47 |
-
|
| 48 |
-
return (probs * bins).sum(dim=-1)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
@torch.no_grad
|
| 52 |
-
def compute_tm(logits: torch.Tensor, aa_mask: torch.Tensor, max_bin: float = 31.0):
|
| 53 |
-
square_mask = _compute_pae_masks(aa_mask)
|
| 54 |
-
seqlens = aa_mask.sum(-1, keepdim=True)
|
| 55 |
-
bins = _pae_bins(max_bin, logits.shape[-1], logits.device)
|
| 56 |
-
d0 = 1.24 * (seqlens.clamp_min(19) - 15) ** (1 / 3) - 1.8
|
| 57 |
-
f_d = 1.0 / (1 + (bins / d0.unsqueeze(-1)) ** 2)
|
| 58 |
-
|
| 59 |
-
min_v = torch.finfo(logits.dtype).min
|
| 60 |
-
probs = logits.masked_fill(~square_mask.unsqueeze(-1), min_v).softmax(dim=-1)
|
| 61 |
-
# This is the sum over bins
|
| 62 |
-
ptm = (probs * f_d.unsqueeze(-2)).sum(dim=-1)
|
| 63 |
-
# This is the mean over residues j
|
| 64 |
-
ptm = masked_mean(square_mask, ptm, dim=-1)
|
| 65 |
-
# The we do a max over residues i
|
| 66 |
-
return ptm.max(dim=-1).values
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def tm_loss(
|
| 70 |
-
logits: torch.Tensor,
|
| 71 |
-
pred_affine: torch.Tensor,
|
| 72 |
-
targ_affine: torch.Tensor,
|
| 73 |
-
targ_mask: torch.Tensor,
|
| 74 |
-
tm_mask: torch.Tensor | None = None,
|
| 75 |
-
sequence_id: torch.Tensor | None = None,
|
| 76 |
-
max_bin: float = 31,
|
| 77 |
-
):
|
| 78 |
-
pred = Affine3D.from_tensor(pred_affine)
|
| 79 |
-
targ = Affine3D.from_tensor(targ_affine)
|
| 80 |
-
|
| 81 |
-
def transform(affine: Affine3D):
|
| 82 |
-
pts = affine.trans[..., None, :, :]
|
| 83 |
-
return affine.invert()[..., None].apply(pts)
|
| 84 |
-
|
| 85 |
-
with torch.no_grad():
|
| 86 |
-
sq_diff = (transform(pred) - transform(targ)).square().sum(dim=-1)
|
| 87 |
-
|
| 88 |
-
num_bins = logits.shape[-1]
|
| 89 |
-
sq_bins = torch.linspace(
|
| 90 |
-
0, max_bin, num_bins - 1, device=logits.device
|
| 91 |
-
).square()
|
| 92 |
-
# Gets the bin id by using a sum.
|
| 93 |
-
true_bins = (sq_diff[..., None] > sq_bins).sum(dim=-1).long()
|
| 94 |
-
|
| 95 |
-
errors = F.cross_entropy(logits.movedim(3, 1), true_bins, reduction="none")
|
| 96 |
-
square_mask = _compute_pae_masks(targ_mask)
|
| 97 |
-
loss = masked_mean(square_mask, errors, dim=(-1, -2))
|
| 98 |
-
|
| 99 |
-
if tm_mask is not None:
|
| 100 |
-
loss = masked_mean(tm_mask, loss, dim=None)
|
| 101 |
-
else:
|
| 102 |
-
loss = loss.mean()
|
| 103 |
-
|
| 104 |
-
return loss
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
|
| 4 |
+
from .esmfold2_affine3d import Affine3D
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def masked_mean(
|
| 8 |
+
mask: torch.Tensor,
|
| 9 |
+
value: torch.Tensor,
|
| 10 |
+
dim: int | None | tuple[int, ...] = None,
|
| 11 |
+
eps=1e-10,
|
| 12 |
+
) -> torch.Tensor:
|
| 13 |
+
"""Compute the mean of `value` where only positions where `mask == true` are
|
| 14 |
+
counted.
|
| 15 |
+
"""
|
| 16 |
+
mask = mask.expand(*value.shape)
|
| 17 |
+
return torch.sum(mask * value, dim=dim) / (eps + torch.sum(mask, dim=dim))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _pae_bins(
|
| 21 |
+
max_bin: float = 31, num_bins: int = 64, device: torch.device = torch.device("cpu")
|
| 22 |
+
):
|
| 23 |
+
bins = torch.linspace(0, max_bin, steps=(num_bins - 1), device=device)
|
| 24 |
+
step = max_bin / (num_bins - 2)
|
| 25 |
+
bin_centers = bins + step / 2
|
| 26 |
+
bin_centers = torch.cat(
|
| 27 |
+
[bin_centers, (bin_centers[-1] + step).unsqueeze(-1)], dim=0
|
| 28 |
+
)
|
| 29 |
+
return bin_centers
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _compute_pae_masks(mask: torch.Tensor):
|
| 33 |
+
square_mask = (mask.unsqueeze(-1) * mask.unsqueeze(-2)).bool()
|
| 34 |
+
return square_mask
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def compute_predicted_aligned_error(
|
| 38 |
+
logits: torch.Tensor,
|
| 39 |
+
aa_mask: torch.Tensor,
|
| 40 |
+
sequence_id: torch.Tensor | None = None,
|
| 41 |
+
max_bin: float = 31,
|
| 42 |
+
) -> torch.Tensor:
|
| 43 |
+
bins = _pae_bins(max_bin, logits.shape[-1], logits.device)
|
| 44 |
+
square_mask = _compute_pae_masks(aa_mask)
|
| 45 |
+
min_v = torch.finfo(logits.dtype).min
|
| 46 |
+
probs = logits.masked_fill(~square_mask.unsqueeze(-1), min_v).softmax(dim=-1)
|
| 47 |
+
|
| 48 |
+
return (probs * bins).sum(dim=-1)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@torch.no_grad
|
| 52 |
+
def compute_tm(logits: torch.Tensor, aa_mask: torch.Tensor, max_bin: float = 31.0):
|
| 53 |
+
square_mask = _compute_pae_masks(aa_mask)
|
| 54 |
+
seqlens = aa_mask.sum(-1, keepdim=True)
|
| 55 |
+
bins = _pae_bins(max_bin, logits.shape[-1], logits.device)
|
| 56 |
+
d0 = 1.24 * (seqlens.clamp_min(19) - 15) ** (1 / 3) - 1.8
|
| 57 |
+
f_d = 1.0 / (1 + (bins / d0.unsqueeze(-1)) ** 2)
|
| 58 |
+
|
| 59 |
+
min_v = torch.finfo(logits.dtype).min
|
| 60 |
+
probs = logits.masked_fill(~square_mask.unsqueeze(-1), min_v).softmax(dim=-1)
|
| 61 |
+
# This is the sum over bins
|
| 62 |
+
ptm = (probs * f_d.unsqueeze(-2)).sum(dim=-1)
|
| 63 |
+
# This is the mean over residues j
|
| 64 |
+
ptm = masked_mean(square_mask, ptm, dim=-1)
|
| 65 |
+
# The we do a max over residues i
|
| 66 |
+
return ptm.max(dim=-1).values
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def tm_loss(
|
| 70 |
+
logits: torch.Tensor,
|
| 71 |
+
pred_affine: torch.Tensor,
|
| 72 |
+
targ_affine: torch.Tensor,
|
| 73 |
+
targ_mask: torch.Tensor,
|
| 74 |
+
tm_mask: torch.Tensor | None = None,
|
| 75 |
+
sequence_id: torch.Tensor | None = None,
|
| 76 |
+
max_bin: float = 31,
|
| 77 |
+
):
|
| 78 |
+
pred = Affine3D.from_tensor(pred_affine)
|
| 79 |
+
targ = Affine3D.from_tensor(targ_affine)
|
| 80 |
+
|
| 81 |
+
def transform(affine: Affine3D):
|
| 82 |
+
pts = affine.trans[..., None, :, :]
|
| 83 |
+
return affine.invert()[..., None].apply(pts)
|
| 84 |
+
|
| 85 |
+
with torch.no_grad():
|
| 86 |
+
sq_diff = (transform(pred) - transform(targ)).square().sum(dim=-1)
|
| 87 |
+
|
| 88 |
+
num_bins = logits.shape[-1]
|
| 89 |
+
sq_bins = torch.linspace(
|
| 90 |
+
0, max_bin, num_bins - 1, device=logits.device
|
| 91 |
+
).square()
|
| 92 |
+
# Gets the bin id by using a sum.
|
| 93 |
+
true_bins = (sq_diff[..., None] > sq_bins).sum(dim=-1).long()
|
| 94 |
+
|
| 95 |
+
errors = F.cross_entropy(logits.movedim(3, 1), true_bins, reduction="none")
|
| 96 |
+
square_mask = _compute_pae_masks(targ_mask)
|
| 97 |
+
loss = masked_mean(square_mask, errors, dim=(-1, -2))
|
| 98 |
+
|
| 99 |
+
if tm_mask is not None:
|
| 100 |
+
loss = masked_mean(tm_mask, loss, dim=None)
|
| 101 |
+
else:
|
| 102 |
+
loss = loss.mean()
|
| 103 |
+
|
| 104 |
+
return loss
|
esmfold2_prepare_input.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
esmfold2_processor.py
CHANGED
|
@@ -1,355 +1,355 @@
|
|
| 1 |
-
import random
|
| 2 |
-
from contextlib import contextmanager, nullcontext
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
from typing import Any
|
| 5 |
-
|
| 6 |
-
import numpy as np
|
| 7 |
-
import torch
|
| 8 |
-
|
| 9 |
-
from .esmfold2_conformers import load_ccd
|
| 10 |
-
from .esmfold2_output import build_molecular_complex_from_features
|
| 11 |
-
from .esmfold2_prepare_input import ChainInfo, prepare_esmfold2_input
|
| 12 |
-
from .esmfold2_types import (
|
| 13 |
-
MSA,
|
| 14 |
-
Modification,
|
| 15 |
-
ProteinInput,
|
| 16 |
-
StructurePredictionInput,
|
| 17 |
-
)
|
| 18 |
-
from .esmfold2_molecular_complex import MolecularComplexResult
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
@contextmanager
|
| 22 |
-
def _seed_context(seed: int | None):
|
| 23 |
-
if seed is None:
|
| 24 |
-
yield
|
| 25 |
-
return
|
| 26 |
-
py_state = random.getstate()
|
| 27 |
-
np_state = np.random.get_state()
|
| 28 |
-
torch_state = torch.random.get_rng_state()
|
| 29 |
-
cuda_state = torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None
|
| 30 |
-
random.seed(seed)
|
| 31 |
-
np.random.seed(seed)
|
| 32 |
-
torch.manual_seed(seed)
|
| 33 |
-
if torch.cuda.is_available():
|
| 34 |
-
torch.cuda.manual_seed_all(seed)
|
| 35 |
-
try:
|
| 36 |
-
yield
|
| 37 |
-
finally:
|
| 38 |
-
random.setstate(py_state)
|
| 39 |
-
np.random.set_state(np_state)
|
| 40 |
-
torch.random.set_rng_state(torch_state)
|
| 41 |
-
if cuda_state is not None:
|
| 42 |
-
torch.cuda.set_rng_state_all(cuda_state)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def clean_esmfold2_input(input: StructurePredictionInput) -> StructurePredictionInput:
|
| 46 |
-
"""Group identical protein sequences into the same ProteinInput with multiple ids.
|
| 47 |
-
|
| 48 |
-
Example: Passing a tetramer like [ProteinInput(id=["0"], seq="AAA|AAA|BBB|BBB")]
|
| 49 |
-
gets converted into [ProteinInput(id=["0_0", "0_1"], seq="AAA"),
|
| 50 |
-
ProteinInput(id=["0_2", "0_3"], seq="BBB")]
|
| 51 |
-
|
| 52 |
-
Preserves the original order of unique sequences. Also converts "|" chainbreak
|
| 53 |
-
tokens to ":" in the sequence.
|
| 54 |
-
"""
|
| 55 |
-
cleaned_sequences: list = []
|
| 56 |
-
chain_to_ids: dict[str, list[str]] = {}
|
| 57 |
-
chain_to_modifications: dict[str, list] = {}
|
| 58 |
-
chain_to_msa: dict[str, MSA | None] = {}
|
| 59 |
-
|
| 60 |
-
for item in input.sequences:
|
| 61 |
-
if isinstance(item, ProteinInput):
|
| 62 |
-
sequence = ":".join(item.sequence.split("|"))
|
| 63 |
-
if ":" not in sequence:
|
| 64 |
-
cleaned_sequences.append(item)
|
| 65 |
-
continue
|
| 66 |
-
|
| 67 |
-
if ":" in sequence and input.covalent_bonds is not None:
|
| 68 |
-
raise ValueError(
|
| 69 |
-
"Covalent bonds are not supported when using chainbreaks. "
|
| 70 |
-
"Chains must be separated into multiple ProteinInput objects."
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
base_id = item.id[0] if isinstance(item.id, list) else item.id
|
| 74 |
-
chain_to_ids = {}
|
| 75 |
-
chain_to_modifications = {}
|
| 76 |
-
chain_to_msa = {}
|
| 77 |
-
chains = sequence.split(":")
|
| 78 |
-
|
| 79 |
-
chain_start_positions = []
|
| 80 |
-
pos = 0
|
| 81 |
-
for chain in chains:
|
| 82 |
-
chain_start_positions.append(pos)
|
| 83 |
-
pos += len(chain) + 1
|
| 84 |
-
|
| 85 |
-
if item.modifications is not None:
|
| 86 |
-
for chain_idx, chain in enumerate(chains):
|
| 87 |
-
chain_start = chain_start_positions[chain_idx]
|
| 88 |
-
chain_end = chain_start + len(chain)
|
| 89 |
-
chain_modifications = []
|
| 90 |
-
for mod in item.modifications:
|
| 91 |
-
if chain_start <= mod.position < chain_end:
|
| 92 |
-
adjusted_mod = Modification(
|
| 93 |
-
position=mod.position - chain_start, ccd=mod.ccd
|
| 94 |
-
)
|
| 95 |
-
chain_modifications.append(adjusted_mod)
|
| 96 |
-
if chain not in chain_to_modifications:
|
| 97 |
-
chain_to_modifications[chain] = chain_modifications
|
| 98 |
-
else:
|
| 99 |
-
chain_to_modifications[chain].extend(chain_modifications)
|
| 100 |
-
|
| 101 |
-
if item.msa is not None:
|
| 102 |
-
for chain_idx, chain in enumerate(chains):
|
| 103 |
-
if chain not in chain_to_msa:
|
| 104 |
-
chain_start = chain_start_positions[chain_idx]
|
| 105 |
-
chain_end = chain_start + len(chain)
|
| 106 |
-
chain_msa = item.msa.select_positions( # type: ignore
|
| 107 |
-
np.arange(chain_start, chain_end)
|
| 108 |
-
)
|
| 109 |
-
chain_to_msa[chain] = chain_msa
|
| 110 |
-
|
| 111 |
-
for i, chain in enumerate(chains):
|
| 112 |
-
chain_id = base_id + "_" + str(i)
|
| 113 |
-
if chain in chain_to_ids:
|
| 114 |
-
chain_to_ids[chain].append(chain_id)
|
| 115 |
-
else:
|
| 116 |
-
chain_to_ids[chain] = [chain_id]
|
| 117 |
-
cleaned_sequences.append((item, chain))
|
| 118 |
-
else:
|
| 119 |
-
cleaned_sequences.append(item)
|
| 120 |
-
|
| 121 |
-
for i in range(len(cleaned_sequences)):
|
| 122 |
-
if isinstance(cleaned_sequences[i], tuple):
|
| 123 |
-
item, chain = cleaned_sequences[i]
|
| 124 |
-
chain_ids = chain_to_ids[chain]
|
| 125 |
-
chain_modifications = (
|
| 126 |
-
chain_to_modifications.get(chain) if item.modifications else None
|
| 127 |
-
)
|
| 128 |
-
chain_msa = chain_to_msa.get(chain) if item.msa else None
|
| 129 |
-
cleaned_sequences[i] = ProteinInput(
|
| 130 |
-
id=chain_ids,
|
| 131 |
-
sequence=chain,
|
| 132 |
-
msa=chain_msa,
|
| 133 |
-
modifications=chain_modifications,
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
return StructurePredictionInput(
|
| 137 |
-
sequences=cleaned_sequences,
|
| 138 |
-
distogram_conditioning=input.distogram_conditioning,
|
| 139 |
-
covalent_bonds=input.covalent_bonds,
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
class ESMFold2InputBuilder:
|
| 144 |
-
def __init__(self, ccd_cache: Path | None = None):
|
| 145 |
-
load_ccd(ccd_cache)
|
| 146 |
-
|
| 147 |
-
def prepare_input(
|
| 148 |
-
self,
|
| 149 |
-
input: StructurePredictionInput,
|
| 150 |
-
seed: int | None = None,
|
| 151 |
-
device: torch.device | str | None = None,
|
| 152 |
-
) -> tuple[dict, list[ChainInfo]]:
|
| 153 |
-
"""Prepare raw input for the folding model.
|
| 154 |
-
|
| 155 |
-
Converts user-provided StructurePredictionInput into batched tensors
|
| 156 |
-
ready for model inference.
|
| 157 |
-
|
| 158 |
-
Parameters
|
| 159 |
-
----------
|
| 160 |
-
input : StructurePredictionInput
|
| 161 |
-
Input specification (sequences, structures, constraints, etc.).
|
| 162 |
-
seed : int, optional
|
| 163 |
-
Random seed for reproducibility.
|
| 164 |
-
device : torch.device or str, optional
|
| 165 |
-
Target device for the returned tensors. Defaults to CPU; pass
|
| 166 |
-
``model.device`` to skip a separate ``.to(...)`` step. ``fold()``
|
| 167 |
-
forwards ``model.device`` automatically.
|
| 168 |
-
|
| 169 |
-
Returns
|
| 170 |
-
-------
|
| 171 |
-
tuple[dict, list[ChainInfo]]
|
| 172 |
-
Batched input tensors and chain metadata for output processing.
|
| 173 |
-
"""
|
| 174 |
-
structure_prediction_input = clean_esmfold2_input(input)
|
| 175 |
-
with _seed_context(seed) if seed is not None else nullcontext():
|
| 176 |
-
features, chain_infos = prepare_esmfold2_input(
|
| 177 |
-
structure_prediction_input, seed=seed
|
| 178 |
-
)
|
| 179 |
-
features = {
|
| 180 |
-
k: (v[None].to(device) if device is not None else v[None])
|
| 181 |
-
if isinstance(v, torch.Tensor)
|
| 182 |
-
else v
|
| 183 |
-
for k, v in features.items()
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
return features, chain_infos
|
| 187 |
-
|
| 188 |
-
def __call__(
|
| 189 |
-
self,
|
| 190 |
-
input: StructurePredictionInput,
|
| 191 |
-
seed: int | None = None,
|
| 192 |
-
device: torch.device | str | None = None,
|
| 193 |
-
) -> tuple[dict, list[ChainInfo]]:
|
| 194 |
-
return self.prepare_input(input, seed=seed, device=device)
|
| 195 |
-
|
| 196 |
-
def decode(
|
| 197 |
-
self,
|
| 198 |
-
output: dict[str, torch.Tensor],
|
| 199 |
-
features: dict[str, torch.Tensor],
|
| 200 |
-
chain_infos: list[ChainInfo],
|
| 201 |
-
*,
|
| 202 |
-
num_diffusion_samples: int = 1,
|
| 203 |
-
complex_id: str = "pred",
|
| 204 |
-
) -> MolecularComplexResult | list[MolecularComplexResult]:
|
| 205 |
-
"""Convert raw model outputs into one MolecularComplexResult per sample.
|
| 206 |
-
|
| 207 |
-
Parameters
|
| 208 |
-
----------
|
| 209 |
-
output : dict[str, Tensor]
|
| 210 |
-
Output dict returned by ESMFold2Model.forward.
|
| 211 |
-
features : dict[str, Tensor]
|
| 212 |
-
Feature dict from :meth:`prepare_input` (batched, on the model device).
|
| 213 |
-
chain_infos : list[ChainInfo]
|
| 214 |
-
Chain metadata returned alongside `features`.
|
| 215 |
-
num_diffusion_samples : int
|
| 216 |
-
Number of diffusion samples present in the output (Bm = B * num_diffusion_samples).
|
| 217 |
-
complex_id : str
|
| 218 |
-
Identifier assigned to each MolecularComplex.
|
| 219 |
-
|
| 220 |
-
Returns
|
| 221 |
-
-------
|
| 222 |
-
MolecularComplexResult or list[MolecularComplexResult]
|
| 223 |
-
A single result when num_diffusion_samples == 1, otherwise a list of length Bm.
|
| 224 |
-
"""
|
| 225 |
-
atom_mask = features["atom_attention_mask"][0]
|
| 226 |
-
ref_element = features["ref_element"][0]
|
| 227 |
-
ref_atom_name_chars = features["ref_atom_name_chars"][0]
|
| 228 |
-
|
| 229 |
-
sample_coords = output["sample_atom_coords"]
|
| 230 |
-
plddts = output["plddt"]
|
| 231 |
-
Bm = sample_coords.shape[0]
|
| 232 |
-
|
| 233 |
-
ptm_t = output.get("ptm")
|
| 234 |
-
iptm_t = output.get("iptm")
|
| 235 |
-
pae_t = output.get("pae")
|
| 236 |
-
distogram_t = output.get("distogram_logits")
|
| 237 |
-
pair_chains_t = output.get("pair_chains_iptm")
|
| 238 |
-
residue_index_t = output.get("residue_index")
|
| 239 |
-
entity_id_t = output.get("entity_id")
|
| 240 |
-
|
| 241 |
-
results: list[MolecularComplexResult] = []
|
| 242 |
-
for i in range(Bm):
|
| 243 |
-
mc = build_molecular_complex_from_features(
|
| 244 |
-
coords=sample_coords[i],
|
| 245 |
-
plddt=plddts[i],
|
| 246 |
-
atom_mask=atom_mask,
|
| 247 |
-
ref_element=ref_element,
|
| 248 |
-
ref_atom_name_chars=ref_atom_name_chars,
|
| 249 |
-
chain_infos=chain_infos,
|
| 250 |
-
complex_id=complex_id,
|
| 251 |
-
)
|
| 252 |
-
results.append(
|
| 253 |
-
MolecularComplexResult(
|
| 254 |
-
complex=mc,
|
| 255 |
-
plddt=plddts[i].detach().cpu(),
|
| 256 |
-
ptm=float(ptm_t[i].item()) if ptm_t is not None else None,
|
| 257 |
-
iptm=float(iptm_t[i].item()) if iptm_t is not None else None,
|
| 258 |
-
pae=pae_t[i].detach().cpu() if pae_t is not None else None,
|
| 259 |
-
distogram=(
|
| 260 |
-
distogram_t[0].detach().cpu()
|
| 261 |
-
if distogram_t is not None
|
| 262 |
-
else None
|
| 263 |
-
),
|
| 264 |
-
pair_chains_iptm=(
|
| 265 |
-
pair_chains_t[i].detach().cpu()
|
| 266 |
-
if pair_chains_t is not None
|
| 267 |
-
else None
|
| 268 |
-
),
|
| 269 |
-
residue_index=(
|
| 270 |
-
residue_index_t[0].detach().cpu()
|
| 271 |
-
if residue_index_t is not None
|
| 272 |
-
else None
|
| 273 |
-
),
|
| 274 |
-
entity_id=(
|
| 275 |
-
entity_id_t[0].detach().cpu()
|
| 276 |
-
if entity_id_t is not None
|
| 277 |
-
else None
|
| 278 |
-
),
|
| 279 |
-
)
|
| 280 |
-
)
|
| 281 |
-
|
| 282 |
-
if num_diffusion_samples == 1 and len(results) == 1:
|
| 283 |
-
return results[0]
|
| 284 |
-
return results
|
| 285 |
-
|
| 286 |
-
def fold(
|
| 287 |
-
self,
|
| 288 |
-
model: Any,
|
| 289 |
-
input: StructurePredictionInput,
|
| 290 |
-
*,
|
| 291 |
-
num_loops: int = 3,
|
| 292 |
-
num_sampling_steps: int = 200,
|
| 293 |
-
num_diffusion_samples: int = 1,
|
| 294 |
-
seed: int | None = None,
|
| 295 |
-
noise_scale: float | None = None,
|
| 296 |
-
step_scale: float | None = None,
|
| 297 |
-
max_inference_sigma: int | None = None,
|
| 298 |
-
early_exit: bool = False,
|
| 299 |
-
complex_id: str = "pred",
|
| 300 |
-
) -> MolecularComplexResult | list[MolecularComplexResult]:
|
| 301 |
-
"""Fold a structure end-to-end: encode → model → decode.
|
| 302 |
-
|
| 303 |
-
Parameters
|
| 304 |
-
----------
|
| 305 |
-
model : ESMFold2Model
|
| 306 |
-
The folding model. Must already be on the target device and in eval mode.
|
| 307 |
-
input : StructurePredictionInput
|
| 308 |
-
User-facing input specification.
|
| 309 |
-
num_loops, num_sampling_steps, num_diffusion_samples : int
|
| 310 |
-
Inference knobs forwarded to the model.
|
| 311 |
-
seed : int, optional
|
| 312 |
-
Seeds both input prep (SMILES conformer generation) and diffusion sampling.
|
| 313 |
-
noise_scale, step_scale, max_inference_sigma, early_exit
|
| 314 |
-
Optional sampler overrides forwarded to the model when not None.
|
| 315 |
-
complex_id : str
|
| 316 |
-
Identifier assigned to the predicted MolecularComplex(es).
|
| 317 |
-
|
| 318 |
-
Returns
|
| 319 |
-
-------
|
| 320 |
-
MolecularComplexResult or list[MolecularComplexResult]
|
| 321 |
-
A single result when num_diffusion_samples == 1, otherwise a list.
|
| 322 |
-
"""
|
| 323 |
-
features, chain_infos = self.prepare_input(
|
| 324 |
-
input, seed=seed, device=model.device
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
sampler_kwargs: dict[str, Any] = {}
|
| 328 |
-
if noise_scale is not None:
|
| 329 |
-
sampler_kwargs["noise_scale"] = noise_scale
|
| 330 |
-
if step_scale is not None:
|
| 331 |
-
sampler_kwargs["step_scale"] = step_scale
|
| 332 |
-
if max_inference_sigma is not None:
|
| 333 |
-
sampler_kwargs["max_inference_sigma"] = max_inference_sigma
|
| 334 |
-
|
| 335 |
-
with torch.no_grad():
|
| 336 |
-
with _seed_context(seed) if seed is not None else nullcontext():
|
| 337 |
-
output = model(
|
| 338 |
-
**features,
|
| 339 |
-
num_loops=num_loops,
|
| 340 |
-
num_sampling_steps=num_sampling_steps,
|
| 341 |
-
num_diffusion_samples=num_diffusion_samples,
|
| 342 |
-
early_exit=early_exit,
|
| 343 |
-
**sampler_kwargs,
|
| 344 |
-
)
|
| 345 |
-
|
| 346 |
-
return self.decode(
|
| 347 |
-
output,
|
| 348 |
-
features,
|
| 349 |
-
chain_infos,
|
| 350 |
-
num_diffusion_samples=num_diffusion_samples,
|
| 351 |
-
complex_id=complex_id,
|
| 352 |
-
)
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
__all__ = ["ESMFold2InputBuilder", "clean_esmfold2_input"]
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from contextlib import contextmanager, nullcontext
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from .esmfold2_conformers import load_ccd
|
| 10 |
+
from .esmfold2_output import build_molecular_complex_from_features
|
| 11 |
+
from .esmfold2_prepare_input import ChainInfo, prepare_esmfold2_input
|
| 12 |
+
from .esmfold2_types import (
|
| 13 |
+
MSA,
|
| 14 |
+
Modification,
|
| 15 |
+
ProteinInput,
|
| 16 |
+
StructurePredictionInput,
|
| 17 |
+
)
|
| 18 |
+
from .esmfold2_molecular_complex import MolecularComplexResult
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@contextmanager
|
| 22 |
+
def _seed_context(seed: int | None):
|
| 23 |
+
if seed is None:
|
| 24 |
+
yield
|
| 25 |
+
return
|
| 26 |
+
py_state = random.getstate()
|
| 27 |
+
np_state = np.random.get_state()
|
| 28 |
+
torch_state = torch.random.get_rng_state()
|
| 29 |
+
cuda_state = torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None
|
| 30 |
+
random.seed(seed)
|
| 31 |
+
np.random.seed(seed)
|
| 32 |
+
torch.manual_seed(seed)
|
| 33 |
+
if torch.cuda.is_available():
|
| 34 |
+
torch.cuda.manual_seed_all(seed)
|
| 35 |
+
try:
|
| 36 |
+
yield
|
| 37 |
+
finally:
|
| 38 |
+
random.setstate(py_state)
|
| 39 |
+
np.random.set_state(np_state)
|
| 40 |
+
torch.random.set_rng_state(torch_state)
|
| 41 |
+
if cuda_state is not None:
|
| 42 |
+
torch.cuda.set_rng_state_all(cuda_state)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def clean_esmfold2_input(input: StructurePredictionInput) -> StructurePredictionInput:
|
| 46 |
+
"""Group identical protein sequences into the same ProteinInput with multiple ids.
|
| 47 |
+
|
| 48 |
+
Example: Passing a tetramer like [ProteinInput(id=["0"], seq="AAA|AAA|BBB|BBB")]
|
| 49 |
+
gets converted into [ProteinInput(id=["0_0", "0_1"], seq="AAA"),
|
| 50 |
+
ProteinInput(id=["0_2", "0_3"], seq="BBB")]
|
| 51 |
+
|
| 52 |
+
Preserves the original order of unique sequences. Also converts "|" chainbreak
|
| 53 |
+
tokens to ":" in the sequence.
|
| 54 |
+
"""
|
| 55 |
+
cleaned_sequences: list = []
|
| 56 |
+
chain_to_ids: dict[str, list[str]] = {}
|
| 57 |
+
chain_to_modifications: dict[str, list] = {}
|
| 58 |
+
chain_to_msa: dict[str, MSA | None] = {}
|
| 59 |
+
|
| 60 |
+
for item in input.sequences:
|
| 61 |
+
if isinstance(item, ProteinInput):
|
| 62 |
+
sequence = ":".join(item.sequence.split("|"))
|
| 63 |
+
if ":" not in sequence:
|
| 64 |
+
cleaned_sequences.append(item)
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
if ":" in sequence and input.covalent_bonds is not None:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
"Covalent bonds are not supported when using chainbreaks. "
|
| 70 |
+
"Chains must be separated into multiple ProteinInput objects."
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
base_id = item.id[0] if isinstance(item.id, list) else item.id
|
| 74 |
+
chain_to_ids = {}
|
| 75 |
+
chain_to_modifications = {}
|
| 76 |
+
chain_to_msa = {}
|
| 77 |
+
chains = sequence.split(":")
|
| 78 |
+
|
| 79 |
+
chain_start_positions = []
|
| 80 |
+
pos = 0
|
| 81 |
+
for chain in chains:
|
| 82 |
+
chain_start_positions.append(pos)
|
| 83 |
+
pos += len(chain) + 1
|
| 84 |
+
|
| 85 |
+
if item.modifications is not None:
|
| 86 |
+
for chain_idx, chain in enumerate(chains):
|
| 87 |
+
chain_start = chain_start_positions[chain_idx]
|
| 88 |
+
chain_end = chain_start + len(chain)
|
| 89 |
+
chain_modifications = []
|
| 90 |
+
for mod in item.modifications:
|
| 91 |
+
if chain_start <= mod.position < chain_end:
|
| 92 |
+
adjusted_mod = Modification(
|
| 93 |
+
position=mod.position - chain_start, ccd=mod.ccd
|
| 94 |
+
)
|
| 95 |
+
chain_modifications.append(adjusted_mod)
|
| 96 |
+
if chain not in chain_to_modifications:
|
| 97 |
+
chain_to_modifications[chain] = chain_modifications
|
| 98 |
+
else:
|
| 99 |
+
chain_to_modifications[chain].extend(chain_modifications)
|
| 100 |
+
|
| 101 |
+
if item.msa is not None:
|
| 102 |
+
for chain_idx, chain in enumerate(chains):
|
| 103 |
+
if chain not in chain_to_msa:
|
| 104 |
+
chain_start = chain_start_positions[chain_idx]
|
| 105 |
+
chain_end = chain_start + len(chain)
|
| 106 |
+
chain_msa = item.msa.select_positions( # type: ignore
|
| 107 |
+
np.arange(chain_start, chain_end)
|
| 108 |
+
)
|
| 109 |
+
chain_to_msa[chain] = chain_msa
|
| 110 |
+
|
| 111 |
+
for i, chain in enumerate(chains):
|
| 112 |
+
chain_id = base_id + "_" + str(i)
|
| 113 |
+
if chain in chain_to_ids:
|
| 114 |
+
chain_to_ids[chain].append(chain_id)
|
| 115 |
+
else:
|
| 116 |
+
chain_to_ids[chain] = [chain_id]
|
| 117 |
+
cleaned_sequences.append((item, chain))
|
| 118 |
+
else:
|
| 119 |
+
cleaned_sequences.append(item)
|
| 120 |
+
|
| 121 |
+
for i in range(len(cleaned_sequences)):
|
| 122 |
+
if isinstance(cleaned_sequences[i], tuple):
|
| 123 |
+
item, chain = cleaned_sequences[i]
|
| 124 |
+
chain_ids = chain_to_ids[chain]
|
| 125 |
+
chain_modifications = (
|
| 126 |
+
chain_to_modifications.get(chain) if item.modifications else None
|
| 127 |
+
)
|
| 128 |
+
chain_msa = chain_to_msa.get(chain) if item.msa else None
|
| 129 |
+
cleaned_sequences[i] = ProteinInput(
|
| 130 |
+
id=chain_ids,
|
| 131 |
+
sequence=chain,
|
| 132 |
+
msa=chain_msa,
|
| 133 |
+
modifications=chain_modifications,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
return StructurePredictionInput(
|
| 137 |
+
sequences=cleaned_sequences,
|
| 138 |
+
distogram_conditioning=input.distogram_conditioning,
|
| 139 |
+
covalent_bonds=input.covalent_bonds,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class ESMFold2InputBuilder:
|
| 144 |
+
def __init__(self, ccd_cache: Path | None = None):
|
| 145 |
+
load_ccd(ccd_cache)
|
| 146 |
+
|
| 147 |
+
def prepare_input(
|
| 148 |
+
self,
|
| 149 |
+
input: StructurePredictionInput,
|
| 150 |
+
seed: int | None = None,
|
| 151 |
+
device: torch.device | str | None = None,
|
| 152 |
+
) -> tuple[dict, list[ChainInfo]]:
|
| 153 |
+
"""Prepare raw input for the folding model.
|
| 154 |
+
|
| 155 |
+
Converts user-provided StructurePredictionInput into batched tensors
|
| 156 |
+
ready for model inference.
|
| 157 |
+
|
| 158 |
+
Parameters
|
| 159 |
+
----------
|
| 160 |
+
input : StructurePredictionInput
|
| 161 |
+
Input specification (sequences, structures, constraints, etc.).
|
| 162 |
+
seed : int, optional
|
| 163 |
+
Random seed for reproducibility.
|
| 164 |
+
device : torch.device or str, optional
|
| 165 |
+
Target device for the returned tensors. Defaults to CPU; pass
|
| 166 |
+
``model.device`` to skip a separate ``.to(...)`` step. ``fold()``
|
| 167 |
+
forwards ``model.device`` automatically.
|
| 168 |
+
|
| 169 |
+
Returns
|
| 170 |
+
-------
|
| 171 |
+
tuple[dict, list[ChainInfo]]
|
| 172 |
+
Batched input tensors and chain metadata for output processing.
|
| 173 |
+
"""
|
| 174 |
+
structure_prediction_input = clean_esmfold2_input(input)
|
| 175 |
+
with _seed_context(seed) if seed is not None else nullcontext():
|
| 176 |
+
features, chain_infos = prepare_esmfold2_input(
|
| 177 |
+
structure_prediction_input, seed=seed
|
| 178 |
+
)
|
| 179 |
+
features = {
|
| 180 |
+
k: (v[None].to(device) if device is not None else v[None])
|
| 181 |
+
if isinstance(v, torch.Tensor)
|
| 182 |
+
else v
|
| 183 |
+
for k, v in features.items()
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return features, chain_infos
|
| 187 |
+
|
| 188 |
+
def __call__(
|
| 189 |
+
self,
|
| 190 |
+
input: StructurePredictionInput,
|
| 191 |
+
seed: int | None = None,
|
| 192 |
+
device: torch.device | str | None = None,
|
| 193 |
+
) -> tuple[dict, list[ChainInfo]]:
|
| 194 |
+
return self.prepare_input(input, seed=seed, device=device)
|
| 195 |
+
|
| 196 |
+
def decode(
|
| 197 |
+
self,
|
| 198 |
+
output: dict[str, torch.Tensor],
|
| 199 |
+
features: dict[str, torch.Tensor],
|
| 200 |
+
chain_infos: list[ChainInfo],
|
| 201 |
+
*,
|
| 202 |
+
num_diffusion_samples: int = 1,
|
| 203 |
+
complex_id: str = "pred",
|
| 204 |
+
) -> MolecularComplexResult | list[MolecularComplexResult]:
|
| 205 |
+
"""Convert raw model outputs into one MolecularComplexResult per sample.
|
| 206 |
+
|
| 207 |
+
Parameters
|
| 208 |
+
----------
|
| 209 |
+
output : dict[str, Tensor]
|
| 210 |
+
Output dict returned by ESMFold2Model.forward.
|
| 211 |
+
features : dict[str, Tensor]
|
| 212 |
+
Feature dict from :meth:`prepare_input` (batched, on the model device).
|
| 213 |
+
chain_infos : list[ChainInfo]
|
| 214 |
+
Chain metadata returned alongside `features`.
|
| 215 |
+
num_diffusion_samples : int
|
| 216 |
+
Number of diffusion samples present in the output (Bm = B * num_diffusion_samples).
|
| 217 |
+
complex_id : str
|
| 218 |
+
Identifier assigned to each MolecularComplex.
|
| 219 |
+
|
| 220 |
+
Returns
|
| 221 |
+
-------
|
| 222 |
+
MolecularComplexResult or list[MolecularComplexResult]
|
| 223 |
+
A single result when num_diffusion_samples == 1, otherwise a list of length Bm.
|
| 224 |
+
"""
|
| 225 |
+
atom_mask = features["atom_attention_mask"][0]
|
| 226 |
+
ref_element = features["ref_element"][0]
|
| 227 |
+
ref_atom_name_chars = features["ref_atom_name_chars"][0]
|
| 228 |
+
|
| 229 |
+
sample_coords = output["sample_atom_coords"]
|
| 230 |
+
plddts = output["plddt"]
|
| 231 |
+
Bm = sample_coords.shape[0]
|
| 232 |
+
|
| 233 |
+
ptm_t = output.get("ptm")
|
| 234 |
+
iptm_t = output.get("iptm")
|
| 235 |
+
pae_t = output.get("pae")
|
| 236 |
+
distogram_t = output.get("distogram_logits")
|
| 237 |
+
pair_chains_t = output.get("pair_chains_iptm")
|
| 238 |
+
residue_index_t = output.get("residue_index")
|
| 239 |
+
entity_id_t = output.get("entity_id")
|
| 240 |
+
|
| 241 |
+
results: list[MolecularComplexResult] = []
|
| 242 |
+
for i in range(Bm):
|
| 243 |
+
mc = build_molecular_complex_from_features(
|
| 244 |
+
coords=sample_coords[i],
|
| 245 |
+
plddt=plddts[i],
|
| 246 |
+
atom_mask=atom_mask,
|
| 247 |
+
ref_element=ref_element,
|
| 248 |
+
ref_atom_name_chars=ref_atom_name_chars,
|
| 249 |
+
chain_infos=chain_infos,
|
| 250 |
+
complex_id=complex_id,
|
| 251 |
+
)
|
| 252 |
+
results.append(
|
| 253 |
+
MolecularComplexResult(
|
| 254 |
+
complex=mc,
|
| 255 |
+
plddt=plddts[i].detach().cpu(),
|
| 256 |
+
ptm=float(ptm_t[i].item()) if ptm_t is not None else None,
|
| 257 |
+
iptm=float(iptm_t[i].item()) if iptm_t is not None else None,
|
| 258 |
+
pae=pae_t[i].detach().cpu() if pae_t is not None else None,
|
| 259 |
+
distogram=(
|
| 260 |
+
distogram_t[0].detach().cpu()
|
| 261 |
+
if distogram_t is not None
|
| 262 |
+
else None
|
| 263 |
+
),
|
| 264 |
+
pair_chains_iptm=(
|
| 265 |
+
pair_chains_t[i].detach().cpu()
|
| 266 |
+
if pair_chains_t is not None
|
| 267 |
+
else None
|
| 268 |
+
),
|
| 269 |
+
residue_index=(
|
| 270 |
+
residue_index_t[0].detach().cpu()
|
| 271 |
+
if residue_index_t is not None
|
| 272 |
+
else None
|
| 273 |
+
),
|
| 274 |
+
entity_id=(
|
| 275 |
+
entity_id_t[0].detach().cpu()
|
| 276 |
+
if entity_id_t is not None
|
| 277 |
+
else None
|
| 278 |
+
),
|
| 279 |
+
)
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
if num_diffusion_samples == 1 and len(results) == 1:
|
| 283 |
+
return results[0]
|
| 284 |
+
return results
|
| 285 |
+
|
| 286 |
+
def fold(
|
| 287 |
+
self,
|
| 288 |
+
model: Any,
|
| 289 |
+
input: StructurePredictionInput,
|
| 290 |
+
*,
|
| 291 |
+
num_loops: int = 3,
|
| 292 |
+
num_sampling_steps: int = 200,
|
| 293 |
+
num_diffusion_samples: int = 1,
|
| 294 |
+
seed: int | None = None,
|
| 295 |
+
noise_scale: float | None = None,
|
| 296 |
+
step_scale: float | None = None,
|
| 297 |
+
max_inference_sigma: int | None = None,
|
| 298 |
+
early_exit: bool = False,
|
| 299 |
+
complex_id: str = "pred",
|
| 300 |
+
) -> MolecularComplexResult | list[MolecularComplexResult]:
|
| 301 |
+
"""Fold a structure end-to-end: encode → model → decode.
|
| 302 |
+
|
| 303 |
+
Parameters
|
| 304 |
+
----------
|
| 305 |
+
model : ESMFold2Model
|
| 306 |
+
The folding model. Must already be on the target device and in eval mode.
|
| 307 |
+
input : StructurePredictionInput
|
| 308 |
+
User-facing input specification.
|
| 309 |
+
num_loops, num_sampling_steps, num_diffusion_samples : int
|
| 310 |
+
Inference knobs forwarded to the model.
|
| 311 |
+
seed : int, optional
|
| 312 |
+
Seeds both input prep (SMILES conformer generation) and diffusion sampling.
|
| 313 |
+
noise_scale, step_scale, max_inference_sigma, early_exit
|
| 314 |
+
Optional sampler overrides forwarded to the model when not None.
|
| 315 |
+
complex_id : str
|
| 316 |
+
Identifier assigned to the predicted MolecularComplex(es).
|
| 317 |
+
|
| 318 |
+
Returns
|
| 319 |
+
-------
|
| 320 |
+
MolecularComplexResult or list[MolecularComplexResult]
|
| 321 |
+
A single result when num_diffusion_samples == 1, otherwise a list.
|
| 322 |
+
"""
|
| 323 |
+
features, chain_infos = self.prepare_input(
|
| 324 |
+
input, seed=seed, device=model.device
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
sampler_kwargs: dict[str, Any] = {}
|
| 328 |
+
if noise_scale is not None:
|
| 329 |
+
sampler_kwargs["noise_scale"] = noise_scale
|
| 330 |
+
if step_scale is not None:
|
| 331 |
+
sampler_kwargs["step_scale"] = step_scale
|
| 332 |
+
if max_inference_sigma is not None:
|
| 333 |
+
sampler_kwargs["max_inference_sigma"] = max_inference_sigma
|
| 334 |
+
|
| 335 |
+
with torch.no_grad():
|
| 336 |
+
with _seed_context(seed) if seed is not None else nullcontext():
|
| 337 |
+
output = model(
|
| 338 |
+
**features,
|
| 339 |
+
num_loops=num_loops,
|
| 340 |
+
num_sampling_steps=num_sampling_steps,
|
| 341 |
+
num_diffusion_samples=num_diffusion_samples,
|
| 342 |
+
early_exit=early_exit,
|
| 343 |
+
**sampler_kwargs,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
return self.decode(
|
| 347 |
+
output,
|
| 348 |
+
features,
|
| 349 |
+
chain_infos,
|
| 350 |
+
num_diffusion_samples=num_diffusion_samples,
|
| 351 |
+
complex_id=complex_id,
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
__all__ = ["ESMFold2InputBuilder", "clean_esmfold2_input"]
|
esmfold2_protein_chain.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
esmfold2_protein_complex.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
esmfold2_protein_structure.py
CHANGED
|
@@ -1,306 +1,306 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
from typing import Tuple, TypeVar
|
| 4 |
-
|
| 5 |
-
import numpy as np
|
| 6 |
-
import torch
|
| 7 |
-
import torch.nn.functional as F
|
| 8 |
-
from torch import Tensor
|
| 9 |
-
from torch.amp import autocast # type: ignore
|
| 10 |
-
|
| 11 |
-
from . import esmfold2_residue_constants as residue_constants
|
| 12 |
-
from .esmfold2_misc import unbinpack
|
| 13 |
-
from .esmfold2_affine3d import Affine3D
|
| 14 |
-
|
| 15 |
-
ArrayOrTensor = TypeVar("ArrayOrTensor", np.ndarray, Tensor)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def index_by_atom_name(
|
| 19 |
-
atom37: ArrayOrTensor, atom_names: str | list[str], dim: int = -2
|
| 20 |
-
) -> ArrayOrTensor:
|
| 21 |
-
squeeze = False
|
| 22 |
-
if isinstance(atom_names, str):
|
| 23 |
-
atom_names = [atom_names]
|
| 24 |
-
squeeze = True
|
| 25 |
-
indices = [residue_constants.atom_order[atom_name] for atom_name in atom_names]
|
| 26 |
-
dim = dim % atom37.ndim
|
| 27 |
-
index = tuple(slice(None) if dim != i else indices for i in range(atom37.ndim))
|
| 28 |
-
result = atom37[index] # type: ignore
|
| 29 |
-
if squeeze:
|
| 30 |
-
result = result.squeeze(dim)
|
| 31 |
-
return result
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def infer_cbeta_from_atom37(
|
| 35 |
-
atom37: ArrayOrTensor, L: float = 1.522, A: float = 1.927, D: float = -2.143
|
| 36 |
-
):
|
| 37 |
-
"""
|
| 38 |
-
Inspired by a util in trDesign:
|
| 39 |
-
https://github.com/gjoni/trDesign/blob/f2d5930b472e77bfacc2f437b3966e7a708a8d37/02-GD/utils.py#L92
|
| 40 |
-
|
| 41 |
-
input: atom37, (L)ength, (A)ngle, and (D)ihedral
|
| 42 |
-
output: 4th coord
|
| 43 |
-
"""
|
| 44 |
-
N = index_by_atom_name(atom37, "N", dim=-2)
|
| 45 |
-
CA = index_by_atom_name(atom37, "CA", dim=-2)
|
| 46 |
-
C = index_by_atom_name(atom37, "C", dim=-2)
|
| 47 |
-
|
| 48 |
-
if isinstance(atom37, np.ndarray):
|
| 49 |
-
|
| 50 |
-
def normalize(x: ArrayOrTensor):
|
| 51 |
-
return x / np.linalg.norm(x, axis=-1, keepdims=True)
|
| 52 |
-
|
| 53 |
-
cross = np.cross
|
| 54 |
-
else:
|
| 55 |
-
normalize = F.normalize # type: ignore
|
| 56 |
-
cross = torch.cross
|
| 57 |
-
|
| 58 |
-
with np.errstate(invalid="ignore"): # inf - inf = nan is ok here
|
| 59 |
-
vec_nca = N - CA
|
| 60 |
-
vec_nc = N - C
|
| 61 |
-
nca = normalize(vec_nca)
|
| 62 |
-
n = normalize(cross(vec_nc, nca)) # type: ignore
|
| 63 |
-
m = [nca, cross(n, nca), n]
|
| 64 |
-
d = [L * np.cos(A), L * np.sin(A) * np.cos(D), -L * np.sin(A) * np.sin(D)]
|
| 65 |
-
return CA + sum([m * d for m, d in zip(m, d)])
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
@torch.no_grad()
|
| 69 |
-
@autocast("cuda", enabled=False)
|
| 70 |
-
def compute_alignment_tensors(
|
| 71 |
-
mobile: torch.Tensor,
|
| 72 |
-
target: torch.Tensor,
|
| 73 |
-
atom_exists_mask: torch.Tensor | None = None,
|
| 74 |
-
sequence_id: torch.Tensor | None = None,
|
| 75 |
-
):
|
| 76 |
-
"""
|
| 77 |
-
Align two batches of structures with support for masking invalid atoms using PyTorch.
|
| 78 |
-
|
| 79 |
-
Args:
|
| 80 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 81 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 82 |
-
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 83 |
-
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 84 |
-
|
| 85 |
-
Returns:
|
| 86 |
-
- centered_mobile (torch.Tensor): Batch of coordinates of structure centered mobile (B, N, 3)
|
| 87 |
-
- centroid_mobile (torch.Tensor): Batch of coordinates of mobile centeroid (B, 3)
|
| 88 |
-
- centered_target (torch.Tensor): Batch of coordinates of structure centered target (B, N, 3)
|
| 89 |
-
- centroid_target (torch.Tensor): Batch of coordinates of target centeroid (B, 3)
|
| 90 |
-
- rotation_matrix (torch.Tensor): Batch of coordinates of rotation matrix (B, 3, 3)
|
| 91 |
-
- num_valid_atoms (torch.Tensor): Batch of number of valid atoms for alignment (B,)
|
| 92 |
-
"""
|
| 93 |
-
|
| 94 |
-
# Ensure both batches have the same number of structures, atoms, and dimensions
|
| 95 |
-
if sequence_id is not None:
|
| 96 |
-
mobile = unbinpack(mobile, sequence_id, pad_value=torch.nan)
|
| 97 |
-
target = unbinpack(target, sequence_id, pad_value=torch.nan)
|
| 98 |
-
if atom_exists_mask is not None:
|
| 99 |
-
atom_exists_mask = unbinpack(atom_exists_mask, sequence_id, pad_value=0)
|
| 100 |
-
else:
|
| 101 |
-
atom_exists_mask = torch.isfinite(target).all(-1)
|
| 102 |
-
|
| 103 |
-
assert mobile.shape == target.shape, "Batch structure shapes do not match!"
|
| 104 |
-
|
| 105 |
-
# Number of structures in the batch
|
| 106 |
-
batch_size = mobile.shape[0]
|
| 107 |
-
|
| 108 |
-
# if [B, Nres, Natom, 3], resize
|
| 109 |
-
if mobile.dim() == 4:
|
| 110 |
-
mobile = mobile.view(batch_size, -1, 3)
|
| 111 |
-
if target.dim() == 4:
|
| 112 |
-
target = target.view(batch_size, -1, 3)
|
| 113 |
-
if atom_exists_mask is not None and atom_exists_mask.dim() == 3:
|
| 114 |
-
atom_exists_mask = atom_exists_mask.view(batch_size, -1)
|
| 115 |
-
|
| 116 |
-
# Number of atoms
|
| 117 |
-
num_atoms = mobile.shape[1]
|
| 118 |
-
|
| 119 |
-
# Apply masks if provided
|
| 120 |
-
if atom_exists_mask is not None:
|
| 121 |
-
mobile = mobile.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 122 |
-
target = target.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 123 |
-
else:
|
| 124 |
-
atom_exists_mask = torch.ones(
|
| 125 |
-
batch_size, num_atoms, dtype=torch.bool, device=mobile.device
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
num_valid_atoms = atom_exists_mask.sum(dim=-1, keepdim=True)
|
| 129 |
-
# Compute centroids for each batch
|
| 130 |
-
centroid_mobile = mobile.sum(dim=-2, keepdim=True) / num_valid_atoms.unsqueeze(-1)
|
| 131 |
-
centroid_target = target.sum(dim=-2, keepdim=True) / num_valid_atoms.unsqueeze(-1)
|
| 132 |
-
|
| 133 |
-
# Handle potential division by zero if all atoms are invalid in a structure
|
| 134 |
-
centroid_mobile[num_valid_atoms == 0] = 0
|
| 135 |
-
centroid_target[num_valid_atoms == 0] = 0
|
| 136 |
-
|
| 137 |
-
# Center structures by subtracting centroids
|
| 138 |
-
centered_mobile = mobile - centroid_mobile
|
| 139 |
-
centered_target = target - centroid_target
|
| 140 |
-
|
| 141 |
-
centered_mobile = centered_mobile.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 142 |
-
centered_target = centered_target.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 143 |
-
|
| 144 |
-
# Compute covariance matrix for each batch
|
| 145 |
-
covariance_matrix = torch.matmul(centered_mobile.transpose(1, 2), centered_target)
|
| 146 |
-
|
| 147 |
-
# Singular Value Decomposition for each batch
|
| 148 |
-
u, _, v = torch.svd(covariance_matrix)
|
| 149 |
-
|
| 150 |
-
# Calculate rotation matrices for each batch
|
| 151 |
-
rotation_matrix = torch.matmul(u, v.transpose(1, 2))
|
| 152 |
-
|
| 153 |
-
return (
|
| 154 |
-
centered_mobile,
|
| 155 |
-
centroid_mobile,
|
| 156 |
-
centered_target,
|
| 157 |
-
centroid_target,
|
| 158 |
-
rotation_matrix,
|
| 159 |
-
num_valid_atoms,
|
| 160 |
-
)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
@torch.no_grad()
|
| 164 |
-
@autocast("cuda", enabled=False)
|
| 165 |
-
def compute_rmsd_no_alignment(
|
| 166 |
-
aligned: torch.Tensor,
|
| 167 |
-
target: torch.Tensor,
|
| 168 |
-
num_valid_atoms: torch.Tensor,
|
| 169 |
-
reduction: str = "batch",
|
| 170 |
-
) -> torch.Tensor:
|
| 171 |
-
"""
|
| 172 |
-
Compute RMSD between two batches of structures without alignment.
|
| 173 |
-
|
| 174 |
-
Args:
|
| 175 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 176 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 177 |
-
- num_valid_atoms (torch.Tensor): Batch of number of valid atoms for alignment (B,)
|
| 178 |
-
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 179 |
-
|
| 180 |
-
Returns:
|
| 181 |
-
|
| 182 |
-
If reduction == "batch":
|
| 183 |
-
(torch.Tensor): 0-dim, Average Root Mean Square Deviation between the structures for each batch
|
| 184 |
-
If reduction == "per_sample":
|
| 185 |
-
(torch.Tensor): (B,)-dim, Root Mean Square Deviation between the structures for each batch
|
| 186 |
-
If reduction == "per_residue":
|
| 187 |
-
(torch.Tensor): (B, N)-dim, Root Mean Square Deviation between the structures for residue in the batch
|
| 188 |
-
"""
|
| 189 |
-
if reduction not in ("per_residue", "per_sample", "batch"):
|
| 190 |
-
raise ValueError("Unrecognized reduction: '{reduction}'")
|
| 191 |
-
# Compute RMSD for each batch
|
| 192 |
-
diff = aligned - target
|
| 193 |
-
if reduction == "per_residue":
|
| 194 |
-
mean_squared_error = diff.square().view(diff.size(0), -1, 9).mean(dim=-1)
|
| 195 |
-
else:
|
| 196 |
-
mean_squared_error = diff.square().sum(dim=(1, 2)) / (
|
| 197 |
-
num_valid_atoms.squeeze(-1)
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
rmsd = torch.sqrt(mean_squared_error)
|
| 201 |
-
if reduction in ("per_sample", "per_residue"):
|
| 202 |
-
return rmsd
|
| 203 |
-
elif reduction == "batch":
|
| 204 |
-
avg_rmsd = rmsd.masked_fill(num_valid_atoms.squeeze(-1) == 0, 0).sum() / (
|
| 205 |
-
(num_valid_atoms > 0).sum() + 1e-8
|
| 206 |
-
)
|
| 207 |
-
return avg_rmsd
|
| 208 |
-
else:
|
| 209 |
-
raise ValueError(reduction)
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
@torch.no_grad()
|
| 213 |
-
@autocast("cuda", enabled=False)
|
| 214 |
-
def compute_affine_and_rmsd(
|
| 215 |
-
mobile: torch.Tensor,
|
| 216 |
-
target: torch.Tensor,
|
| 217 |
-
atom_exists_mask: torch.Tensor | None = None,
|
| 218 |
-
sequence_id: torch.Tensor | None = None,
|
| 219 |
-
) -> Tuple[Affine3D, torch.Tensor]:
|
| 220 |
-
"""
|
| 221 |
-
Compute RMSD between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 222 |
-
|
| 223 |
-
Args:
|
| 224 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 225 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 226 |
-
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 227 |
-
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 228 |
-
|
| 229 |
-
Returns:
|
| 230 |
-
- affine (Affine3D): Transformation between mobile and target structure
|
| 231 |
-
- avg_rmsd (torch.Tensor): Average Root Mean Square Deviation between the structures for each batch
|
| 232 |
-
"""
|
| 233 |
-
|
| 234 |
-
(
|
| 235 |
-
centered_mobile,
|
| 236 |
-
centroid_mobile,
|
| 237 |
-
centered_target,
|
| 238 |
-
centroid_target,
|
| 239 |
-
rotation_matrix,
|
| 240 |
-
num_valid_atoms,
|
| 241 |
-
) = compute_alignment_tensors(
|
| 242 |
-
mobile=mobile,
|
| 243 |
-
target=target,
|
| 244 |
-
atom_exists_mask=atom_exists_mask,
|
| 245 |
-
sequence_id=sequence_id,
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
# Apply rotation to mobile centroid
|
| 249 |
-
translation = torch.matmul(-centroid_mobile, rotation_matrix) + centroid_target
|
| 250 |
-
affine = Affine3D.from_tensor_pair(
|
| 251 |
-
translation, rotation_matrix.unsqueeze(dim=-3).transpose(-2, -1)
|
| 252 |
-
)
|
| 253 |
-
|
| 254 |
-
# Apply transformation to centered structure to compute rmsd
|
| 255 |
-
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 256 |
-
avg_rmsd = compute_rmsd_no_alignment(
|
| 257 |
-
rotated_mobile, centered_target, num_valid_atoms, reduction="batch"
|
| 258 |
-
)
|
| 259 |
-
|
| 260 |
-
return affine, avg_rmsd
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
def compute_gdt_ts_no_alignment(
|
| 264 |
-
aligned: torch.Tensor,
|
| 265 |
-
target: torch.Tensor,
|
| 266 |
-
atom_exists_mask: torch.Tensor,
|
| 267 |
-
reduction: str = "batch",
|
| 268 |
-
) -> torch.Tensor:
|
| 269 |
-
"""
|
| 270 |
-
Compute GDT_TS between two batches of structures without alignment.
|
| 271 |
-
|
| 272 |
-
Args:
|
| 273 |
-
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 274 |
-
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 275 |
-
- atom_exists_mask (torch.Tensor): Mask for Whether an atom exists of shape (B, N). noo
|
| 276 |
-
- reduction (str): One of "batch", "per_sample".
|
| 277 |
-
|
| 278 |
-
Returns:
|
| 279 |
-
If reduction == "batch":
|
| 280 |
-
(torch.Tensor): 0-dim, GDT_TS between the structures for each batch
|
| 281 |
-
If reduction == "per_sample":
|
| 282 |
-
(torch.Tensor): (B,)-dim, GDT_TS between the structures for each sample in the batch
|
| 283 |
-
"""
|
| 284 |
-
if reduction not in ("per_sample", "batch"):
|
| 285 |
-
raise ValueError("Unrecognized reduction: '{reduction}'")
|
| 286 |
-
|
| 287 |
-
if atom_exists_mask is None:
|
| 288 |
-
atom_exists_mask = torch.isfinite(target).all(dim=-1)
|
| 289 |
-
|
| 290 |
-
deviation = torch.linalg.vector_norm(aligned - target, dim=-1)
|
| 291 |
-
num_valid_atoms = atom_exists_mask.sum(dim=-1)
|
| 292 |
-
|
| 293 |
-
# Compute GDT_TS
|
| 294 |
-
score = (
|
| 295 |
-
((deviation < 1) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 296 |
-
+ ((deviation < 2) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 297 |
-
+ ((deviation < 4) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 298 |
-
+ ((deviation < 8) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 299 |
-
) * 0.25
|
| 300 |
-
|
| 301 |
-
if reduction == "batch":
|
| 302 |
-
return score.mean()
|
| 303 |
-
elif reduction == "per_sample":
|
| 304 |
-
return score
|
| 305 |
-
else:
|
| 306 |
-
raise ValueError("Unrecognized reduction: '{reduction}'")
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Tuple, TypeVar
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
from torch.amp import autocast # type: ignore
|
| 10 |
+
|
| 11 |
+
from . import esmfold2_residue_constants as residue_constants
|
| 12 |
+
from .esmfold2_misc import unbinpack
|
| 13 |
+
from .esmfold2_affine3d import Affine3D
|
| 14 |
+
|
| 15 |
+
ArrayOrTensor = TypeVar("ArrayOrTensor", np.ndarray, Tensor)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def index_by_atom_name(
|
| 19 |
+
atom37: ArrayOrTensor, atom_names: str | list[str], dim: int = -2
|
| 20 |
+
) -> ArrayOrTensor:
|
| 21 |
+
squeeze = False
|
| 22 |
+
if isinstance(atom_names, str):
|
| 23 |
+
atom_names = [atom_names]
|
| 24 |
+
squeeze = True
|
| 25 |
+
indices = [residue_constants.atom_order[atom_name] for atom_name in atom_names]
|
| 26 |
+
dim = dim % atom37.ndim
|
| 27 |
+
index = tuple(slice(None) if dim != i else indices for i in range(atom37.ndim))
|
| 28 |
+
result = atom37[index] # type: ignore
|
| 29 |
+
if squeeze:
|
| 30 |
+
result = result.squeeze(dim)
|
| 31 |
+
return result
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def infer_cbeta_from_atom37(
|
| 35 |
+
atom37: ArrayOrTensor, L: float = 1.522, A: float = 1.927, D: float = -2.143
|
| 36 |
+
):
|
| 37 |
+
"""
|
| 38 |
+
Inspired by a util in trDesign:
|
| 39 |
+
https://github.com/gjoni/trDesign/blob/f2d5930b472e77bfacc2f437b3966e7a708a8d37/02-GD/utils.py#L92
|
| 40 |
+
|
| 41 |
+
input: atom37, (L)ength, (A)ngle, and (D)ihedral
|
| 42 |
+
output: 4th coord
|
| 43 |
+
"""
|
| 44 |
+
N = index_by_atom_name(atom37, "N", dim=-2)
|
| 45 |
+
CA = index_by_atom_name(atom37, "CA", dim=-2)
|
| 46 |
+
C = index_by_atom_name(atom37, "C", dim=-2)
|
| 47 |
+
|
| 48 |
+
if isinstance(atom37, np.ndarray):
|
| 49 |
+
|
| 50 |
+
def normalize(x: ArrayOrTensor):
|
| 51 |
+
return x / np.linalg.norm(x, axis=-1, keepdims=True)
|
| 52 |
+
|
| 53 |
+
cross = np.cross
|
| 54 |
+
else:
|
| 55 |
+
normalize = F.normalize # type: ignore
|
| 56 |
+
cross = torch.cross
|
| 57 |
+
|
| 58 |
+
with np.errstate(invalid="ignore"): # inf - inf = nan is ok here
|
| 59 |
+
vec_nca = N - CA
|
| 60 |
+
vec_nc = N - C
|
| 61 |
+
nca = normalize(vec_nca)
|
| 62 |
+
n = normalize(cross(vec_nc, nca)) # type: ignore
|
| 63 |
+
m = [nca, cross(n, nca), n]
|
| 64 |
+
d = [L * np.cos(A), L * np.sin(A) * np.cos(D), -L * np.sin(A) * np.sin(D)]
|
| 65 |
+
return CA + sum([m * d for m, d in zip(m, d)])
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@torch.no_grad()
|
| 69 |
+
@autocast("cuda", enabled=False)
|
| 70 |
+
def compute_alignment_tensors(
|
| 71 |
+
mobile: torch.Tensor,
|
| 72 |
+
target: torch.Tensor,
|
| 73 |
+
atom_exists_mask: torch.Tensor | None = None,
|
| 74 |
+
sequence_id: torch.Tensor | None = None,
|
| 75 |
+
):
|
| 76 |
+
"""
|
| 77 |
+
Align two batches of structures with support for masking invalid atoms using PyTorch.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 81 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 82 |
+
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 83 |
+
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
- centered_mobile (torch.Tensor): Batch of coordinates of structure centered mobile (B, N, 3)
|
| 87 |
+
- centroid_mobile (torch.Tensor): Batch of coordinates of mobile centeroid (B, 3)
|
| 88 |
+
- centered_target (torch.Tensor): Batch of coordinates of structure centered target (B, N, 3)
|
| 89 |
+
- centroid_target (torch.Tensor): Batch of coordinates of target centeroid (B, 3)
|
| 90 |
+
- rotation_matrix (torch.Tensor): Batch of coordinates of rotation matrix (B, 3, 3)
|
| 91 |
+
- num_valid_atoms (torch.Tensor): Batch of number of valid atoms for alignment (B,)
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
# Ensure both batches have the same number of structures, atoms, and dimensions
|
| 95 |
+
if sequence_id is not None:
|
| 96 |
+
mobile = unbinpack(mobile, sequence_id, pad_value=torch.nan)
|
| 97 |
+
target = unbinpack(target, sequence_id, pad_value=torch.nan)
|
| 98 |
+
if atom_exists_mask is not None:
|
| 99 |
+
atom_exists_mask = unbinpack(atom_exists_mask, sequence_id, pad_value=0)
|
| 100 |
+
else:
|
| 101 |
+
atom_exists_mask = torch.isfinite(target).all(-1)
|
| 102 |
+
|
| 103 |
+
assert mobile.shape == target.shape, "Batch structure shapes do not match!"
|
| 104 |
+
|
| 105 |
+
# Number of structures in the batch
|
| 106 |
+
batch_size = mobile.shape[0]
|
| 107 |
+
|
| 108 |
+
# if [B, Nres, Natom, 3], resize
|
| 109 |
+
if mobile.dim() == 4:
|
| 110 |
+
mobile = mobile.view(batch_size, -1, 3)
|
| 111 |
+
if target.dim() == 4:
|
| 112 |
+
target = target.view(batch_size, -1, 3)
|
| 113 |
+
if atom_exists_mask is not None and atom_exists_mask.dim() == 3:
|
| 114 |
+
atom_exists_mask = atom_exists_mask.view(batch_size, -1)
|
| 115 |
+
|
| 116 |
+
# Number of atoms
|
| 117 |
+
num_atoms = mobile.shape[1]
|
| 118 |
+
|
| 119 |
+
# Apply masks if provided
|
| 120 |
+
if atom_exists_mask is not None:
|
| 121 |
+
mobile = mobile.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 122 |
+
target = target.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 123 |
+
else:
|
| 124 |
+
atom_exists_mask = torch.ones(
|
| 125 |
+
batch_size, num_atoms, dtype=torch.bool, device=mobile.device
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
num_valid_atoms = atom_exists_mask.sum(dim=-1, keepdim=True)
|
| 129 |
+
# Compute centroids for each batch
|
| 130 |
+
centroid_mobile = mobile.sum(dim=-2, keepdim=True) / num_valid_atoms.unsqueeze(-1)
|
| 131 |
+
centroid_target = target.sum(dim=-2, keepdim=True) / num_valid_atoms.unsqueeze(-1)
|
| 132 |
+
|
| 133 |
+
# Handle potential division by zero if all atoms are invalid in a structure
|
| 134 |
+
centroid_mobile[num_valid_atoms == 0] = 0
|
| 135 |
+
centroid_target[num_valid_atoms == 0] = 0
|
| 136 |
+
|
| 137 |
+
# Center structures by subtracting centroids
|
| 138 |
+
centered_mobile = mobile - centroid_mobile
|
| 139 |
+
centered_target = target - centroid_target
|
| 140 |
+
|
| 141 |
+
centered_mobile = centered_mobile.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 142 |
+
centered_target = centered_target.masked_fill(~atom_exists_mask.unsqueeze(-1), 0)
|
| 143 |
+
|
| 144 |
+
# Compute covariance matrix for each batch
|
| 145 |
+
covariance_matrix = torch.matmul(centered_mobile.transpose(1, 2), centered_target)
|
| 146 |
+
|
| 147 |
+
# Singular Value Decomposition for each batch
|
| 148 |
+
u, _, v = torch.svd(covariance_matrix)
|
| 149 |
+
|
| 150 |
+
# Calculate rotation matrices for each batch
|
| 151 |
+
rotation_matrix = torch.matmul(u, v.transpose(1, 2))
|
| 152 |
+
|
| 153 |
+
return (
|
| 154 |
+
centered_mobile,
|
| 155 |
+
centroid_mobile,
|
| 156 |
+
centered_target,
|
| 157 |
+
centroid_target,
|
| 158 |
+
rotation_matrix,
|
| 159 |
+
num_valid_atoms,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@torch.no_grad()
|
| 164 |
+
@autocast("cuda", enabled=False)
|
| 165 |
+
def compute_rmsd_no_alignment(
|
| 166 |
+
aligned: torch.Tensor,
|
| 167 |
+
target: torch.Tensor,
|
| 168 |
+
num_valid_atoms: torch.Tensor,
|
| 169 |
+
reduction: str = "batch",
|
| 170 |
+
) -> torch.Tensor:
|
| 171 |
+
"""
|
| 172 |
+
Compute RMSD between two batches of structures without alignment.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 176 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 177 |
+
- num_valid_atoms (torch.Tensor): Batch of number of valid atoms for alignment (B,)
|
| 178 |
+
- reduction (str): One of "batch", "per_sample", "per_residue".
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
|
| 182 |
+
If reduction == "batch":
|
| 183 |
+
(torch.Tensor): 0-dim, Average Root Mean Square Deviation between the structures for each batch
|
| 184 |
+
If reduction == "per_sample":
|
| 185 |
+
(torch.Tensor): (B,)-dim, Root Mean Square Deviation between the structures for each batch
|
| 186 |
+
If reduction == "per_residue":
|
| 187 |
+
(torch.Tensor): (B, N)-dim, Root Mean Square Deviation between the structures for residue in the batch
|
| 188 |
+
"""
|
| 189 |
+
if reduction not in ("per_residue", "per_sample", "batch"):
|
| 190 |
+
raise ValueError("Unrecognized reduction: '{reduction}'")
|
| 191 |
+
# Compute RMSD for each batch
|
| 192 |
+
diff = aligned - target
|
| 193 |
+
if reduction == "per_residue":
|
| 194 |
+
mean_squared_error = diff.square().view(diff.size(0), -1, 9).mean(dim=-1)
|
| 195 |
+
else:
|
| 196 |
+
mean_squared_error = diff.square().sum(dim=(1, 2)) / (
|
| 197 |
+
num_valid_atoms.squeeze(-1)
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
rmsd = torch.sqrt(mean_squared_error)
|
| 201 |
+
if reduction in ("per_sample", "per_residue"):
|
| 202 |
+
return rmsd
|
| 203 |
+
elif reduction == "batch":
|
| 204 |
+
avg_rmsd = rmsd.masked_fill(num_valid_atoms.squeeze(-1) == 0, 0).sum() / (
|
| 205 |
+
(num_valid_atoms > 0).sum() + 1e-8
|
| 206 |
+
)
|
| 207 |
+
return avg_rmsd
|
| 208 |
+
else:
|
| 209 |
+
raise ValueError(reduction)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@torch.no_grad()
|
| 213 |
+
@autocast("cuda", enabled=False)
|
| 214 |
+
def compute_affine_and_rmsd(
|
| 215 |
+
mobile: torch.Tensor,
|
| 216 |
+
target: torch.Tensor,
|
| 217 |
+
atom_exists_mask: torch.Tensor | None = None,
|
| 218 |
+
sequence_id: torch.Tensor | None = None,
|
| 219 |
+
) -> Tuple[Affine3D, torch.Tensor]:
|
| 220 |
+
"""
|
| 221 |
+
Compute RMSD between two batches of structures with support for masking invalid atoms using PyTorch.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 225 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 226 |
+
- atom_exists_mask (torch.Tensor, optional): Mask for Whether an atom exists of shape (B, N)
|
| 227 |
+
- sequence_id (torch.Tensor, optional): Sequence id tensor for binpacking.
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
- affine (Affine3D): Transformation between mobile and target structure
|
| 231 |
+
- avg_rmsd (torch.Tensor): Average Root Mean Square Deviation between the structures for each batch
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
(
|
| 235 |
+
centered_mobile,
|
| 236 |
+
centroid_mobile,
|
| 237 |
+
centered_target,
|
| 238 |
+
centroid_target,
|
| 239 |
+
rotation_matrix,
|
| 240 |
+
num_valid_atoms,
|
| 241 |
+
) = compute_alignment_tensors(
|
| 242 |
+
mobile=mobile,
|
| 243 |
+
target=target,
|
| 244 |
+
atom_exists_mask=atom_exists_mask,
|
| 245 |
+
sequence_id=sequence_id,
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Apply rotation to mobile centroid
|
| 249 |
+
translation = torch.matmul(-centroid_mobile, rotation_matrix) + centroid_target
|
| 250 |
+
affine = Affine3D.from_tensor_pair(
|
| 251 |
+
translation, rotation_matrix.unsqueeze(dim=-3).transpose(-2, -1)
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Apply transformation to centered structure to compute rmsd
|
| 255 |
+
rotated_mobile = torch.matmul(centered_mobile, rotation_matrix)
|
| 256 |
+
avg_rmsd = compute_rmsd_no_alignment(
|
| 257 |
+
rotated_mobile, centered_target, num_valid_atoms, reduction="batch"
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
return affine, avg_rmsd
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def compute_gdt_ts_no_alignment(
|
| 264 |
+
aligned: torch.Tensor,
|
| 265 |
+
target: torch.Tensor,
|
| 266 |
+
atom_exists_mask: torch.Tensor,
|
| 267 |
+
reduction: str = "batch",
|
| 268 |
+
) -> torch.Tensor:
|
| 269 |
+
"""
|
| 270 |
+
Compute GDT_TS between two batches of structures without alignment.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
- mobile (torch.Tensor): Batch of coordinates of structure to be superimposed in shape (B, N, 3)
|
| 274 |
+
- target (torch.Tensor): Batch of coordinates of structure that is fixed in shape (B, N, 3)
|
| 275 |
+
- atom_exists_mask (torch.Tensor): Mask for Whether an atom exists of shape (B, N). noo
|
| 276 |
+
- reduction (str): One of "batch", "per_sample".
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
If reduction == "batch":
|
| 280 |
+
(torch.Tensor): 0-dim, GDT_TS between the structures for each batch
|
| 281 |
+
If reduction == "per_sample":
|
| 282 |
+
(torch.Tensor): (B,)-dim, GDT_TS between the structures for each sample in the batch
|
| 283 |
+
"""
|
| 284 |
+
if reduction not in ("per_sample", "batch"):
|
| 285 |
+
raise ValueError("Unrecognized reduction: '{reduction}'")
|
| 286 |
+
|
| 287 |
+
if atom_exists_mask is None:
|
| 288 |
+
atom_exists_mask = torch.isfinite(target).all(dim=-1)
|
| 289 |
+
|
| 290 |
+
deviation = torch.linalg.vector_norm(aligned - target, dim=-1)
|
| 291 |
+
num_valid_atoms = atom_exists_mask.sum(dim=-1)
|
| 292 |
+
|
| 293 |
+
# Compute GDT_TS
|
| 294 |
+
score = (
|
| 295 |
+
((deviation < 1) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 296 |
+
+ ((deviation < 2) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 297 |
+
+ ((deviation < 4) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 298 |
+
+ ((deviation < 8) * atom_exists_mask).sum(dim=-1) / num_valid_atoms
|
| 299 |
+
) * 0.25
|
| 300 |
+
|
| 301 |
+
if reduction == "batch":
|
| 302 |
+
return score.mean()
|
| 303 |
+
elif reduction == "per_sample":
|
| 304 |
+
return score
|
| 305 |
+
else:
|
| 306 |
+
raise ValueError("Unrecognized reduction: '{reduction}'")
|
esmfold2_residue_constants.py
CHANGED
|
@@ -1,1223 +1,1223 @@
|
|
| 1 |
-
# Copyright 2025 EvolutionaryScale
|
| 2 |
-
# Copyright 2021 AlQuraishi Laboratory
|
| 3 |
-
# Copyright 2021 DeepMind Technologies Limited
|
| 4 |
-
#
|
| 5 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
-
# you may not use this file except in compliance with the License.
|
| 7 |
-
# You may obtain a copy of the License at
|
| 8 |
-
#
|
| 9 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
-
#
|
| 11 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
-
# See the License for the specific language governing permissions and
|
| 15 |
-
# limitations under the License.
|
| 16 |
-
|
| 17 |
-
"""Constants used in AlphaFold."""
|
| 18 |
-
|
| 19 |
-
import collections
|
| 20 |
-
import functools
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
from typing import List, Mapping, Tuple
|
| 23 |
-
|
| 24 |
-
import numpy as np
|
| 25 |
-
|
| 26 |
-
# import tree
|
| 27 |
-
|
| 28 |
-
# Internal import (35fd).
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# Distance from one CA to next CA [trans configuration: omega = 180].
|
| 32 |
-
ca_ca = 3.80209737096
|
| 33 |
-
|
| 34 |
-
# Format: The list for each AA type contains chi1, chi2, chi3, chi4 in
|
| 35 |
-
# this order (or a relevant subset from chi1 onwards). ALA and GLY don't have
|
| 36 |
-
# chi angles so their chi angle lists are empty.
|
| 37 |
-
chi_angles_atoms = {
|
| 38 |
-
"ALA": [],
|
| 39 |
-
# Chi5 in arginine is always 0 +- 5 degrees, so ignore it.
|
| 40 |
-
"ARG": [
|
| 41 |
-
["N", "CA", "CB", "CG"],
|
| 42 |
-
["CA", "CB", "CG", "CD"],
|
| 43 |
-
["CB", "CG", "CD", "NE"],
|
| 44 |
-
["CG", "CD", "NE", "CZ"],
|
| 45 |
-
],
|
| 46 |
-
"ASN": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "OD1"]],
|
| 47 |
-
"ASP": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "OD1"]],
|
| 48 |
-
"CYS": [["N", "CA", "CB", "SG"]],
|
| 49 |
-
"GLN": [
|
| 50 |
-
["N", "CA", "CB", "CG"],
|
| 51 |
-
["CA", "CB", "CG", "CD"],
|
| 52 |
-
["CB", "CG", "CD", "OE1"],
|
| 53 |
-
],
|
| 54 |
-
"GLU": [
|
| 55 |
-
["N", "CA", "CB", "CG"],
|
| 56 |
-
["CA", "CB", "CG", "CD"],
|
| 57 |
-
["CB", "CG", "CD", "OE1"],
|
| 58 |
-
],
|
| 59 |
-
"GLY": [],
|
| 60 |
-
"HIS": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "ND1"]],
|
| 61 |
-
"ILE": [["N", "CA", "CB", "CG1"], ["CA", "CB", "CG1", "CD1"]],
|
| 62 |
-
"LEU": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 63 |
-
"LYS": [
|
| 64 |
-
["N", "CA", "CB", "CG"],
|
| 65 |
-
["CA", "CB", "CG", "CD"],
|
| 66 |
-
["CB", "CG", "CD", "CE"],
|
| 67 |
-
["CG", "CD", "CE", "NZ"],
|
| 68 |
-
],
|
| 69 |
-
"MET": [
|
| 70 |
-
["N", "CA", "CB", "CG"],
|
| 71 |
-
["CA", "CB", "CG", "SD"],
|
| 72 |
-
["CB", "CG", "SD", "CE"],
|
| 73 |
-
],
|
| 74 |
-
"PHE": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 75 |
-
"PRO": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD"]],
|
| 76 |
-
"SER": [["N", "CA", "CB", "OG"]],
|
| 77 |
-
"THR": [["N", "CA", "CB", "OG1"]],
|
| 78 |
-
"TRP": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 79 |
-
"TYR": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 80 |
-
"VAL": [["N", "CA", "CB", "CG1"]],
|
| 81 |
-
"UNK": [],
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
# If chi angles given in fixed-length array, this matrix determines how to mask
|
| 85 |
-
# them for each AA type. The order is as per restype_order (see below).
|
| 86 |
-
chi_angles_mask = [
|
| 87 |
-
[0.0, 0.0, 0.0, 0.0], # ALA
|
| 88 |
-
[1.0, 1.0, 1.0, 1.0], # ARG
|
| 89 |
-
[1.0, 1.0, 0.0, 0.0], # ASN
|
| 90 |
-
[1.0, 1.0, 0.0, 0.0], # ASP
|
| 91 |
-
[1.0, 0.0, 0.0, 0.0], # CYS
|
| 92 |
-
[1.0, 1.0, 1.0, 0.0], # GLN
|
| 93 |
-
[1.0, 1.0, 1.0, 0.0], # GLU
|
| 94 |
-
[0.0, 0.0, 0.0, 0.0], # GLY
|
| 95 |
-
[1.0, 1.0, 0.0, 0.0], # HIS
|
| 96 |
-
[1.0, 1.0, 0.0, 0.0], # ILE
|
| 97 |
-
[1.0, 1.0, 0.0, 0.0], # LEU
|
| 98 |
-
[1.0, 1.0, 1.0, 1.0], # LYS
|
| 99 |
-
[1.0, 1.0, 1.0, 0.0], # MET
|
| 100 |
-
[1.0, 1.0, 0.0, 0.0], # PHE
|
| 101 |
-
[1.0, 1.0, 0.0, 0.0], # PRO
|
| 102 |
-
[1.0, 0.0, 0.0, 0.0], # SER
|
| 103 |
-
[1.0, 0.0, 0.0, 0.0], # THR
|
| 104 |
-
[1.0, 1.0, 0.0, 0.0], # TRP
|
| 105 |
-
[1.0, 1.0, 0.0, 0.0], # TYR
|
| 106 |
-
[1.0, 0.0, 0.0, 0.0], # VAL
|
| 107 |
-
[0.0, 0.0, 0.0, 0.0], # UNK
|
| 108 |
-
]
|
| 109 |
-
|
| 110 |
-
# The following chi angles are pi periodic: they can be rotated by a multiple
|
| 111 |
-
# of pi without affecting the structure.
|
| 112 |
-
chi_pi_periodic = [
|
| 113 |
-
[0.0, 0.0, 0.0, 0.0], # ALA
|
| 114 |
-
[0.0, 0.0, 0.0, 0.0], # ARG
|
| 115 |
-
[0.0, 0.0, 0.0, 0.0], # ASN
|
| 116 |
-
[0.0, 1.0, 0.0, 0.0], # ASP
|
| 117 |
-
[0.0, 0.0, 0.0, 0.0], # CYS
|
| 118 |
-
[0.0, 0.0, 0.0, 0.0], # GLN
|
| 119 |
-
[0.0, 0.0, 1.0, 0.0], # GLU
|
| 120 |
-
[0.0, 0.0, 0.0, 0.0], # GLY
|
| 121 |
-
[0.0, 0.0, 0.0, 0.0], # HIS
|
| 122 |
-
[0.0, 0.0, 0.0, 0.0], # ILE
|
| 123 |
-
[0.0, 0.0, 0.0, 0.0], # LEU
|
| 124 |
-
[0.0, 0.0, 0.0, 0.0], # LYS
|
| 125 |
-
[0.0, 0.0, 0.0, 0.0], # MET
|
| 126 |
-
[0.0, 1.0, 0.0, 0.0], # PHE
|
| 127 |
-
[0.0, 0.0, 0.0, 0.0], # PRO
|
| 128 |
-
[0.0, 0.0, 0.0, 0.0], # SER
|
| 129 |
-
[0.0, 0.0, 0.0, 0.0], # THR
|
| 130 |
-
[0.0, 0.0, 0.0, 0.0], # TRP
|
| 131 |
-
[0.0, 1.0, 0.0, 0.0], # TYR
|
| 132 |
-
[0.0, 0.0, 0.0, 0.0], # VAL
|
| 133 |
-
[0.0, 0.0, 0.0, 0.0], # UNK
|
| 134 |
-
]
|
| 135 |
-
|
| 136 |
-
# Atoms positions relative to the 8 rigid groups, defined by the pre-omega, phi,
|
| 137 |
-
# psi and chi angles:
|
| 138 |
-
# 0: 'backbone group',
|
| 139 |
-
# 1: 'pre-omega-group', (empty)
|
| 140 |
-
# 2: 'phi-group', (currently empty, because it defines only hydrogens)
|
| 141 |
-
# 3: 'psi-group',
|
| 142 |
-
# 4,5,6,7: 'chi1,2,3,4-group'
|
| 143 |
-
# The atom positions are relative to the axis-end-atom of the corresponding
|
| 144 |
-
# rotation axis. The x-axis is in direction of the rotation axis, and the y-axis
|
| 145 |
-
# is defined such that the dihedral-angle-definiting atom (the last entry in
|
| 146 |
-
# chi_angles_atoms above) is in the xy-plane (with a positive y-coordinate).
|
| 147 |
-
# format: [atomname, group_idx, rel_position]
|
| 148 |
-
rigid_group_atom_positions = {
|
| 149 |
-
"ALA": [
|
| 150 |
-
["N", 0, (-0.525, 1.363, 0.000)],
|
| 151 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 152 |
-
["C", 0, (1.526, -0.000, -0.000)],
|
| 153 |
-
["CB", 0, (-0.529, -0.774, -1.205)],
|
| 154 |
-
["O", 3, (0.627, 1.062, 0.000)],
|
| 155 |
-
],
|
| 156 |
-
"ARG": [
|
| 157 |
-
["N", 0, (-0.524, 1.362, -0.000)],
|
| 158 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 159 |
-
["C", 0, (1.525, -0.000, -0.000)],
|
| 160 |
-
["CB", 0, (-0.524, -0.778, -1.209)],
|
| 161 |
-
["O", 3, (0.626, 1.062, 0.000)],
|
| 162 |
-
["CG", 4, (0.616, 1.390, -0.000)],
|
| 163 |
-
["CD", 5, (0.564, 1.414, 0.000)],
|
| 164 |
-
["NE", 6, (0.539, 1.357, -0.000)],
|
| 165 |
-
["NH1", 7, (0.206, 2.301, 0.000)],
|
| 166 |
-
["NH2", 7, (2.078, 0.978, -0.000)],
|
| 167 |
-
["CZ", 7, (0.758, 1.093, -0.000)],
|
| 168 |
-
],
|
| 169 |
-
"ASN": [
|
| 170 |
-
["N", 0, (-0.536, 1.357, 0.000)],
|
| 171 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 172 |
-
["C", 0, (1.526, -0.000, -0.000)],
|
| 173 |
-
["CB", 0, (-0.531, -0.787, -1.200)],
|
| 174 |
-
["O", 3, (0.625, 1.062, 0.000)],
|
| 175 |
-
["CG", 4, (0.584, 1.399, 0.000)],
|
| 176 |
-
["ND2", 5, (0.593, -1.188, 0.001)],
|
| 177 |
-
["OD1", 5, (0.633, 1.059, 0.000)],
|
| 178 |
-
],
|
| 179 |
-
"ASP": [
|
| 180 |
-
["N", 0, (-0.525, 1.362, -0.000)],
|
| 181 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 182 |
-
["C", 0, (1.527, 0.000, -0.000)],
|
| 183 |
-
["CB", 0, (-0.526, -0.778, -1.208)],
|
| 184 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 185 |
-
["CG", 4, (0.593, 1.398, -0.000)],
|
| 186 |
-
["OD1", 5, (0.610, 1.091, 0.000)],
|
| 187 |
-
["OD2", 5, (0.592, -1.101, -0.003)],
|
| 188 |
-
],
|
| 189 |
-
"CYS": [
|
| 190 |
-
["N", 0, (-0.522, 1.362, -0.000)],
|
| 191 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 192 |
-
["C", 0, (1.524, 0.000, 0.000)],
|
| 193 |
-
["CB", 0, (-0.519, -0.773, -1.212)],
|
| 194 |
-
["O", 3, (0.625, 1.062, -0.000)],
|
| 195 |
-
["SG", 4, (0.728, 1.653, 0.000)],
|
| 196 |
-
],
|
| 197 |
-
"GLN": [
|
| 198 |
-
["N", 0, (-0.526, 1.361, -0.000)],
|
| 199 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 200 |
-
["C", 0, (1.526, 0.000, 0.000)],
|
| 201 |
-
["CB", 0, (-0.525, -0.779, -1.207)],
|
| 202 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 203 |
-
["CG", 4, (0.615, 1.393, 0.000)],
|
| 204 |
-
["CD", 5, (0.587, 1.399, -0.000)],
|
| 205 |
-
["NE2", 6, (0.593, -1.189, -0.001)],
|
| 206 |
-
["OE1", 6, (0.634, 1.060, 0.000)],
|
| 207 |
-
],
|
| 208 |
-
"GLU": [
|
| 209 |
-
["N", 0, (-0.528, 1.361, 0.000)],
|
| 210 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 211 |
-
["C", 0, (1.526, -0.000, -0.000)],
|
| 212 |
-
["CB", 0, (-0.526, -0.781, -1.207)],
|
| 213 |
-
["O", 3, (0.626, 1.062, 0.000)],
|
| 214 |
-
["CG", 4, (0.615, 1.392, 0.000)],
|
| 215 |
-
["CD", 5, (0.600, 1.397, 0.000)],
|
| 216 |
-
["OE1", 6, (0.607, 1.095, -0.000)],
|
| 217 |
-
["OE2", 6, (0.589, -1.104, -0.001)],
|
| 218 |
-
],
|
| 219 |
-
"GLY": [
|
| 220 |
-
["N", 0, (-0.572, 1.337, 0.000)],
|
| 221 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 222 |
-
["C", 0, (1.517, -0.000, -0.000)],
|
| 223 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 224 |
-
],
|
| 225 |
-
"HIS": [
|
| 226 |
-
["N", 0, (-0.527, 1.360, 0.000)],
|
| 227 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 228 |
-
["C", 0, (1.525, 0.000, 0.000)],
|
| 229 |
-
["CB", 0, (-0.525, -0.778, -1.208)],
|
| 230 |
-
["O", 3, (0.625, 1.063, 0.000)],
|
| 231 |
-
["CG", 4, (0.600, 1.370, -0.000)],
|
| 232 |
-
["CD2", 5, (0.889, -1.021, 0.003)],
|
| 233 |
-
["ND1", 5, (0.744, 1.160, -0.000)],
|
| 234 |
-
["CE1", 5, (2.030, 0.851, 0.002)],
|
| 235 |
-
["NE2", 5, (2.145, -0.466, 0.004)],
|
| 236 |
-
],
|
| 237 |
-
"ILE": [
|
| 238 |
-
["N", 0, (-0.493, 1.373, -0.000)],
|
| 239 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 240 |
-
["C", 0, (1.527, -0.000, -0.000)],
|
| 241 |
-
["CB", 0, (-0.536, -0.793, -1.213)],
|
| 242 |
-
["O", 3, (0.627, 1.062, -0.000)],
|
| 243 |
-
["CG1", 4, (0.534, 1.437, -0.000)],
|
| 244 |
-
["CG2", 4, (0.540, -0.785, -1.199)],
|
| 245 |
-
["CD1", 5, (0.619, 1.391, 0.000)],
|
| 246 |
-
],
|
| 247 |
-
"LEU": [
|
| 248 |
-
["N", 0, (-0.520, 1.363, 0.000)],
|
| 249 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 250 |
-
["C", 0, (1.525, -0.000, -0.000)],
|
| 251 |
-
["CB", 0, (-0.522, -0.773, -1.214)],
|
| 252 |
-
["O", 3, (0.625, 1.063, -0.000)],
|
| 253 |
-
["CG", 4, (0.678, 1.371, 0.000)],
|
| 254 |
-
["CD1", 5, (0.530, 1.430, -0.000)],
|
| 255 |
-
["CD2", 5, (0.535, -0.774, 1.200)],
|
| 256 |
-
],
|
| 257 |
-
"LYS": [
|
| 258 |
-
["N", 0, (-0.526, 1.362, -0.000)],
|
| 259 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 260 |
-
["C", 0, (1.526, 0.000, 0.000)],
|
| 261 |
-
["CB", 0, (-0.524, -0.778, -1.208)],
|
| 262 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 263 |
-
["CG", 4, (0.619, 1.390, 0.000)],
|
| 264 |
-
["CD", 5, (0.559, 1.417, 0.000)],
|
| 265 |
-
["CE", 6, (0.560, 1.416, 0.000)],
|
| 266 |
-
["NZ", 7, (0.554, 1.387, 0.000)],
|
| 267 |
-
],
|
| 268 |
-
"MET": [
|
| 269 |
-
["N", 0, (-0.521, 1.364, -0.000)],
|
| 270 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 271 |
-
["C", 0, (1.525, 0.000, 0.000)],
|
| 272 |
-
["CB", 0, (-0.523, -0.776, -1.210)],
|
| 273 |
-
["O", 3, (0.625, 1.062, -0.000)],
|
| 274 |
-
["CG", 4, (0.613, 1.391, -0.000)],
|
| 275 |
-
["SD", 5, (0.703, 1.695, 0.000)],
|
| 276 |
-
["CE", 6, (0.320, 1.786, -0.000)],
|
| 277 |
-
],
|
| 278 |
-
"PHE": [
|
| 279 |
-
["N", 0, (-0.518, 1.363, 0.000)],
|
| 280 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 281 |
-
["C", 0, (1.524, 0.000, -0.000)],
|
| 282 |
-
["CB", 0, (-0.525, -0.776, -1.212)],
|
| 283 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 284 |
-
["CG", 4, (0.607, 1.377, 0.000)],
|
| 285 |
-
["CD1", 5, (0.709, 1.195, -0.000)],
|
| 286 |
-
["CD2", 5, (0.706, -1.196, 0.000)],
|
| 287 |
-
["CE1", 5, (2.102, 1.198, -0.000)],
|
| 288 |
-
["CE2", 5, (2.098, -1.201, -0.000)],
|
| 289 |
-
["CZ", 5, (2.794, -0.003, -0.001)],
|
| 290 |
-
],
|
| 291 |
-
"PRO": [
|
| 292 |
-
["N", 0, (-0.566, 1.351, -0.000)],
|
| 293 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 294 |
-
["C", 0, (1.527, -0.000, 0.000)],
|
| 295 |
-
["CB", 0, (-0.546, -0.611, -1.293)],
|
| 296 |
-
["O", 3, (0.621, 1.066, 0.000)],
|
| 297 |
-
["CG", 4, (0.382, 1.445, 0.0)],
|
| 298 |
-
# ['CD', 5, (0.427, 1.440, 0.0)],
|
| 299 |
-
["CD", 5, (0.477, 1.424, 0.0)], # manually made angle 2 degrees larger
|
| 300 |
-
],
|
| 301 |
-
"SER": [
|
| 302 |
-
["N", 0, (-0.529, 1.360, -0.000)],
|
| 303 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 304 |
-
["C", 0, (1.525, -0.000, -0.000)],
|
| 305 |
-
["CB", 0, (-0.518, -0.777, -1.211)],
|
| 306 |
-
["O", 3, (0.626, 1.062, -0.000)],
|
| 307 |
-
["OG", 4, (0.503, 1.325, 0.000)],
|
| 308 |
-
],
|
| 309 |
-
"THR": [
|
| 310 |
-
["N", 0, (-0.517, 1.364, 0.000)],
|
| 311 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 312 |
-
["C", 0, (1.526, 0.000, -0.000)],
|
| 313 |
-
["CB", 0, (-0.516, -0.793, -1.215)],
|
| 314 |
-
["O", 3, (0.626, 1.062, 0.000)],
|
| 315 |
-
["CG2", 4, (0.550, -0.718, -1.228)],
|
| 316 |
-
["OG1", 4, (0.472, 1.353, 0.000)],
|
| 317 |
-
],
|
| 318 |
-
"TRP": [
|
| 319 |
-
["N", 0, (-0.521, 1.363, 0.000)],
|
| 320 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 321 |
-
["C", 0, (1.525, -0.000, 0.000)],
|
| 322 |
-
["CB", 0, (-0.523, -0.776, -1.212)],
|
| 323 |
-
["O", 3, (0.627, 1.062, 0.000)],
|
| 324 |
-
["CG", 4, (0.609, 1.370, -0.000)],
|
| 325 |
-
["CD1", 5, (0.824, 1.091, 0.000)],
|
| 326 |
-
["CD2", 5, (0.854, -1.148, -0.005)],
|
| 327 |
-
["CE2", 5, (2.186, -0.678, -0.007)],
|
| 328 |
-
["CE3", 5, (0.622, -2.530, -0.007)],
|
| 329 |
-
["NE1", 5, (2.140, 0.690, -0.004)],
|
| 330 |
-
["CH2", 5, (3.028, -2.890, -0.013)],
|
| 331 |
-
["CZ2", 5, (3.283, -1.543, -0.011)],
|
| 332 |
-
["CZ3", 5, (1.715, -3.389, -0.011)],
|
| 333 |
-
],
|
| 334 |
-
"TYR": [
|
| 335 |
-
["N", 0, (-0.522, 1.362, 0.000)],
|
| 336 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 337 |
-
["C", 0, (1.524, -0.000, -0.000)],
|
| 338 |
-
["CB", 0, (-0.522, -0.776, -1.213)],
|
| 339 |
-
["O", 3, (0.627, 1.062, -0.000)],
|
| 340 |
-
["CG", 4, (0.607, 1.382, -0.000)],
|
| 341 |
-
["CD1", 5, (0.716, 1.195, -0.000)],
|
| 342 |
-
["CD2", 5, (0.713, -1.194, -0.001)],
|
| 343 |
-
["CE1", 5, (2.107, 1.200, -0.002)],
|
| 344 |
-
["CE2", 5, (2.104, -1.201, -0.003)],
|
| 345 |
-
["OH", 5, (4.168, -0.002, -0.005)],
|
| 346 |
-
["CZ", 5, (2.791, -0.001, -0.003)],
|
| 347 |
-
],
|
| 348 |
-
"VAL": [
|
| 349 |
-
["N", 0, (-0.494, 1.373, -0.000)],
|
| 350 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 351 |
-
["C", 0, (1.527, -0.000, -0.000)],
|
| 352 |
-
["CB", 0, (-0.533, -0.795, -1.213)],
|
| 353 |
-
["O", 3, (0.627, 1.062, -0.000)],
|
| 354 |
-
["CG1", 4, (0.540, 1.429, -0.000)],
|
| 355 |
-
["CG2", 4, (0.533, -0.776, 1.203)],
|
| 356 |
-
],
|
| 357 |
-
# Assume alanine positions for unknown AA
|
| 358 |
-
"UNK": [
|
| 359 |
-
["N", 0, (-0.525, 1.363, 0.000)],
|
| 360 |
-
["CA", 0, (0.000, 0.000, 0.000)],
|
| 361 |
-
["C", 0, (1.526, -0.000, -0.000)],
|
| 362 |
-
],
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
# A list of atoms (excluding hydrogen) for each AA type. PDB naming convention.
|
| 366 |
-
residue_atoms = {
|
| 367 |
-
"ALA": ["C", "CA", "CB", "N", "O"],
|
| 368 |
-
"ARG": ["C", "CA", "CB", "CG", "CD", "CZ", "N", "NE", "O", "NH1", "NH2"],
|
| 369 |
-
"ASP": ["C", "CA", "CB", "CG", "N", "O", "OD1", "OD2"],
|
| 370 |
-
"ASN": ["C", "CA", "CB", "CG", "N", "ND2", "O", "OD1"],
|
| 371 |
-
"CYS": ["C", "CA", "CB", "N", "O", "SG"],
|
| 372 |
-
"GLU": ["C", "CA", "CB", "CG", "CD", "N", "O", "OE1", "OE2"],
|
| 373 |
-
"GLN": ["C", "CA", "CB", "CG", "CD", "N", "NE2", "O", "OE1"],
|
| 374 |
-
"GLY": ["C", "CA", "N", "O"],
|
| 375 |
-
"HIS": ["C", "CA", "CB", "CG", "CD2", "CE1", "N", "ND1", "NE2", "O"],
|
| 376 |
-
"ILE": ["C", "CA", "CB", "CG1", "CG2", "CD1", "N", "O"],
|
| 377 |
-
"LEU": ["C", "CA", "CB", "CG", "CD1", "CD2", "N", "O"],
|
| 378 |
-
"LYS": ["C", "CA", "CB", "CG", "CD", "CE", "N", "NZ", "O"],
|
| 379 |
-
"MET": ["C", "CA", "CB", "CG", "CE", "N", "O", "SD"],
|
| 380 |
-
"PHE": ["C", "CA", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "N", "O"],
|
| 381 |
-
"PRO": ["C", "CA", "CB", "CG", "CD", "N", "O"],
|
| 382 |
-
"SER": ["C", "CA", "CB", "N", "O", "OG"],
|
| 383 |
-
"THR": ["C", "CA", "CB", "CG2", "N", "O", "OG1"],
|
| 384 |
-
"TRP": [
|
| 385 |
-
"C",
|
| 386 |
-
"CA",
|
| 387 |
-
"CB",
|
| 388 |
-
"CG",
|
| 389 |
-
"CD1",
|
| 390 |
-
"CD2",
|
| 391 |
-
"CE2",
|
| 392 |
-
"CE3",
|
| 393 |
-
"CZ2",
|
| 394 |
-
"CZ3",
|
| 395 |
-
"CH2",
|
| 396 |
-
"N",
|
| 397 |
-
"NE1",
|
| 398 |
-
"O",
|
| 399 |
-
],
|
| 400 |
-
"TYR": ["C", "CA", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "N", "O", "OH"],
|
| 401 |
-
"VAL": ["C", "CA", "CB", "CG1", "CG2", "N", "O"],
|
| 402 |
-
"UNK": ["C", "CA", "N"],
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
# Naming swaps for ambiguous atom names.
|
| 406 |
-
# Due to symmetries in the amino acids the naming of atoms is ambiguous in
|
| 407 |
-
# 4 of the 20 amino acids.
|
| 408 |
-
# (The LDDT paper lists 7 amino acids as ambiguous, but the naming ambiguities
|
| 409 |
-
# in LEU, VAL and ARG can be resolved by using the 3d constellations of
|
| 410 |
-
# the 'ambiguous' atoms and their neighbours)
|
| 411 |
-
# TODO: ^ interpret this
|
| 412 |
-
residue_atom_renaming_swaps = {
|
| 413 |
-
"ASP": {"OD1": "OD2"},
|
| 414 |
-
"GLU": {"OE1": "OE2"},
|
| 415 |
-
"PHE": {"CD1": "CD2", "CE1": "CE2"},
|
| 416 |
-
"TYR": {"CD1": "CD2", "CE1": "CE2"},
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
# Van der Waals radii [Angstroem] of the atoms (from Wikipedia)
|
| 420 |
-
van_der_waals_radius = {"C": 1.7, "N": 1.55, "O": 1.52, "S": 1.8}
|
| 421 |
-
|
| 422 |
-
Bond = collections.namedtuple("Bond", ["atom1_name", "atom2_name", "length", "stddev"])
|
| 423 |
-
BondAngle = collections.namedtuple(
|
| 424 |
-
"BondAngle", ["atom1_name", "atom2_name", "atom3name", "angle_rad", "stddev"]
|
| 425 |
-
)
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
@functools.lru_cache(maxsize=None)
|
| 429 |
-
def load_stereo_chemical_props() -> (
|
| 430 |
-
Tuple[
|
| 431 |
-
Mapping[str, List[Bond]],
|
| 432 |
-
Mapping[str, List[Bond]],
|
| 433 |
-
Mapping[str, List[BondAngle]],
|
| 434 |
-
]
|
| 435 |
-
):
|
| 436 |
-
"""Load stereo_chemical_props.txt into a nice structure.
|
| 437 |
-
|
| 438 |
-
Load literature values for bond lengths and bond angles and translate
|
| 439 |
-
bond angles into the length of the opposite edge of the triangle
|
| 440 |
-
("residue_virtual_bonds").
|
| 441 |
-
|
| 442 |
-
Returns:
|
| 443 |
-
residue_bonds: dict that maps resname --> list of Bond tuples
|
| 444 |
-
residue_virtual_bonds: dict that maps resname --> list of Bond tuples
|
| 445 |
-
residue_bond_angles: dict that maps resname --> list of BondAngle tuples
|
| 446 |
-
"""
|
| 447 |
-
stereo_chemical_props = Path(
|
| 448 |
-
"evolutionaryscale/structure/stereo_chemical_props.txt"
|
| 449 |
-
).read_text()
|
| 450 |
-
|
| 451 |
-
lines_iter = iter(stereo_chemical_props.splitlines())
|
| 452 |
-
# Load bond lengths.
|
| 453 |
-
residue_bonds = {}
|
| 454 |
-
next(lines_iter) # Skip header line.
|
| 455 |
-
for line in lines_iter:
|
| 456 |
-
if line.strip() == "-":
|
| 457 |
-
break
|
| 458 |
-
bond, resname, length, stddev = line.split()
|
| 459 |
-
atom1, atom2 = bond.split("-")
|
| 460 |
-
if resname not in residue_bonds:
|
| 461 |
-
residue_bonds[resname] = []
|
| 462 |
-
residue_bonds[resname].append(Bond(atom1, atom2, float(length), float(stddev)))
|
| 463 |
-
residue_bonds["UNK"] = []
|
| 464 |
-
|
| 465 |
-
# Load bond angles.
|
| 466 |
-
residue_bond_angles = {}
|
| 467 |
-
next(lines_iter) # Skip empty line.
|
| 468 |
-
next(lines_iter) # Skip header line.
|
| 469 |
-
for line in lines_iter:
|
| 470 |
-
if line.strip() == "-":
|
| 471 |
-
break
|
| 472 |
-
bond, resname, angle_degree, stddev_degree = line.split()
|
| 473 |
-
atom1, atom2, atom3 = bond.split("-")
|
| 474 |
-
if resname not in residue_bond_angles:
|
| 475 |
-
residue_bond_angles[resname] = []
|
| 476 |
-
residue_bond_angles[resname].append(
|
| 477 |
-
BondAngle(
|
| 478 |
-
atom1,
|
| 479 |
-
atom2,
|
| 480 |
-
atom3,
|
| 481 |
-
float(angle_degree) / 180.0 * np.pi,
|
| 482 |
-
float(stddev_degree) / 180.0 * np.pi,
|
| 483 |
-
)
|
| 484 |
-
)
|
| 485 |
-
residue_bond_angles["UNK"] = []
|
| 486 |
-
|
| 487 |
-
def make_bond_key(atom1_name, atom2_name):
|
| 488 |
-
"""Unique key to lookup bonds."""
|
| 489 |
-
return "-".join(sorted([atom1_name, atom2_name]))
|
| 490 |
-
|
| 491 |
-
# Translate bond angles into distances ("virtual bonds").
|
| 492 |
-
residue_virtual_bonds = {}
|
| 493 |
-
for resname, bond_angles in residue_bond_angles.items():
|
| 494 |
-
# Create a fast lookup dict for bond lengths.
|
| 495 |
-
bond_cache = {}
|
| 496 |
-
for b in residue_bonds[resname]:
|
| 497 |
-
bond_cache[make_bond_key(b.atom1_name, b.atom2_name)] = b
|
| 498 |
-
residue_virtual_bonds[resname] = []
|
| 499 |
-
for ba in bond_angles:
|
| 500 |
-
bond1 = bond_cache[make_bond_key(ba.atom1_name, ba.atom2_name)]
|
| 501 |
-
bond2 = bond_cache[make_bond_key(ba.atom2_name, ba.atom3name)]
|
| 502 |
-
|
| 503 |
-
# Compute distance between atom1 and atom3 using the law of cosines
|
| 504 |
-
# c^2 = a^2 + b^2 - 2ab*cos(gamma).
|
| 505 |
-
gamma = ba.angle_rad
|
| 506 |
-
length = np.sqrt(
|
| 507 |
-
bond1.length**2
|
| 508 |
-
+ bond2.length**2
|
| 509 |
-
- 2 * bond1.length * bond2.length * np.cos(gamma)
|
| 510 |
-
)
|
| 511 |
-
|
| 512 |
-
# Propagation of uncertainty assuming uncorrelated errors.
|
| 513 |
-
dl_outer = 0.5 / length
|
| 514 |
-
dl_dgamma = (2 * bond1.length * bond2.length * np.sin(gamma)) * dl_outer
|
| 515 |
-
dl_db1 = (2 * bond1.length - 2 * bond2.length * np.cos(gamma)) * dl_outer
|
| 516 |
-
dl_db2 = (2 * bond2.length - 2 * bond1.length * np.cos(gamma)) * dl_outer
|
| 517 |
-
stddev = np.sqrt(
|
| 518 |
-
(dl_dgamma * ba.stddev) ** 2
|
| 519 |
-
+ (dl_db1 * bond1.stddev) ** 2
|
| 520 |
-
+ (dl_db2 * bond2.stddev) ** 2
|
| 521 |
-
)
|
| 522 |
-
residue_virtual_bonds[resname].append(
|
| 523 |
-
Bond(ba.atom1_name, ba.atom3name, length, stddev)
|
| 524 |
-
)
|
| 525 |
-
|
| 526 |
-
return (residue_bonds, residue_virtual_bonds, residue_bond_angles)
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
# Between-residue bond lengths for general bonds (first element) and for Proline
|
| 530 |
-
# (second element).
|
| 531 |
-
between_res_bond_length_c_n = [1.329, 1.341]
|
| 532 |
-
between_res_bond_length_stddev_c_n = [0.014, 0.016]
|
| 533 |
-
|
| 534 |
-
# Between-residue cos_angles.
|
| 535 |
-
between_res_cos_angles_c_n_ca = [-0.5203, 0.0353] # degrees: 121.352 +- 2.315
|
| 536 |
-
between_res_cos_angles_ca_c_n = [-0.4473, 0.0311] # degrees: 116.568 +- 1.995
|
| 537 |
-
|
| 538 |
-
# This mapping is used when we need to store atom data in a format that requires
|
| 539 |
-
# fixed atom data size for every residue (e.g. a numpy array).
|
| 540 |
-
atom_types = [
|
| 541 |
-
"N",
|
| 542 |
-
"CA",
|
| 543 |
-
"C",
|
| 544 |
-
"CB",
|
| 545 |
-
"O",
|
| 546 |
-
"CG",
|
| 547 |
-
"CG1",
|
| 548 |
-
"CG2",
|
| 549 |
-
"OG",
|
| 550 |
-
"OG1",
|
| 551 |
-
"SG",
|
| 552 |
-
"CD",
|
| 553 |
-
"CD1",
|
| 554 |
-
"CD2",
|
| 555 |
-
"ND1",
|
| 556 |
-
"ND2",
|
| 557 |
-
"OD1",
|
| 558 |
-
"OD2",
|
| 559 |
-
"SD",
|
| 560 |
-
"CE",
|
| 561 |
-
"CE1",
|
| 562 |
-
"CE2",
|
| 563 |
-
"CE3",
|
| 564 |
-
"NE",
|
| 565 |
-
"NE1",
|
| 566 |
-
"NE2",
|
| 567 |
-
"OE1",
|
| 568 |
-
"OE2",
|
| 569 |
-
"CH2",
|
| 570 |
-
"NH1",
|
| 571 |
-
"NH2",
|
| 572 |
-
"OH",
|
| 573 |
-
"CZ",
|
| 574 |
-
"CZ2",
|
| 575 |
-
"CZ3",
|
| 576 |
-
"NZ",
|
| 577 |
-
"OXT",
|
| 578 |
-
]
|
| 579 |
-
atom_order = {atom_type: i for i, atom_type in enumerate(atom_types)}
|
| 580 |
-
atom_type_num = len(atom_types) # := 37.
|
| 581 |
-
|
| 582 |
-
# A compact atom encoding with 14 columns
|
| 583 |
-
# pylint: disable=line-too-long
|
| 584 |
-
# pylint: disable=bad-whitespace
|
| 585 |
-
restype_name_to_atom14_names = {
|
| 586 |
-
"ALA": ["N", "CA", "C", "O", "CB", "", "", "", "", "", "", "", "", ""],
|
| 587 |
-
"ARG": [
|
| 588 |
-
"N",
|
| 589 |
-
"CA",
|
| 590 |
-
"C",
|
| 591 |
-
"O",
|
| 592 |
-
"CB",
|
| 593 |
-
"CG",
|
| 594 |
-
"CD",
|
| 595 |
-
"NE",
|
| 596 |
-
"CZ",
|
| 597 |
-
"NH1",
|
| 598 |
-
"NH2",
|
| 599 |
-
"",
|
| 600 |
-
"",
|
| 601 |
-
"",
|
| 602 |
-
],
|
| 603 |
-
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2", "", "", "", "", "", ""],
|
| 604 |
-
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2", "", "", "", "", "", ""],
|
| 605 |
-
"CYS": ["N", "CA", "C", "O", "CB", "SG", "", "", "", "", "", "", "", ""],
|
| 606 |
-
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2", "", "", "", "", ""],
|
| 607 |
-
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2", "", "", "", "", ""],
|
| 608 |
-
"GLY": ["N", "CA", "C", "O", "", "", "", "", "", "", "", "", "", ""],
|
| 609 |
-
"HIS": [
|
| 610 |
-
"N",
|
| 611 |
-
"CA",
|
| 612 |
-
"C",
|
| 613 |
-
"O",
|
| 614 |
-
"CB",
|
| 615 |
-
"CG",
|
| 616 |
-
"ND1",
|
| 617 |
-
"CD2",
|
| 618 |
-
"CE1",
|
| 619 |
-
"NE2",
|
| 620 |
-
"",
|
| 621 |
-
"",
|
| 622 |
-
"",
|
| 623 |
-
"",
|
| 624 |
-
],
|
| 625 |
-
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1", "", "", "", "", "", ""],
|
| 626 |
-
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "", "", "", "", "", ""],
|
| 627 |
-
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ", "", "", "", "", ""],
|
| 628 |
-
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE", "", "", "", "", "", ""],
|
| 629 |
-
"PHE": [
|
| 630 |
-
"N",
|
| 631 |
-
"CA",
|
| 632 |
-
"C",
|
| 633 |
-
"O",
|
| 634 |
-
"CB",
|
| 635 |
-
"CG",
|
| 636 |
-
"CD1",
|
| 637 |
-
"CD2",
|
| 638 |
-
"CE1",
|
| 639 |
-
"CE2",
|
| 640 |
-
"CZ",
|
| 641 |
-
"",
|
| 642 |
-
"",
|
| 643 |
-
"",
|
| 644 |
-
],
|
| 645 |
-
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD", "", "", "", "", "", "", ""],
|
| 646 |
-
"SER": ["N", "CA", "C", "O", "CB", "OG", "", "", "", "", "", "", "", ""],
|
| 647 |
-
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2", "", "", "", "", "", "", ""],
|
| 648 |
-
"TRP": [
|
| 649 |
-
"N",
|
| 650 |
-
"CA",
|
| 651 |
-
"C",
|
| 652 |
-
"O",
|
| 653 |
-
"CB",
|
| 654 |
-
"CG",
|
| 655 |
-
"CD1",
|
| 656 |
-
"CD2",
|
| 657 |
-
"NE1",
|
| 658 |
-
"CE2",
|
| 659 |
-
"CE3",
|
| 660 |
-
"CZ2",
|
| 661 |
-
"CZ3",
|
| 662 |
-
"CH2",
|
| 663 |
-
],
|
| 664 |
-
"TYR": [
|
| 665 |
-
"N",
|
| 666 |
-
"CA",
|
| 667 |
-
"C",
|
| 668 |
-
"O",
|
| 669 |
-
"CB",
|
| 670 |
-
"CG",
|
| 671 |
-
"CD1",
|
| 672 |
-
"CD2",
|
| 673 |
-
"CE1",
|
| 674 |
-
"CE2",
|
| 675 |
-
"CZ",
|
| 676 |
-
"OH",
|
| 677 |
-
"",
|
| 678 |
-
"",
|
| 679 |
-
],
|
| 680 |
-
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "", "", "", "", "", "", ""],
|
| 681 |
-
"UNK": ["N", "CA", "C", "", "", "", "", "", "", "", "", "", "", ""],
|
| 682 |
-
}
|
| 683 |
-
# pylint: enable=line-too-long
|
| 684 |
-
# pylint: enable=bad-whitespace
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
# This is the standard residue order when coding AA type as a number.
|
| 688 |
-
# Reproduce it by taking 3-letter AA codes and sorting them alphabetically.
|
| 689 |
-
restypes = [
|
| 690 |
-
"A",
|
| 691 |
-
"R",
|
| 692 |
-
"N",
|
| 693 |
-
"D",
|
| 694 |
-
"C",
|
| 695 |
-
"Q",
|
| 696 |
-
"E",
|
| 697 |
-
"G",
|
| 698 |
-
"H",
|
| 699 |
-
"I",
|
| 700 |
-
"L",
|
| 701 |
-
"K",
|
| 702 |
-
"M",
|
| 703 |
-
"F",
|
| 704 |
-
"P",
|
| 705 |
-
"S",
|
| 706 |
-
"T",
|
| 707 |
-
"W",
|
| 708 |
-
"Y",
|
| 709 |
-
"V",
|
| 710 |
-
]
|
| 711 |
-
restype_order = {restype: i for i, restype in enumerate(restypes)}
|
| 712 |
-
restype_num = len(restypes) # := 20.
|
| 713 |
-
unk_restype_index = restype_num # Catch-all index for unknown restypes.
|
| 714 |
-
|
| 715 |
-
restypes_with_x = restypes + ["X"]
|
| 716 |
-
restype_order_with_x = {restype: i for i, restype in enumerate(restypes_with_x)}
|
| 717 |
-
|
| 718 |
-
bb_atoms = ["N", "CA", "C", "O"]
|
| 719 |
-
|
| 720 |
-
# Hydrophobicity by residue (positive values are hydrophobic). Derived from Black & Mould (1991), normalized by subtracting 0.5.
|
| 721 |
-
hydrophobicity = {
|
| 722 |
-
"ALA": 0.116,
|
| 723 |
-
"ARG": -0.5,
|
| 724 |
-
"ASN": -0.264,
|
| 725 |
-
"ASP": -0.472,
|
| 726 |
-
"CYS": 0.18,
|
| 727 |
-
"GLN": -0.249,
|
| 728 |
-
"GLU": -0.457,
|
| 729 |
-
"GLY": 0.001,
|
| 730 |
-
"HIS": -0.335,
|
| 731 |
-
"ILE": 0.443,
|
| 732 |
-
"LEU": 0.443,
|
| 733 |
-
"LYS": -0.217,
|
| 734 |
-
"MET": 0.238,
|
| 735 |
-
"PHE": 0.5,
|
| 736 |
-
"PRO": 0.211,
|
| 737 |
-
"SER": -0.141,
|
| 738 |
-
"THR": -0.05,
|
| 739 |
-
"TRP": 0.378,
|
| 740 |
-
"TYR": 0.38,
|
| 741 |
-
"VAL": 0.325,
|
| 742 |
-
}
|
| 743 |
-
|
| 744 |
-
# Side chain max accessible surface area in Ala-X-Ala tripeptide (from Chennamsetty et al. 2010).
|
| 745 |
-
side_chain_asa = {
|
| 746 |
-
"ALA": 64.7809,
|
| 747 |
-
"ARG": 210.02,
|
| 748 |
-
"ASN": 113.187,
|
| 749 |
-
"ASP": 110.209,
|
| 750 |
-
"CYS": 95.2439,
|
| 751 |
-
"GLN": 147.855,
|
| 752 |
-
"GLU": 143.924,
|
| 753 |
-
"GLY": 23.1338,
|
| 754 |
-
"HIS": 146.449,
|
| 755 |
-
"ILE": 151.242,
|
| 756 |
-
"LEU": 139.524,
|
| 757 |
-
"LYS": 177.366,
|
| 758 |
-
"MET": 164.674,
|
| 759 |
-
"PHE": 186.7,
|
| 760 |
-
"PRO": 111.533,
|
| 761 |
-
"SER": 81.2159,
|
| 762 |
-
"THR": 111.597,
|
| 763 |
-
"TRP": 229.619,
|
| 764 |
-
"TYR": 200.306,
|
| 765 |
-
"VAL": 124.237,
|
| 766 |
-
}
|
| 767 |
-
|
| 768 |
-
# Approximate Volumes of amino acids in cubic angstroms.
|
| 769 |
-
# https://www.imgt.org/IMGTeducation/Aide-memoire/_UK/aminoacids/abbreviation.html
|
| 770 |
-
amino_acid_volumes = {
|
| 771 |
-
"A": 88.6, # Alanine
|
| 772 |
-
"R": 173.4, # Arginine
|
| 773 |
-
"N": 114.1, # Asparagine
|
| 774 |
-
"D": 111.1, # Aspartic acid
|
| 775 |
-
"C": 108.5, # Cysteine
|
| 776 |
-
"Q": 143.8, # Glutamine
|
| 777 |
-
"E": 138.4, # Glutamic acid
|
| 778 |
-
"G": 60.1, # Glycine
|
| 779 |
-
"H": 153.2, # Histidine
|
| 780 |
-
"I": 166.7, # Isoleucine
|
| 781 |
-
"L": 166.7, # Leucine
|
| 782 |
-
"K": 168.6, # Lysine
|
| 783 |
-
"M": 162.9, # Methionine
|
| 784 |
-
"F": 189.9, # Phenylalanine
|
| 785 |
-
"P": 112.7, # Proline
|
| 786 |
-
"S": 89.0, # Serine
|
| 787 |
-
"T": 116.1, # Threonine
|
| 788 |
-
"W": 227.8, # Tryptophan
|
| 789 |
-
"Y": 193.6, # Tyrosine
|
| 790 |
-
"V": 140.0, # Valine
|
| 791 |
-
"X": 88.6, # Unknown, use Alanine as approximation
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
def sequence_to_onehot(
|
| 796 |
-
sequence: str, mapping: Mapping[str, int], map_unknown_to_x: bool = False
|
| 797 |
-
) -> np.ndarray:
|
| 798 |
-
"""Maps the given sequence into a one-hot encoded matrix.
|
| 799 |
-
|
| 800 |
-
Args:
|
| 801 |
-
sequence: An amino acid sequence.
|
| 802 |
-
mapping: A dictionary mapping amino acids to integers.
|
| 803 |
-
map_unknown_to_x: If True, any amino acid that is not in the mapping will be
|
| 804 |
-
mapped to the unknown amino acid 'X'. If the mapping doesn't contain
|
| 805 |
-
amino acid 'X', an error will be thrown. If False, any amino acid not in
|
| 806 |
-
the mapping will throw an error.
|
| 807 |
-
|
| 808 |
-
Returns:
|
| 809 |
-
A numpy array of shape (seq_len, num_unique_aas) with one-hot encoding of
|
| 810 |
-
the sequence.
|
| 811 |
-
|
| 812 |
-
Raises:
|
| 813 |
-
ValueError: If the mapping doesn't contain values from 0 to
|
| 814 |
-
num_unique_aas - 1 without any gaps.
|
| 815 |
-
"""
|
| 816 |
-
num_entries = max(mapping.values()) + 1
|
| 817 |
-
|
| 818 |
-
if sorted(set(mapping.values())) != list(range(num_entries)):
|
| 819 |
-
raise ValueError(
|
| 820 |
-
"The mapping must have values from 0 to num_unique_aas-1 "
|
| 821 |
-
"without any gaps. Got: %s" % sorted(mapping.values())
|
| 822 |
-
)
|
| 823 |
-
|
| 824 |
-
one_hot_arr = np.zeros((len(sequence), num_entries), dtype=np.int32)
|
| 825 |
-
|
| 826 |
-
for aa_index, aa_type in enumerate(sequence):
|
| 827 |
-
if map_unknown_to_x:
|
| 828 |
-
if aa_type.isalpha() and aa_type.isupper():
|
| 829 |
-
aa_id = mapping.get(aa_type, mapping["X"])
|
| 830 |
-
else:
|
| 831 |
-
raise ValueError(f"Invalid character in the sequence: {aa_type}")
|
| 832 |
-
else:
|
| 833 |
-
aa_id = mapping[aa_type]
|
| 834 |
-
one_hot_arr[aa_index, aa_id] = 1
|
| 835 |
-
|
| 836 |
-
return one_hot_arr
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
restype_1to3 = {
|
| 840 |
-
"A": "ALA",
|
| 841 |
-
"R": "ARG",
|
| 842 |
-
"N": "ASN",
|
| 843 |
-
"D": "ASP",
|
| 844 |
-
"C": "CYS",
|
| 845 |
-
"Q": "GLN",
|
| 846 |
-
"E": "GLU",
|
| 847 |
-
"G": "GLY",
|
| 848 |
-
"H": "HIS",
|
| 849 |
-
"I": "ILE",
|
| 850 |
-
"L": "LEU",
|
| 851 |
-
"K": "LYS",
|
| 852 |
-
"M": "MET",
|
| 853 |
-
"F": "PHE",
|
| 854 |
-
"P": "PRO",
|
| 855 |
-
"S": "SER",
|
| 856 |
-
"T": "THR",
|
| 857 |
-
"W": "TRP",
|
| 858 |
-
"Y": "TYR",
|
| 859 |
-
"V": "VAL",
|
| 860 |
-
"X": "UNK",
|
| 861 |
-
}
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
# NB: restype_3to1 differs from Bio.PDB.protein_letters_3to1 by being a simple
|
| 865 |
-
# 1-to-1 mapping of 3 letter names to one letter names. The latter contains
|
| 866 |
-
# many more, and less common, three letter names as keys and maps many of these
|
| 867 |
-
# to the same one letter name (including 'X' and 'U' which we don't use here).
|
| 868 |
-
restype_3to1 = {v: k for k, v in restype_1to3.items()}
|
| 869 |
-
|
| 870 |
-
# Define a restype name for all unknown residues.
|
| 871 |
-
unk_restype = "UNK"
|
| 872 |
-
|
| 873 |
-
resnames = [restype_1to3[r] for r in restypes] + [unk_restype]
|
| 874 |
-
resname_to_idx = {resname: i for i, resname in enumerate(resnames)}
|
| 875 |
-
|
| 876 |
-
hydrophobic_resnames = {"VAL", "ILE", "LEU", "PHE", "MET", "TRP"}
|
| 877 |
-
|
| 878 |
-
# The mapping here uses hhblits convention, so that B is mapped to D, J and O
|
| 879 |
-
# are mapped to X, U is mapped to C, and Z is mapped to E. Other than that the
|
| 880 |
-
# remaining 20 amino acids are kept in alphabetical order.
|
| 881 |
-
# There are 2 non-amino acid codes, X (representing any amino acid) and
|
| 882 |
-
# "-" representing a missing amino acid in an alignment. The id for these
|
| 883 |
-
# codes is put at the end (20 and 21) so that they can easily be ignored if
|
| 884 |
-
# desired.
|
| 885 |
-
HHBLITS_AA_TO_ID = {
|
| 886 |
-
"A": 0,
|
| 887 |
-
"B": 2,
|
| 888 |
-
"C": 1,
|
| 889 |
-
"D": 2,
|
| 890 |
-
"E": 3,
|
| 891 |
-
"F": 4,
|
| 892 |
-
"G": 5,
|
| 893 |
-
"H": 6,
|
| 894 |
-
"I": 7,
|
| 895 |
-
"J": 20,
|
| 896 |
-
"K": 8,
|
| 897 |
-
"L": 9,
|
| 898 |
-
"M": 10,
|
| 899 |
-
"N": 11,
|
| 900 |
-
"O": 20,
|
| 901 |
-
"P": 12,
|
| 902 |
-
"Q": 13,
|
| 903 |
-
"R": 14,
|
| 904 |
-
"S": 15,
|
| 905 |
-
"T": 16,
|
| 906 |
-
"U": 1,
|
| 907 |
-
"V": 17,
|
| 908 |
-
"W": 18,
|
| 909 |
-
"X": 20,
|
| 910 |
-
"Y": 19,
|
| 911 |
-
"Z": 3,
|
| 912 |
-
"-": 21,
|
| 913 |
-
}
|
| 914 |
-
|
| 915 |
-
# Partial inversion of HHBLITS_AA_TO_ID.
|
| 916 |
-
ID_TO_HHBLITS_AA = {
|
| 917 |
-
0: "A",
|
| 918 |
-
1: "C", # Also U.
|
| 919 |
-
2: "D", # Also B.
|
| 920 |
-
3: "E", # Also Z.
|
| 921 |
-
4: "F",
|
| 922 |
-
5: "G",
|
| 923 |
-
6: "H",
|
| 924 |
-
7: "I",
|
| 925 |
-
8: "K",
|
| 926 |
-
9: "L",
|
| 927 |
-
10: "M",
|
| 928 |
-
11: "N",
|
| 929 |
-
12: "P",
|
| 930 |
-
13: "Q",
|
| 931 |
-
14: "R",
|
| 932 |
-
15: "S",
|
| 933 |
-
16: "T",
|
| 934 |
-
17: "V",
|
| 935 |
-
18: "W",
|
| 936 |
-
19: "Y",
|
| 937 |
-
20: "X", # Includes J and O.
|
| 938 |
-
21: "-",
|
| 939 |
-
}
|
| 940 |
-
|
| 941 |
-
restypes_with_x_and_gap = restypes + ["X", "-"]
|
| 942 |
-
MAP_HHBLITS_AATYPE_TO_OUR_AATYPE = tuple(
|
| 943 |
-
restypes_with_x_and_gap.index(ID_TO_HHBLITS_AA[i])
|
| 944 |
-
for i in range(len(restypes_with_x_and_gap))
|
| 945 |
-
)
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
def _make_standard_atom_mask() -> np.ndarray:
|
| 949 |
-
"""Returns [num_res_types, num_atom_types] mask array."""
|
| 950 |
-
# +1 to account for unknown (all 0s).
|
| 951 |
-
mask = np.zeros([restype_num + 1, atom_type_num], dtype=np.int32)
|
| 952 |
-
for restype, restype_letter in enumerate(restypes):
|
| 953 |
-
restype_name = restype_1to3[restype_letter]
|
| 954 |
-
atom_names = residue_atoms[restype_name]
|
| 955 |
-
for atom_name in atom_names:
|
| 956 |
-
atom_type = atom_order[atom_name]
|
| 957 |
-
mask[restype, atom_type] = 1
|
| 958 |
-
return mask
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
STANDARD_ATOM_MASK = _make_standard_atom_mask()
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
# A one hot representation for the first and second atoms defining the axis
|
| 965 |
-
# of rotation for each chi-angle in each residue.
|
| 966 |
-
def chi_angle_atom(atom_index: int) -> np.ndarray:
|
| 967 |
-
"""Define chi-angle rigid groups via one-hot representations."""
|
| 968 |
-
chi_angles_index = {}
|
| 969 |
-
one_hots = []
|
| 970 |
-
|
| 971 |
-
for k, v in chi_angles_atoms.items():
|
| 972 |
-
indices = [atom_types.index(s[atom_index]) for s in v]
|
| 973 |
-
indices.extend([-1] * (4 - len(indices)))
|
| 974 |
-
chi_angles_index[k] = indices
|
| 975 |
-
|
| 976 |
-
for r in restypes:
|
| 977 |
-
res3 = restype_1to3[r]
|
| 978 |
-
one_hot = np.eye(atom_type_num)[chi_angles_index[res3]]
|
| 979 |
-
one_hots.append(one_hot)
|
| 980 |
-
|
| 981 |
-
one_hots.append(np.zeros([4, atom_type_num])) # Add zeros for residue `X`.
|
| 982 |
-
one_hot = np.stack(one_hots, axis=0)
|
| 983 |
-
one_hot = np.transpose(one_hot, [0, 2, 1])
|
| 984 |
-
|
| 985 |
-
return one_hot
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
chi_atom_1_one_hot = chi_angle_atom(1)
|
| 989 |
-
chi_atom_2_one_hot = chi_angle_atom(2)
|
| 990 |
-
|
| 991 |
-
# An array like chi_angles_atoms but using indices rather than names.
|
| 992 |
-
chi_angles_atom_indices = [chi_angles_atoms[restype_1to3[r]] for r in restypes]
|
| 993 |
-
# chi_angles_atom_indices = tree.map_structure(
|
| 994 |
-
# lambda atom_name: atom_order[atom_name], chi_angles_atom_indices
|
| 995 |
-
# )
|
| 996 |
-
chi_angles_atom_indices = np.array(
|
| 997 |
-
[
|
| 998 |
-
chi_atoms + ([[0, 0, 0, 0]] * (4 - len(chi_atoms)))
|
| 999 |
-
for chi_atoms in chi_angles_atom_indices
|
| 1000 |
-
]
|
| 1001 |
-
)
|
| 1002 |
-
|
| 1003 |
-
# Mapping from (res_name, atom_name) pairs to the atom's chi group index
|
| 1004 |
-
# and atom index within that group.
|
| 1005 |
-
chi_groups_for_atom = collections.defaultdict(list)
|
| 1006 |
-
for res_name, chi_angle_atoms_for_res in chi_angles_atoms.items():
|
| 1007 |
-
for chi_group_i, chi_group in enumerate(chi_angle_atoms_for_res):
|
| 1008 |
-
for atom_i, atom in enumerate(chi_group):
|
| 1009 |
-
chi_groups_for_atom[(res_name, atom)].append((chi_group_i, atom_i))
|
| 1010 |
-
chi_groups_for_atom = dict(chi_groups_for_atom)
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
def _make_rigid_transformation_4x4(ex, ey, translation):
|
| 1014 |
-
"""Create a rigid 4x4 transformation matrix from two axes and transl."""
|
| 1015 |
-
# Normalize ex.
|
| 1016 |
-
ex_normalized = ex / np.linalg.norm(ex)
|
| 1017 |
-
|
| 1018 |
-
# make ey perpendicular to ex
|
| 1019 |
-
ey_normalized = ey - np.dot(ey, ex_normalized) * ex_normalized
|
| 1020 |
-
ey_normalized /= np.linalg.norm(ey_normalized)
|
| 1021 |
-
|
| 1022 |
-
# compute ez as cross product
|
| 1023 |
-
eznorm = np.cross(ex_normalized, ey_normalized)
|
| 1024 |
-
m = np.stack([ex_normalized, ey_normalized, eznorm, translation]).transpose()
|
| 1025 |
-
m = np.concatenate([m, [[0.0, 0.0, 0.0, 1.0]]], axis=0)
|
| 1026 |
-
return m
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
# create an array with (restype, atomtype) --> rigid_group_idx
|
| 1030 |
-
# and an array with (restype, atomtype, coord) for the atom positions
|
| 1031 |
-
# and compute affine transformation matrices (4,4) from one rigid group to the
|
| 1032 |
-
# previous group
|
| 1033 |
-
restype_atom37_to_rigid_group = np.zeros([21, 37], dtype=int)
|
| 1034 |
-
restype_atom37_mask = np.zeros([21, 37], dtype=np.float32)
|
| 1035 |
-
restype_atom37_rigid_group_positions = np.zeros([21, 37, 3], dtype=np.float32)
|
| 1036 |
-
restype_atom14_to_rigid_group = np.zeros([21, 14], dtype=int)
|
| 1037 |
-
restype_atom14_mask = np.zeros([21, 14], dtype=np.float32)
|
| 1038 |
-
restype_atom14_rigid_group_positions = np.zeros([21, 14, 3], dtype=np.float32)
|
| 1039 |
-
restype_rigid_group_default_frame = np.zeros([21, 8, 4, 4], dtype=np.float32)
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
def _make_rigid_group_constants():
|
| 1043 |
-
"""Fill the arrays above."""
|
| 1044 |
-
for restype, restype_letter in enumerate(restypes_with_x):
|
| 1045 |
-
resname = restype_1to3[restype_letter]
|
| 1046 |
-
for atomname, group_idx, atom_position in rigid_group_atom_positions[resname]:
|
| 1047 |
-
atomtype = atom_order[atomname]
|
| 1048 |
-
restype_atom37_to_rigid_group[restype, atomtype] = group_idx
|
| 1049 |
-
restype_atom37_mask[restype, atomtype] = 1
|
| 1050 |
-
restype_atom37_rigid_group_positions[restype, atomtype, :] = atom_position
|
| 1051 |
-
|
| 1052 |
-
atom14idx = restype_name_to_atom14_names[resname].index(atomname)
|
| 1053 |
-
restype_atom14_to_rigid_group[restype, atom14idx] = group_idx
|
| 1054 |
-
restype_atom14_mask[restype, atom14idx] = 1
|
| 1055 |
-
restype_atom14_rigid_group_positions[restype, atom14idx, :] = atom_position
|
| 1056 |
-
|
| 1057 |
-
for restype, restype_letter in enumerate(restypes_with_x):
|
| 1058 |
-
resname = restype_1to3[restype_letter]
|
| 1059 |
-
atom_positions = {
|
| 1060 |
-
name: np.array(pos) for name, _, pos in rigid_group_atom_positions[resname]
|
| 1061 |
-
}
|
| 1062 |
-
|
| 1063 |
-
# backbone to backbone is the identity transform
|
| 1064 |
-
restype_rigid_group_default_frame[restype, 0, :, :] = np.eye(4)
|
| 1065 |
-
|
| 1066 |
-
# pre-omega-frame to backbone (currently dummy identity matrix)
|
| 1067 |
-
restype_rigid_group_default_frame[restype, 1, :, :] = np.eye(4)
|
| 1068 |
-
|
| 1069 |
-
# phi-frame to backbone
|
| 1070 |
-
mat = _make_rigid_transformation_4x4(
|
| 1071 |
-
ex=atom_positions["N"] - atom_positions["CA"],
|
| 1072 |
-
ey=np.array([1.0, 0.0, 0.0]),
|
| 1073 |
-
translation=atom_positions["N"],
|
| 1074 |
-
)
|
| 1075 |
-
restype_rigid_group_default_frame[restype, 2, :, :] = mat
|
| 1076 |
-
|
| 1077 |
-
# psi-frame to backbone
|
| 1078 |
-
mat = _make_rigid_transformation_4x4(
|
| 1079 |
-
ex=atom_positions["C"] - atom_positions["CA"],
|
| 1080 |
-
ey=atom_positions["CA"] - atom_positions["N"],
|
| 1081 |
-
translation=atom_positions["C"],
|
| 1082 |
-
)
|
| 1083 |
-
restype_rigid_group_default_frame[restype, 3, :, :] = mat
|
| 1084 |
-
|
| 1085 |
-
# chi1-frame to backbone
|
| 1086 |
-
if chi_angles_mask[restype][0]:
|
| 1087 |
-
base_atom_names = chi_angles_atoms[resname][0]
|
| 1088 |
-
base_atom_positions = [atom_positions[name] for name in base_atom_names]
|
| 1089 |
-
mat = _make_rigid_transformation_4x4(
|
| 1090 |
-
ex=base_atom_positions[2] - base_atom_positions[1],
|
| 1091 |
-
ey=base_atom_positions[0] - base_atom_positions[1],
|
| 1092 |
-
translation=base_atom_positions[2],
|
| 1093 |
-
)
|
| 1094 |
-
restype_rigid_group_default_frame[restype, 4, :, :] = mat
|
| 1095 |
-
|
| 1096 |
-
# chi2-frame to chi1-frame
|
| 1097 |
-
# chi3-frame to chi2-frame
|
| 1098 |
-
# chi4-frame to chi3-frame
|
| 1099 |
-
# luckily all rotation axes for the next frame start at (0,0,0) of the
|
| 1100 |
-
# previous frame
|
| 1101 |
-
for chi_idx in range(1, 4):
|
| 1102 |
-
if chi_angles_mask[restype][chi_idx]:
|
| 1103 |
-
axis_end_atom_name = chi_angles_atoms[resname][chi_idx][2]
|
| 1104 |
-
axis_end_atom_position = atom_positions[axis_end_atom_name]
|
| 1105 |
-
mat = _make_rigid_transformation_4x4(
|
| 1106 |
-
ex=axis_end_atom_position,
|
| 1107 |
-
ey=np.array([-1.0, 0.0, 0.0]),
|
| 1108 |
-
translation=axis_end_atom_position,
|
| 1109 |
-
)
|
| 1110 |
-
restype_rigid_group_default_frame[restype, 4 + chi_idx, :, :] = mat
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
_make_rigid_group_constants()
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
def make_atom14_dists_bounds(overlap_tolerance=1.5, bond_length_tolerance_factor=15.0):
|
| 1117 |
-
"""compute upper and lower bounds for bonds to assess violations."""
|
| 1118 |
-
restype_atom14_bond_lower_bound = np.zeros([21, 14, 14], np.float32)
|
| 1119 |
-
restype_atom14_bond_upper_bound = np.zeros([21, 14, 14], np.float32)
|
| 1120 |
-
restype_atom14_bond_stddev = np.zeros([21, 14, 14], np.float32)
|
| 1121 |
-
residue_bonds, residue_virtual_bonds, _ = load_stereo_chemical_props()
|
| 1122 |
-
for restype, restype_letter in enumerate(restypes):
|
| 1123 |
-
resname = restype_1to3[restype_letter]
|
| 1124 |
-
atom_list = restype_name_to_atom14_names[resname]
|
| 1125 |
-
|
| 1126 |
-
# create lower and upper bounds for clashes
|
| 1127 |
-
for atom1_idx, atom1_name in enumerate(atom_list):
|
| 1128 |
-
if not atom1_name:
|
| 1129 |
-
continue
|
| 1130 |
-
atom1_radius = van_der_waals_radius[atom1_name[0]]
|
| 1131 |
-
for atom2_idx, atom2_name in enumerate(atom_list):
|
| 1132 |
-
if (not atom2_name) or atom1_idx == atom2_idx:
|
| 1133 |
-
continue
|
| 1134 |
-
atom2_radius = van_der_waals_radius[atom2_name[0]]
|
| 1135 |
-
lower = atom1_radius + atom2_radius - overlap_tolerance
|
| 1136 |
-
upper = 1e10
|
| 1137 |
-
restype_atom14_bond_lower_bound[restype, atom1_idx, atom2_idx] = lower
|
| 1138 |
-
restype_atom14_bond_lower_bound[restype, atom2_idx, atom1_idx] = lower
|
| 1139 |
-
restype_atom14_bond_upper_bound[restype, atom1_idx, atom2_idx] = upper
|
| 1140 |
-
restype_atom14_bond_upper_bound[restype, atom2_idx, atom1_idx] = upper
|
| 1141 |
-
|
| 1142 |
-
# overwrite lower and upper bounds for bonds and angles
|
| 1143 |
-
for b in residue_bonds[resname] + residue_virtual_bonds[resname]:
|
| 1144 |
-
atom1_idx = atom_list.index(b.atom1_name)
|
| 1145 |
-
atom2_idx = atom_list.index(b.atom2_name)
|
| 1146 |
-
lower = b.length - bond_length_tolerance_factor * b.stddev
|
| 1147 |
-
upper = b.length + bond_length_tolerance_factor * b.stddev
|
| 1148 |
-
restype_atom14_bond_lower_bound[restype, atom1_idx, atom2_idx] = lower
|
| 1149 |
-
restype_atom14_bond_lower_bound[restype, atom2_idx, atom1_idx] = lower
|
| 1150 |
-
restype_atom14_bond_upper_bound[restype, atom1_idx, atom2_idx] = upper
|
| 1151 |
-
restype_atom14_bond_upper_bound[restype, atom2_idx, atom1_idx] = upper
|
| 1152 |
-
restype_atom14_bond_stddev[restype, atom1_idx, atom2_idx] = b.stddev
|
| 1153 |
-
restype_atom14_bond_stddev[restype, atom2_idx, atom1_idx] = b.stddev
|
| 1154 |
-
return {
|
| 1155 |
-
"lower_bound": restype_atom14_bond_lower_bound, # shape (21,14,14)
|
| 1156 |
-
"upper_bound": restype_atom14_bond_upper_bound, # shape (21,14,14)
|
| 1157 |
-
"stddev": restype_atom14_bond_stddev, # shape (21,14,14)
|
| 1158 |
-
}
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
restype_atom14_ambiguous_atoms = np.zeros((21, 14), dtype=np.float32)
|
| 1162 |
-
restype_atom14_ambiguous_atoms_swap_idx = np.tile(np.arange(14, dtype=int), (21, 1))
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
def _make_atom14_ambiguity_feats():
|
| 1166 |
-
for res, pairs in residue_atom_renaming_swaps.items():
|
| 1167 |
-
res_idx = restype_order[restype_3to1[res]]
|
| 1168 |
-
for atom1, atom2 in pairs.items():
|
| 1169 |
-
atom1_idx = restype_name_to_atom14_names[res].index(atom1)
|
| 1170 |
-
atom2_idx = restype_name_to_atom14_names[res].index(atom2)
|
| 1171 |
-
restype_atom14_ambiguous_atoms[res_idx, atom1_idx] = 1
|
| 1172 |
-
restype_atom14_ambiguous_atoms[res_idx, atom2_idx] = 1
|
| 1173 |
-
restype_atom14_ambiguous_atoms_swap_idx[res_idx, atom1_idx] = atom2_idx
|
| 1174 |
-
restype_atom14_ambiguous_atoms_swap_idx[res_idx, atom2_idx] = atom1_idx
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
_make_atom14_ambiguity_feats()
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
def aatype_to_str_sequence(aatype):
|
| 1181 |
-
return "".join([restypes_with_x[aatype[i]] for i in range(len(aatype))])
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
# NOTE(thayes): These are computed based on the average CA->C and CA->N norm from rigid_group_atom_positions
|
| 1185 |
-
CA_TO_N_NORM = 1.4591
|
| 1186 |
-
CA_TO_C_NORM = 1.5252
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
def _make_restype_atom37_to_atom14():
|
| 1190 |
-
"""Map from atom37 to atom14 per residue type."""
|
| 1191 |
-
restype_atom37_to_atom14 = [] # mapping (restype, atom37) --> atom14
|
| 1192 |
-
for rt in restypes:
|
| 1193 |
-
atom_names = restype_name_to_atom14_names[restype_1to3[rt]]
|
| 1194 |
-
atom_name_to_idx14 = {name: i for i, name in enumerate(atom_names)}
|
| 1195 |
-
restype_atom37_to_atom14.append(
|
| 1196 |
-
[
|
| 1197 |
-
(atom_name_to_idx14[name] if name in atom_name_to_idx14 else 0)
|
| 1198 |
-
for name in atom_types
|
| 1199 |
-
]
|
| 1200 |
-
)
|
| 1201 |
-
|
| 1202 |
-
restype_atom37_to_atom14.append([0] * 37)
|
| 1203 |
-
restype_atom37_to_atom14 = np.array(restype_atom37_to_atom14, dtype=np.int32)
|
| 1204 |
-
return restype_atom37_to_atom14
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
def _make_restype_atom14_to_atom37():
|
| 1208 |
-
"""Map from atom14 to atom37 per residue type."""
|
| 1209 |
-
restype_atom14_to_atom37 = [] # mapping (restype, atom14) --> atom37
|
| 1210 |
-
for rt in restypes:
|
| 1211 |
-
atom_names = restype_name_to_atom14_names[restype_1to3[rt]]
|
| 1212 |
-
restype_atom14_to_atom37.append(
|
| 1213 |
-
[(atom_order[name] if name else 0) for name in atom_names]
|
| 1214 |
-
)
|
| 1215 |
-
# Add dummy mapping for restype 'UNK'
|
| 1216 |
-
restype_atom14_to_atom37.append([0] * 14)
|
| 1217 |
-
restype_atom14_to_atom37 = np.array(restype_atom14_to_atom37, dtype=np.int32)
|
| 1218 |
-
return restype_atom14_to_atom37
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
RESTYPE_ATOM14_TO_ATOM37 = _make_restype_atom14_to_atom37()
|
| 1222 |
-
RESTYPE_ATOM37_TO_ATOM14 = _make_restype_atom37_to_atom14()
|
| 1223 |
-
CHAIN_BREAK_TOKEN = "|"
|
|
|
|
| 1 |
+
# Copyright 2025 EvolutionaryScale
|
| 2 |
+
# Copyright 2021 AlQuraishi Laboratory
|
| 3 |
+
# Copyright 2021 DeepMind Technologies Limited
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
"""Constants used in AlphaFold."""
|
| 18 |
+
|
| 19 |
+
import collections
|
| 20 |
+
import functools
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import List, Mapping, Tuple
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
|
| 26 |
+
# import tree
|
| 27 |
+
|
| 28 |
+
# Internal import (35fd).
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Distance from one CA to next CA [trans configuration: omega = 180].
|
| 32 |
+
ca_ca = 3.80209737096
|
| 33 |
+
|
| 34 |
+
# Format: The list for each AA type contains chi1, chi2, chi3, chi4 in
|
| 35 |
+
# this order (or a relevant subset from chi1 onwards). ALA and GLY don't have
|
| 36 |
+
# chi angles so their chi angle lists are empty.
|
| 37 |
+
chi_angles_atoms = {
|
| 38 |
+
"ALA": [],
|
| 39 |
+
# Chi5 in arginine is always 0 +- 5 degrees, so ignore it.
|
| 40 |
+
"ARG": [
|
| 41 |
+
["N", "CA", "CB", "CG"],
|
| 42 |
+
["CA", "CB", "CG", "CD"],
|
| 43 |
+
["CB", "CG", "CD", "NE"],
|
| 44 |
+
["CG", "CD", "NE", "CZ"],
|
| 45 |
+
],
|
| 46 |
+
"ASN": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "OD1"]],
|
| 47 |
+
"ASP": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "OD1"]],
|
| 48 |
+
"CYS": [["N", "CA", "CB", "SG"]],
|
| 49 |
+
"GLN": [
|
| 50 |
+
["N", "CA", "CB", "CG"],
|
| 51 |
+
["CA", "CB", "CG", "CD"],
|
| 52 |
+
["CB", "CG", "CD", "OE1"],
|
| 53 |
+
],
|
| 54 |
+
"GLU": [
|
| 55 |
+
["N", "CA", "CB", "CG"],
|
| 56 |
+
["CA", "CB", "CG", "CD"],
|
| 57 |
+
["CB", "CG", "CD", "OE1"],
|
| 58 |
+
],
|
| 59 |
+
"GLY": [],
|
| 60 |
+
"HIS": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "ND1"]],
|
| 61 |
+
"ILE": [["N", "CA", "CB", "CG1"], ["CA", "CB", "CG1", "CD1"]],
|
| 62 |
+
"LEU": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 63 |
+
"LYS": [
|
| 64 |
+
["N", "CA", "CB", "CG"],
|
| 65 |
+
["CA", "CB", "CG", "CD"],
|
| 66 |
+
["CB", "CG", "CD", "CE"],
|
| 67 |
+
["CG", "CD", "CE", "NZ"],
|
| 68 |
+
],
|
| 69 |
+
"MET": [
|
| 70 |
+
["N", "CA", "CB", "CG"],
|
| 71 |
+
["CA", "CB", "CG", "SD"],
|
| 72 |
+
["CB", "CG", "SD", "CE"],
|
| 73 |
+
],
|
| 74 |
+
"PHE": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 75 |
+
"PRO": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD"]],
|
| 76 |
+
"SER": [["N", "CA", "CB", "OG"]],
|
| 77 |
+
"THR": [["N", "CA", "CB", "OG1"]],
|
| 78 |
+
"TRP": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 79 |
+
"TYR": [["N", "CA", "CB", "CG"], ["CA", "CB", "CG", "CD1"]],
|
| 80 |
+
"VAL": [["N", "CA", "CB", "CG1"]],
|
| 81 |
+
"UNK": [],
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# If chi angles given in fixed-length array, this matrix determines how to mask
|
| 85 |
+
# them for each AA type. The order is as per restype_order (see below).
|
| 86 |
+
chi_angles_mask = [
|
| 87 |
+
[0.0, 0.0, 0.0, 0.0], # ALA
|
| 88 |
+
[1.0, 1.0, 1.0, 1.0], # ARG
|
| 89 |
+
[1.0, 1.0, 0.0, 0.0], # ASN
|
| 90 |
+
[1.0, 1.0, 0.0, 0.0], # ASP
|
| 91 |
+
[1.0, 0.0, 0.0, 0.0], # CYS
|
| 92 |
+
[1.0, 1.0, 1.0, 0.0], # GLN
|
| 93 |
+
[1.0, 1.0, 1.0, 0.0], # GLU
|
| 94 |
+
[0.0, 0.0, 0.0, 0.0], # GLY
|
| 95 |
+
[1.0, 1.0, 0.0, 0.0], # HIS
|
| 96 |
+
[1.0, 1.0, 0.0, 0.0], # ILE
|
| 97 |
+
[1.0, 1.0, 0.0, 0.0], # LEU
|
| 98 |
+
[1.0, 1.0, 1.0, 1.0], # LYS
|
| 99 |
+
[1.0, 1.0, 1.0, 0.0], # MET
|
| 100 |
+
[1.0, 1.0, 0.0, 0.0], # PHE
|
| 101 |
+
[1.0, 1.0, 0.0, 0.0], # PRO
|
| 102 |
+
[1.0, 0.0, 0.0, 0.0], # SER
|
| 103 |
+
[1.0, 0.0, 0.0, 0.0], # THR
|
| 104 |
+
[1.0, 1.0, 0.0, 0.0], # TRP
|
| 105 |
+
[1.0, 1.0, 0.0, 0.0], # TYR
|
| 106 |
+
[1.0, 0.0, 0.0, 0.0], # VAL
|
| 107 |
+
[0.0, 0.0, 0.0, 0.0], # UNK
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
# The following chi angles are pi periodic: they can be rotated by a multiple
|
| 111 |
+
# of pi without affecting the structure.
|
| 112 |
+
chi_pi_periodic = [
|
| 113 |
+
[0.0, 0.0, 0.0, 0.0], # ALA
|
| 114 |
+
[0.0, 0.0, 0.0, 0.0], # ARG
|
| 115 |
+
[0.0, 0.0, 0.0, 0.0], # ASN
|
| 116 |
+
[0.0, 1.0, 0.0, 0.0], # ASP
|
| 117 |
+
[0.0, 0.0, 0.0, 0.0], # CYS
|
| 118 |
+
[0.0, 0.0, 0.0, 0.0], # GLN
|
| 119 |
+
[0.0, 0.0, 1.0, 0.0], # GLU
|
| 120 |
+
[0.0, 0.0, 0.0, 0.0], # GLY
|
| 121 |
+
[0.0, 0.0, 0.0, 0.0], # HIS
|
| 122 |
+
[0.0, 0.0, 0.0, 0.0], # ILE
|
| 123 |
+
[0.0, 0.0, 0.0, 0.0], # LEU
|
| 124 |
+
[0.0, 0.0, 0.0, 0.0], # LYS
|
| 125 |
+
[0.0, 0.0, 0.0, 0.0], # MET
|
| 126 |
+
[0.0, 1.0, 0.0, 0.0], # PHE
|
| 127 |
+
[0.0, 0.0, 0.0, 0.0], # PRO
|
| 128 |
+
[0.0, 0.0, 0.0, 0.0], # SER
|
| 129 |
+
[0.0, 0.0, 0.0, 0.0], # THR
|
| 130 |
+
[0.0, 0.0, 0.0, 0.0], # TRP
|
| 131 |
+
[0.0, 1.0, 0.0, 0.0], # TYR
|
| 132 |
+
[0.0, 0.0, 0.0, 0.0], # VAL
|
| 133 |
+
[0.0, 0.0, 0.0, 0.0], # UNK
|
| 134 |
+
]
|
| 135 |
+
|
| 136 |
+
# Atoms positions relative to the 8 rigid groups, defined by the pre-omega, phi,
|
| 137 |
+
# psi and chi angles:
|
| 138 |
+
# 0: 'backbone group',
|
| 139 |
+
# 1: 'pre-omega-group', (empty)
|
| 140 |
+
# 2: 'phi-group', (currently empty, because it defines only hydrogens)
|
| 141 |
+
# 3: 'psi-group',
|
| 142 |
+
# 4,5,6,7: 'chi1,2,3,4-group'
|
| 143 |
+
# The atom positions are relative to the axis-end-atom of the corresponding
|
| 144 |
+
# rotation axis. The x-axis is in direction of the rotation axis, and the y-axis
|
| 145 |
+
# is defined such that the dihedral-angle-definiting atom (the last entry in
|
| 146 |
+
# chi_angles_atoms above) is in the xy-plane (with a positive y-coordinate).
|
| 147 |
+
# format: [atomname, group_idx, rel_position]
|
| 148 |
+
rigid_group_atom_positions = {
|
| 149 |
+
"ALA": [
|
| 150 |
+
["N", 0, (-0.525, 1.363, 0.000)],
|
| 151 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 152 |
+
["C", 0, (1.526, -0.000, -0.000)],
|
| 153 |
+
["CB", 0, (-0.529, -0.774, -1.205)],
|
| 154 |
+
["O", 3, (0.627, 1.062, 0.000)],
|
| 155 |
+
],
|
| 156 |
+
"ARG": [
|
| 157 |
+
["N", 0, (-0.524, 1.362, -0.000)],
|
| 158 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 159 |
+
["C", 0, (1.525, -0.000, -0.000)],
|
| 160 |
+
["CB", 0, (-0.524, -0.778, -1.209)],
|
| 161 |
+
["O", 3, (0.626, 1.062, 0.000)],
|
| 162 |
+
["CG", 4, (0.616, 1.390, -0.000)],
|
| 163 |
+
["CD", 5, (0.564, 1.414, 0.000)],
|
| 164 |
+
["NE", 6, (0.539, 1.357, -0.000)],
|
| 165 |
+
["NH1", 7, (0.206, 2.301, 0.000)],
|
| 166 |
+
["NH2", 7, (2.078, 0.978, -0.000)],
|
| 167 |
+
["CZ", 7, (0.758, 1.093, -0.000)],
|
| 168 |
+
],
|
| 169 |
+
"ASN": [
|
| 170 |
+
["N", 0, (-0.536, 1.357, 0.000)],
|
| 171 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 172 |
+
["C", 0, (1.526, -0.000, -0.000)],
|
| 173 |
+
["CB", 0, (-0.531, -0.787, -1.200)],
|
| 174 |
+
["O", 3, (0.625, 1.062, 0.000)],
|
| 175 |
+
["CG", 4, (0.584, 1.399, 0.000)],
|
| 176 |
+
["ND2", 5, (0.593, -1.188, 0.001)],
|
| 177 |
+
["OD1", 5, (0.633, 1.059, 0.000)],
|
| 178 |
+
],
|
| 179 |
+
"ASP": [
|
| 180 |
+
["N", 0, (-0.525, 1.362, -0.000)],
|
| 181 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 182 |
+
["C", 0, (1.527, 0.000, -0.000)],
|
| 183 |
+
["CB", 0, (-0.526, -0.778, -1.208)],
|
| 184 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 185 |
+
["CG", 4, (0.593, 1.398, -0.000)],
|
| 186 |
+
["OD1", 5, (0.610, 1.091, 0.000)],
|
| 187 |
+
["OD2", 5, (0.592, -1.101, -0.003)],
|
| 188 |
+
],
|
| 189 |
+
"CYS": [
|
| 190 |
+
["N", 0, (-0.522, 1.362, -0.000)],
|
| 191 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 192 |
+
["C", 0, (1.524, 0.000, 0.000)],
|
| 193 |
+
["CB", 0, (-0.519, -0.773, -1.212)],
|
| 194 |
+
["O", 3, (0.625, 1.062, -0.000)],
|
| 195 |
+
["SG", 4, (0.728, 1.653, 0.000)],
|
| 196 |
+
],
|
| 197 |
+
"GLN": [
|
| 198 |
+
["N", 0, (-0.526, 1.361, -0.000)],
|
| 199 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 200 |
+
["C", 0, (1.526, 0.000, 0.000)],
|
| 201 |
+
["CB", 0, (-0.525, -0.779, -1.207)],
|
| 202 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 203 |
+
["CG", 4, (0.615, 1.393, 0.000)],
|
| 204 |
+
["CD", 5, (0.587, 1.399, -0.000)],
|
| 205 |
+
["NE2", 6, (0.593, -1.189, -0.001)],
|
| 206 |
+
["OE1", 6, (0.634, 1.060, 0.000)],
|
| 207 |
+
],
|
| 208 |
+
"GLU": [
|
| 209 |
+
["N", 0, (-0.528, 1.361, 0.000)],
|
| 210 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 211 |
+
["C", 0, (1.526, -0.000, -0.000)],
|
| 212 |
+
["CB", 0, (-0.526, -0.781, -1.207)],
|
| 213 |
+
["O", 3, (0.626, 1.062, 0.000)],
|
| 214 |
+
["CG", 4, (0.615, 1.392, 0.000)],
|
| 215 |
+
["CD", 5, (0.600, 1.397, 0.000)],
|
| 216 |
+
["OE1", 6, (0.607, 1.095, -0.000)],
|
| 217 |
+
["OE2", 6, (0.589, -1.104, -0.001)],
|
| 218 |
+
],
|
| 219 |
+
"GLY": [
|
| 220 |
+
["N", 0, (-0.572, 1.337, 0.000)],
|
| 221 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 222 |
+
["C", 0, (1.517, -0.000, -0.000)],
|
| 223 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 224 |
+
],
|
| 225 |
+
"HIS": [
|
| 226 |
+
["N", 0, (-0.527, 1.360, 0.000)],
|
| 227 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 228 |
+
["C", 0, (1.525, 0.000, 0.000)],
|
| 229 |
+
["CB", 0, (-0.525, -0.778, -1.208)],
|
| 230 |
+
["O", 3, (0.625, 1.063, 0.000)],
|
| 231 |
+
["CG", 4, (0.600, 1.370, -0.000)],
|
| 232 |
+
["CD2", 5, (0.889, -1.021, 0.003)],
|
| 233 |
+
["ND1", 5, (0.744, 1.160, -0.000)],
|
| 234 |
+
["CE1", 5, (2.030, 0.851, 0.002)],
|
| 235 |
+
["NE2", 5, (2.145, -0.466, 0.004)],
|
| 236 |
+
],
|
| 237 |
+
"ILE": [
|
| 238 |
+
["N", 0, (-0.493, 1.373, -0.000)],
|
| 239 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 240 |
+
["C", 0, (1.527, -0.000, -0.000)],
|
| 241 |
+
["CB", 0, (-0.536, -0.793, -1.213)],
|
| 242 |
+
["O", 3, (0.627, 1.062, -0.000)],
|
| 243 |
+
["CG1", 4, (0.534, 1.437, -0.000)],
|
| 244 |
+
["CG2", 4, (0.540, -0.785, -1.199)],
|
| 245 |
+
["CD1", 5, (0.619, 1.391, 0.000)],
|
| 246 |
+
],
|
| 247 |
+
"LEU": [
|
| 248 |
+
["N", 0, (-0.520, 1.363, 0.000)],
|
| 249 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 250 |
+
["C", 0, (1.525, -0.000, -0.000)],
|
| 251 |
+
["CB", 0, (-0.522, -0.773, -1.214)],
|
| 252 |
+
["O", 3, (0.625, 1.063, -0.000)],
|
| 253 |
+
["CG", 4, (0.678, 1.371, 0.000)],
|
| 254 |
+
["CD1", 5, (0.530, 1.430, -0.000)],
|
| 255 |
+
["CD2", 5, (0.535, -0.774, 1.200)],
|
| 256 |
+
],
|
| 257 |
+
"LYS": [
|
| 258 |
+
["N", 0, (-0.526, 1.362, -0.000)],
|
| 259 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 260 |
+
["C", 0, (1.526, 0.000, 0.000)],
|
| 261 |
+
["CB", 0, (-0.524, -0.778, -1.208)],
|
| 262 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 263 |
+
["CG", 4, (0.619, 1.390, 0.000)],
|
| 264 |
+
["CD", 5, (0.559, 1.417, 0.000)],
|
| 265 |
+
["CE", 6, (0.560, 1.416, 0.000)],
|
| 266 |
+
["NZ", 7, (0.554, 1.387, 0.000)],
|
| 267 |
+
],
|
| 268 |
+
"MET": [
|
| 269 |
+
["N", 0, (-0.521, 1.364, -0.000)],
|
| 270 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 271 |
+
["C", 0, (1.525, 0.000, 0.000)],
|
| 272 |
+
["CB", 0, (-0.523, -0.776, -1.210)],
|
| 273 |
+
["O", 3, (0.625, 1.062, -0.000)],
|
| 274 |
+
["CG", 4, (0.613, 1.391, -0.000)],
|
| 275 |
+
["SD", 5, (0.703, 1.695, 0.000)],
|
| 276 |
+
["CE", 6, (0.320, 1.786, -0.000)],
|
| 277 |
+
],
|
| 278 |
+
"PHE": [
|
| 279 |
+
["N", 0, (-0.518, 1.363, 0.000)],
|
| 280 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 281 |
+
["C", 0, (1.524, 0.000, -0.000)],
|
| 282 |
+
["CB", 0, (-0.525, -0.776, -1.212)],
|
| 283 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 284 |
+
["CG", 4, (0.607, 1.377, 0.000)],
|
| 285 |
+
["CD1", 5, (0.709, 1.195, -0.000)],
|
| 286 |
+
["CD2", 5, (0.706, -1.196, 0.000)],
|
| 287 |
+
["CE1", 5, (2.102, 1.198, -0.000)],
|
| 288 |
+
["CE2", 5, (2.098, -1.201, -0.000)],
|
| 289 |
+
["CZ", 5, (2.794, -0.003, -0.001)],
|
| 290 |
+
],
|
| 291 |
+
"PRO": [
|
| 292 |
+
["N", 0, (-0.566, 1.351, -0.000)],
|
| 293 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 294 |
+
["C", 0, (1.527, -0.000, 0.000)],
|
| 295 |
+
["CB", 0, (-0.546, -0.611, -1.293)],
|
| 296 |
+
["O", 3, (0.621, 1.066, 0.000)],
|
| 297 |
+
["CG", 4, (0.382, 1.445, 0.0)],
|
| 298 |
+
# ['CD', 5, (0.427, 1.440, 0.0)],
|
| 299 |
+
["CD", 5, (0.477, 1.424, 0.0)], # manually made angle 2 degrees larger
|
| 300 |
+
],
|
| 301 |
+
"SER": [
|
| 302 |
+
["N", 0, (-0.529, 1.360, -0.000)],
|
| 303 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 304 |
+
["C", 0, (1.525, -0.000, -0.000)],
|
| 305 |
+
["CB", 0, (-0.518, -0.777, -1.211)],
|
| 306 |
+
["O", 3, (0.626, 1.062, -0.000)],
|
| 307 |
+
["OG", 4, (0.503, 1.325, 0.000)],
|
| 308 |
+
],
|
| 309 |
+
"THR": [
|
| 310 |
+
["N", 0, (-0.517, 1.364, 0.000)],
|
| 311 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 312 |
+
["C", 0, (1.526, 0.000, -0.000)],
|
| 313 |
+
["CB", 0, (-0.516, -0.793, -1.215)],
|
| 314 |
+
["O", 3, (0.626, 1.062, 0.000)],
|
| 315 |
+
["CG2", 4, (0.550, -0.718, -1.228)],
|
| 316 |
+
["OG1", 4, (0.472, 1.353, 0.000)],
|
| 317 |
+
],
|
| 318 |
+
"TRP": [
|
| 319 |
+
["N", 0, (-0.521, 1.363, 0.000)],
|
| 320 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 321 |
+
["C", 0, (1.525, -0.000, 0.000)],
|
| 322 |
+
["CB", 0, (-0.523, -0.776, -1.212)],
|
| 323 |
+
["O", 3, (0.627, 1.062, 0.000)],
|
| 324 |
+
["CG", 4, (0.609, 1.370, -0.000)],
|
| 325 |
+
["CD1", 5, (0.824, 1.091, 0.000)],
|
| 326 |
+
["CD2", 5, (0.854, -1.148, -0.005)],
|
| 327 |
+
["CE2", 5, (2.186, -0.678, -0.007)],
|
| 328 |
+
["CE3", 5, (0.622, -2.530, -0.007)],
|
| 329 |
+
["NE1", 5, (2.140, 0.690, -0.004)],
|
| 330 |
+
["CH2", 5, (3.028, -2.890, -0.013)],
|
| 331 |
+
["CZ2", 5, (3.283, -1.543, -0.011)],
|
| 332 |
+
["CZ3", 5, (1.715, -3.389, -0.011)],
|
| 333 |
+
],
|
| 334 |
+
"TYR": [
|
| 335 |
+
["N", 0, (-0.522, 1.362, 0.000)],
|
| 336 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 337 |
+
["C", 0, (1.524, -0.000, -0.000)],
|
| 338 |
+
["CB", 0, (-0.522, -0.776, -1.213)],
|
| 339 |
+
["O", 3, (0.627, 1.062, -0.000)],
|
| 340 |
+
["CG", 4, (0.607, 1.382, -0.000)],
|
| 341 |
+
["CD1", 5, (0.716, 1.195, -0.000)],
|
| 342 |
+
["CD2", 5, (0.713, -1.194, -0.001)],
|
| 343 |
+
["CE1", 5, (2.107, 1.200, -0.002)],
|
| 344 |
+
["CE2", 5, (2.104, -1.201, -0.003)],
|
| 345 |
+
["OH", 5, (4.168, -0.002, -0.005)],
|
| 346 |
+
["CZ", 5, (2.791, -0.001, -0.003)],
|
| 347 |
+
],
|
| 348 |
+
"VAL": [
|
| 349 |
+
["N", 0, (-0.494, 1.373, -0.000)],
|
| 350 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 351 |
+
["C", 0, (1.527, -0.000, -0.000)],
|
| 352 |
+
["CB", 0, (-0.533, -0.795, -1.213)],
|
| 353 |
+
["O", 3, (0.627, 1.062, -0.000)],
|
| 354 |
+
["CG1", 4, (0.540, 1.429, -0.000)],
|
| 355 |
+
["CG2", 4, (0.533, -0.776, 1.203)],
|
| 356 |
+
],
|
| 357 |
+
# Assume alanine positions for unknown AA
|
| 358 |
+
"UNK": [
|
| 359 |
+
["N", 0, (-0.525, 1.363, 0.000)],
|
| 360 |
+
["CA", 0, (0.000, 0.000, 0.000)],
|
| 361 |
+
["C", 0, (1.526, -0.000, -0.000)],
|
| 362 |
+
],
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
# A list of atoms (excluding hydrogen) for each AA type. PDB naming convention.
|
| 366 |
+
residue_atoms = {
|
| 367 |
+
"ALA": ["C", "CA", "CB", "N", "O"],
|
| 368 |
+
"ARG": ["C", "CA", "CB", "CG", "CD", "CZ", "N", "NE", "O", "NH1", "NH2"],
|
| 369 |
+
"ASP": ["C", "CA", "CB", "CG", "N", "O", "OD1", "OD2"],
|
| 370 |
+
"ASN": ["C", "CA", "CB", "CG", "N", "ND2", "O", "OD1"],
|
| 371 |
+
"CYS": ["C", "CA", "CB", "N", "O", "SG"],
|
| 372 |
+
"GLU": ["C", "CA", "CB", "CG", "CD", "N", "O", "OE1", "OE2"],
|
| 373 |
+
"GLN": ["C", "CA", "CB", "CG", "CD", "N", "NE2", "O", "OE1"],
|
| 374 |
+
"GLY": ["C", "CA", "N", "O"],
|
| 375 |
+
"HIS": ["C", "CA", "CB", "CG", "CD2", "CE1", "N", "ND1", "NE2", "O"],
|
| 376 |
+
"ILE": ["C", "CA", "CB", "CG1", "CG2", "CD1", "N", "O"],
|
| 377 |
+
"LEU": ["C", "CA", "CB", "CG", "CD1", "CD2", "N", "O"],
|
| 378 |
+
"LYS": ["C", "CA", "CB", "CG", "CD", "CE", "N", "NZ", "O"],
|
| 379 |
+
"MET": ["C", "CA", "CB", "CG", "CE", "N", "O", "SD"],
|
| 380 |
+
"PHE": ["C", "CA", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "N", "O"],
|
| 381 |
+
"PRO": ["C", "CA", "CB", "CG", "CD", "N", "O"],
|
| 382 |
+
"SER": ["C", "CA", "CB", "N", "O", "OG"],
|
| 383 |
+
"THR": ["C", "CA", "CB", "CG2", "N", "O", "OG1"],
|
| 384 |
+
"TRP": [
|
| 385 |
+
"C",
|
| 386 |
+
"CA",
|
| 387 |
+
"CB",
|
| 388 |
+
"CG",
|
| 389 |
+
"CD1",
|
| 390 |
+
"CD2",
|
| 391 |
+
"CE2",
|
| 392 |
+
"CE3",
|
| 393 |
+
"CZ2",
|
| 394 |
+
"CZ3",
|
| 395 |
+
"CH2",
|
| 396 |
+
"N",
|
| 397 |
+
"NE1",
|
| 398 |
+
"O",
|
| 399 |
+
],
|
| 400 |
+
"TYR": ["C", "CA", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "N", "O", "OH"],
|
| 401 |
+
"VAL": ["C", "CA", "CB", "CG1", "CG2", "N", "O"],
|
| 402 |
+
"UNK": ["C", "CA", "N"],
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
# Naming swaps for ambiguous atom names.
|
| 406 |
+
# Due to symmetries in the amino acids the naming of atoms is ambiguous in
|
| 407 |
+
# 4 of the 20 amino acids.
|
| 408 |
+
# (The LDDT paper lists 7 amino acids as ambiguous, but the naming ambiguities
|
| 409 |
+
# in LEU, VAL and ARG can be resolved by using the 3d constellations of
|
| 410 |
+
# the 'ambiguous' atoms and their neighbours)
|
| 411 |
+
# TODO: ^ interpret this
|
| 412 |
+
residue_atom_renaming_swaps = {
|
| 413 |
+
"ASP": {"OD1": "OD2"},
|
| 414 |
+
"GLU": {"OE1": "OE2"},
|
| 415 |
+
"PHE": {"CD1": "CD2", "CE1": "CE2"},
|
| 416 |
+
"TYR": {"CD1": "CD2", "CE1": "CE2"},
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
# Van der Waals radii [Angstroem] of the atoms (from Wikipedia)
|
| 420 |
+
van_der_waals_radius = {"C": 1.7, "N": 1.55, "O": 1.52, "S": 1.8}
|
| 421 |
+
|
| 422 |
+
Bond = collections.namedtuple("Bond", ["atom1_name", "atom2_name", "length", "stddev"])
|
| 423 |
+
BondAngle = collections.namedtuple(
|
| 424 |
+
"BondAngle", ["atom1_name", "atom2_name", "atom3name", "angle_rad", "stddev"]
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
@functools.lru_cache(maxsize=None)
|
| 429 |
+
def load_stereo_chemical_props() -> (
|
| 430 |
+
Tuple[
|
| 431 |
+
Mapping[str, List[Bond]],
|
| 432 |
+
Mapping[str, List[Bond]],
|
| 433 |
+
Mapping[str, List[BondAngle]],
|
| 434 |
+
]
|
| 435 |
+
):
|
| 436 |
+
"""Load stereo_chemical_props.txt into a nice structure.
|
| 437 |
+
|
| 438 |
+
Load literature values for bond lengths and bond angles and translate
|
| 439 |
+
bond angles into the length of the opposite edge of the triangle
|
| 440 |
+
("residue_virtual_bonds").
|
| 441 |
+
|
| 442 |
+
Returns:
|
| 443 |
+
residue_bonds: dict that maps resname --> list of Bond tuples
|
| 444 |
+
residue_virtual_bonds: dict that maps resname --> list of Bond tuples
|
| 445 |
+
residue_bond_angles: dict that maps resname --> list of BondAngle tuples
|
| 446 |
+
"""
|
| 447 |
+
stereo_chemical_props = Path(
|
| 448 |
+
"evolutionaryscale/structure/stereo_chemical_props.txt"
|
| 449 |
+
).read_text()
|
| 450 |
+
|
| 451 |
+
lines_iter = iter(stereo_chemical_props.splitlines())
|
| 452 |
+
# Load bond lengths.
|
| 453 |
+
residue_bonds = {}
|
| 454 |
+
next(lines_iter) # Skip header line.
|
| 455 |
+
for line in lines_iter:
|
| 456 |
+
if line.strip() == "-":
|
| 457 |
+
break
|
| 458 |
+
bond, resname, length, stddev = line.split()
|
| 459 |
+
atom1, atom2 = bond.split("-")
|
| 460 |
+
if resname not in residue_bonds:
|
| 461 |
+
residue_bonds[resname] = []
|
| 462 |
+
residue_bonds[resname].append(Bond(atom1, atom2, float(length), float(stddev)))
|
| 463 |
+
residue_bonds["UNK"] = []
|
| 464 |
+
|
| 465 |
+
# Load bond angles.
|
| 466 |
+
residue_bond_angles = {}
|
| 467 |
+
next(lines_iter) # Skip empty line.
|
| 468 |
+
next(lines_iter) # Skip header line.
|
| 469 |
+
for line in lines_iter:
|
| 470 |
+
if line.strip() == "-":
|
| 471 |
+
break
|
| 472 |
+
bond, resname, angle_degree, stddev_degree = line.split()
|
| 473 |
+
atom1, atom2, atom3 = bond.split("-")
|
| 474 |
+
if resname not in residue_bond_angles:
|
| 475 |
+
residue_bond_angles[resname] = []
|
| 476 |
+
residue_bond_angles[resname].append(
|
| 477 |
+
BondAngle(
|
| 478 |
+
atom1,
|
| 479 |
+
atom2,
|
| 480 |
+
atom3,
|
| 481 |
+
float(angle_degree) / 180.0 * np.pi,
|
| 482 |
+
float(stddev_degree) / 180.0 * np.pi,
|
| 483 |
+
)
|
| 484 |
+
)
|
| 485 |
+
residue_bond_angles["UNK"] = []
|
| 486 |
+
|
| 487 |
+
def make_bond_key(atom1_name, atom2_name):
|
| 488 |
+
"""Unique key to lookup bonds."""
|
| 489 |
+
return "-".join(sorted([atom1_name, atom2_name]))
|
| 490 |
+
|
| 491 |
+
# Translate bond angles into distances ("virtual bonds").
|
| 492 |
+
residue_virtual_bonds = {}
|
| 493 |
+
for resname, bond_angles in residue_bond_angles.items():
|
| 494 |
+
# Create a fast lookup dict for bond lengths.
|
| 495 |
+
bond_cache = {}
|
| 496 |
+
for b in residue_bonds[resname]:
|
| 497 |
+
bond_cache[make_bond_key(b.atom1_name, b.atom2_name)] = b
|
| 498 |
+
residue_virtual_bonds[resname] = []
|
| 499 |
+
for ba in bond_angles:
|
| 500 |
+
bond1 = bond_cache[make_bond_key(ba.atom1_name, ba.atom2_name)]
|
| 501 |
+
bond2 = bond_cache[make_bond_key(ba.atom2_name, ba.atom3name)]
|
| 502 |
+
|
| 503 |
+
# Compute distance between atom1 and atom3 using the law of cosines
|
| 504 |
+
# c^2 = a^2 + b^2 - 2ab*cos(gamma).
|
| 505 |
+
gamma = ba.angle_rad
|
| 506 |
+
length = np.sqrt(
|
| 507 |
+
bond1.length**2
|
| 508 |
+
+ bond2.length**2
|
| 509 |
+
- 2 * bond1.length * bond2.length * np.cos(gamma)
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
# Propagation of uncertainty assuming uncorrelated errors.
|
| 513 |
+
dl_outer = 0.5 / length
|
| 514 |
+
dl_dgamma = (2 * bond1.length * bond2.length * np.sin(gamma)) * dl_outer
|
| 515 |
+
dl_db1 = (2 * bond1.length - 2 * bond2.length * np.cos(gamma)) * dl_outer
|
| 516 |
+
dl_db2 = (2 * bond2.length - 2 * bond1.length * np.cos(gamma)) * dl_outer
|
| 517 |
+
stddev = np.sqrt(
|
| 518 |
+
(dl_dgamma * ba.stddev) ** 2
|
| 519 |
+
+ (dl_db1 * bond1.stddev) ** 2
|
| 520 |
+
+ (dl_db2 * bond2.stddev) ** 2
|
| 521 |
+
)
|
| 522 |
+
residue_virtual_bonds[resname].append(
|
| 523 |
+
Bond(ba.atom1_name, ba.atom3name, length, stddev)
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
return (residue_bonds, residue_virtual_bonds, residue_bond_angles)
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
# Between-residue bond lengths for general bonds (first element) and for Proline
|
| 530 |
+
# (second element).
|
| 531 |
+
between_res_bond_length_c_n = [1.329, 1.341]
|
| 532 |
+
between_res_bond_length_stddev_c_n = [0.014, 0.016]
|
| 533 |
+
|
| 534 |
+
# Between-residue cos_angles.
|
| 535 |
+
between_res_cos_angles_c_n_ca = [-0.5203, 0.0353] # degrees: 121.352 +- 2.315
|
| 536 |
+
between_res_cos_angles_ca_c_n = [-0.4473, 0.0311] # degrees: 116.568 +- 1.995
|
| 537 |
+
|
| 538 |
+
# This mapping is used when we need to store atom data in a format that requires
|
| 539 |
+
# fixed atom data size for every residue (e.g. a numpy array).
|
| 540 |
+
atom_types = [
|
| 541 |
+
"N",
|
| 542 |
+
"CA",
|
| 543 |
+
"C",
|
| 544 |
+
"CB",
|
| 545 |
+
"O",
|
| 546 |
+
"CG",
|
| 547 |
+
"CG1",
|
| 548 |
+
"CG2",
|
| 549 |
+
"OG",
|
| 550 |
+
"OG1",
|
| 551 |
+
"SG",
|
| 552 |
+
"CD",
|
| 553 |
+
"CD1",
|
| 554 |
+
"CD2",
|
| 555 |
+
"ND1",
|
| 556 |
+
"ND2",
|
| 557 |
+
"OD1",
|
| 558 |
+
"OD2",
|
| 559 |
+
"SD",
|
| 560 |
+
"CE",
|
| 561 |
+
"CE1",
|
| 562 |
+
"CE2",
|
| 563 |
+
"CE3",
|
| 564 |
+
"NE",
|
| 565 |
+
"NE1",
|
| 566 |
+
"NE2",
|
| 567 |
+
"OE1",
|
| 568 |
+
"OE2",
|
| 569 |
+
"CH2",
|
| 570 |
+
"NH1",
|
| 571 |
+
"NH2",
|
| 572 |
+
"OH",
|
| 573 |
+
"CZ",
|
| 574 |
+
"CZ2",
|
| 575 |
+
"CZ3",
|
| 576 |
+
"NZ",
|
| 577 |
+
"OXT",
|
| 578 |
+
]
|
| 579 |
+
atom_order = {atom_type: i for i, atom_type in enumerate(atom_types)}
|
| 580 |
+
atom_type_num = len(atom_types) # := 37.
|
| 581 |
+
|
| 582 |
+
# A compact atom encoding with 14 columns
|
| 583 |
+
# pylint: disable=line-too-long
|
| 584 |
+
# pylint: disable=bad-whitespace
|
| 585 |
+
restype_name_to_atom14_names = {
|
| 586 |
+
"ALA": ["N", "CA", "C", "O", "CB", "", "", "", "", "", "", "", "", ""],
|
| 587 |
+
"ARG": [
|
| 588 |
+
"N",
|
| 589 |
+
"CA",
|
| 590 |
+
"C",
|
| 591 |
+
"O",
|
| 592 |
+
"CB",
|
| 593 |
+
"CG",
|
| 594 |
+
"CD",
|
| 595 |
+
"NE",
|
| 596 |
+
"CZ",
|
| 597 |
+
"NH1",
|
| 598 |
+
"NH2",
|
| 599 |
+
"",
|
| 600 |
+
"",
|
| 601 |
+
"",
|
| 602 |
+
],
|
| 603 |
+
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2", "", "", "", "", "", ""],
|
| 604 |
+
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2", "", "", "", "", "", ""],
|
| 605 |
+
"CYS": ["N", "CA", "C", "O", "CB", "SG", "", "", "", "", "", "", "", ""],
|
| 606 |
+
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2", "", "", "", "", ""],
|
| 607 |
+
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2", "", "", "", "", ""],
|
| 608 |
+
"GLY": ["N", "CA", "C", "O", "", "", "", "", "", "", "", "", "", ""],
|
| 609 |
+
"HIS": [
|
| 610 |
+
"N",
|
| 611 |
+
"CA",
|
| 612 |
+
"C",
|
| 613 |
+
"O",
|
| 614 |
+
"CB",
|
| 615 |
+
"CG",
|
| 616 |
+
"ND1",
|
| 617 |
+
"CD2",
|
| 618 |
+
"CE1",
|
| 619 |
+
"NE2",
|
| 620 |
+
"",
|
| 621 |
+
"",
|
| 622 |
+
"",
|
| 623 |
+
"",
|
| 624 |
+
],
|
| 625 |
+
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1", "", "", "", "", "", ""],
|
| 626 |
+
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "", "", "", "", "", ""],
|
| 627 |
+
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ", "", "", "", "", ""],
|
| 628 |
+
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE", "", "", "", "", "", ""],
|
| 629 |
+
"PHE": [
|
| 630 |
+
"N",
|
| 631 |
+
"CA",
|
| 632 |
+
"C",
|
| 633 |
+
"O",
|
| 634 |
+
"CB",
|
| 635 |
+
"CG",
|
| 636 |
+
"CD1",
|
| 637 |
+
"CD2",
|
| 638 |
+
"CE1",
|
| 639 |
+
"CE2",
|
| 640 |
+
"CZ",
|
| 641 |
+
"",
|
| 642 |
+
"",
|
| 643 |
+
"",
|
| 644 |
+
],
|
| 645 |
+
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD", "", "", "", "", "", "", ""],
|
| 646 |
+
"SER": ["N", "CA", "C", "O", "CB", "OG", "", "", "", "", "", "", "", ""],
|
| 647 |
+
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2", "", "", "", "", "", "", ""],
|
| 648 |
+
"TRP": [
|
| 649 |
+
"N",
|
| 650 |
+
"CA",
|
| 651 |
+
"C",
|
| 652 |
+
"O",
|
| 653 |
+
"CB",
|
| 654 |
+
"CG",
|
| 655 |
+
"CD1",
|
| 656 |
+
"CD2",
|
| 657 |
+
"NE1",
|
| 658 |
+
"CE2",
|
| 659 |
+
"CE3",
|
| 660 |
+
"CZ2",
|
| 661 |
+
"CZ3",
|
| 662 |
+
"CH2",
|
| 663 |
+
],
|
| 664 |
+
"TYR": [
|
| 665 |
+
"N",
|
| 666 |
+
"CA",
|
| 667 |
+
"C",
|
| 668 |
+
"O",
|
| 669 |
+
"CB",
|
| 670 |
+
"CG",
|
| 671 |
+
"CD1",
|
| 672 |
+
"CD2",
|
| 673 |
+
"CE1",
|
| 674 |
+
"CE2",
|
| 675 |
+
"CZ",
|
| 676 |
+
"OH",
|
| 677 |
+
"",
|
| 678 |
+
"",
|
| 679 |
+
],
|
| 680 |
+
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "", "", "", "", "", "", ""],
|
| 681 |
+
"UNK": ["N", "CA", "C", "", "", "", "", "", "", "", "", "", "", ""],
|
| 682 |
+
}
|
| 683 |
+
# pylint: enable=line-too-long
|
| 684 |
+
# pylint: enable=bad-whitespace
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
# This is the standard residue order when coding AA type as a number.
|
| 688 |
+
# Reproduce it by taking 3-letter AA codes and sorting them alphabetically.
|
| 689 |
+
restypes = [
|
| 690 |
+
"A",
|
| 691 |
+
"R",
|
| 692 |
+
"N",
|
| 693 |
+
"D",
|
| 694 |
+
"C",
|
| 695 |
+
"Q",
|
| 696 |
+
"E",
|
| 697 |
+
"G",
|
| 698 |
+
"H",
|
| 699 |
+
"I",
|
| 700 |
+
"L",
|
| 701 |
+
"K",
|
| 702 |
+
"M",
|
| 703 |
+
"F",
|
| 704 |
+
"P",
|
| 705 |
+
"S",
|
| 706 |
+
"T",
|
| 707 |
+
"W",
|
| 708 |
+
"Y",
|
| 709 |
+
"V",
|
| 710 |
+
]
|
| 711 |
+
restype_order = {restype: i for i, restype in enumerate(restypes)}
|
| 712 |
+
restype_num = len(restypes) # := 20.
|
| 713 |
+
unk_restype_index = restype_num # Catch-all index for unknown restypes.
|
| 714 |
+
|
| 715 |
+
restypes_with_x = restypes + ["X"]
|
| 716 |
+
restype_order_with_x = {restype: i for i, restype in enumerate(restypes_with_x)}
|
| 717 |
+
|
| 718 |
+
bb_atoms = ["N", "CA", "C", "O"]
|
| 719 |
+
|
| 720 |
+
# Hydrophobicity by residue (positive values are hydrophobic). Derived from Black & Mould (1991), normalized by subtracting 0.5.
|
| 721 |
+
hydrophobicity = {
|
| 722 |
+
"ALA": 0.116,
|
| 723 |
+
"ARG": -0.5,
|
| 724 |
+
"ASN": -0.264,
|
| 725 |
+
"ASP": -0.472,
|
| 726 |
+
"CYS": 0.18,
|
| 727 |
+
"GLN": -0.249,
|
| 728 |
+
"GLU": -0.457,
|
| 729 |
+
"GLY": 0.001,
|
| 730 |
+
"HIS": -0.335,
|
| 731 |
+
"ILE": 0.443,
|
| 732 |
+
"LEU": 0.443,
|
| 733 |
+
"LYS": -0.217,
|
| 734 |
+
"MET": 0.238,
|
| 735 |
+
"PHE": 0.5,
|
| 736 |
+
"PRO": 0.211,
|
| 737 |
+
"SER": -0.141,
|
| 738 |
+
"THR": -0.05,
|
| 739 |
+
"TRP": 0.378,
|
| 740 |
+
"TYR": 0.38,
|
| 741 |
+
"VAL": 0.325,
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
# Side chain max accessible surface area in Ala-X-Ala tripeptide (from Chennamsetty et al. 2010).
|
| 745 |
+
side_chain_asa = {
|
| 746 |
+
"ALA": 64.7809,
|
| 747 |
+
"ARG": 210.02,
|
| 748 |
+
"ASN": 113.187,
|
| 749 |
+
"ASP": 110.209,
|
| 750 |
+
"CYS": 95.2439,
|
| 751 |
+
"GLN": 147.855,
|
| 752 |
+
"GLU": 143.924,
|
| 753 |
+
"GLY": 23.1338,
|
| 754 |
+
"HIS": 146.449,
|
| 755 |
+
"ILE": 151.242,
|
| 756 |
+
"LEU": 139.524,
|
| 757 |
+
"LYS": 177.366,
|
| 758 |
+
"MET": 164.674,
|
| 759 |
+
"PHE": 186.7,
|
| 760 |
+
"PRO": 111.533,
|
| 761 |
+
"SER": 81.2159,
|
| 762 |
+
"THR": 111.597,
|
| 763 |
+
"TRP": 229.619,
|
| 764 |
+
"TYR": 200.306,
|
| 765 |
+
"VAL": 124.237,
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
# Approximate Volumes of amino acids in cubic angstroms.
|
| 769 |
+
# https://www.imgt.org/IMGTeducation/Aide-memoire/_UK/aminoacids/abbreviation.html
|
| 770 |
+
amino_acid_volumes = {
|
| 771 |
+
"A": 88.6, # Alanine
|
| 772 |
+
"R": 173.4, # Arginine
|
| 773 |
+
"N": 114.1, # Asparagine
|
| 774 |
+
"D": 111.1, # Aspartic acid
|
| 775 |
+
"C": 108.5, # Cysteine
|
| 776 |
+
"Q": 143.8, # Glutamine
|
| 777 |
+
"E": 138.4, # Glutamic acid
|
| 778 |
+
"G": 60.1, # Glycine
|
| 779 |
+
"H": 153.2, # Histidine
|
| 780 |
+
"I": 166.7, # Isoleucine
|
| 781 |
+
"L": 166.7, # Leucine
|
| 782 |
+
"K": 168.6, # Lysine
|
| 783 |
+
"M": 162.9, # Methionine
|
| 784 |
+
"F": 189.9, # Phenylalanine
|
| 785 |
+
"P": 112.7, # Proline
|
| 786 |
+
"S": 89.0, # Serine
|
| 787 |
+
"T": 116.1, # Threonine
|
| 788 |
+
"W": 227.8, # Tryptophan
|
| 789 |
+
"Y": 193.6, # Tyrosine
|
| 790 |
+
"V": 140.0, # Valine
|
| 791 |
+
"X": 88.6, # Unknown, use Alanine as approximation
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
def sequence_to_onehot(
|
| 796 |
+
sequence: str, mapping: Mapping[str, int], map_unknown_to_x: bool = False
|
| 797 |
+
) -> np.ndarray:
|
| 798 |
+
"""Maps the given sequence into a one-hot encoded matrix.
|
| 799 |
+
|
| 800 |
+
Args:
|
| 801 |
+
sequence: An amino acid sequence.
|
| 802 |
+
mapping: A dictionary mapping amino acids to integers.
|
| 803 |
+
map_unknown_to_x: If True, any amino acid that is not in the mapping will be
|
| 804 |
+
mapped to the unknown amino acid 'X'. If the mapping doesn't contain
|
| 805 |
+
amino acid 'X', an error will be thrown. If False, any amino acid not in
|
| 806 |
+
the mapping will throw an error.
|
| 807 |
+
|
| 808 |
+
Returns:
|
| 809 |
+
A numpy array of shape (seq_len, num_unique_aas) with one-hot encoding of
|
| 810 |
+
the sequence.
|
| 811 |
+
|
| 812 |
+
Raises:
|
| 813 |
+
ValueError: If the mapping doesn't contain values from 0 to
|
| 814 |
+
num_unique_aas - 1 without any gaps.
|
| 815 |
+
"""
|
| 816 |
+
num_entries = max(mapping.values()) + 1
|
| 817 |
+
|
| 818 |
+
if sorted(set(mapping.values())) != list(range(num_entries)):
|
| 819 |
+
raise ValueError(
|
| 820 |
+
"The mapping must have values from 0 to num_unique_aas-1 "
|
| 821 |
+
"without any gaps. Got: %s" % sorted(mapping.values())
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
one_hot_arr = np.zeros((len(sequence), num_entries), dtype=np.int32)
|
| 825 |
+
|
| 826 |
+
for aa_index, aa_type in enumerate(sequence):
|
| 827 |
+
if map_unknown_to_x:
|
| 828 |
+
if aa_type.isalpha() and aa_type.isupper():
|
| 829 |
+
aa_id = mapping.get(aa_type, mapping["X"])
|
| 830 |
+
else:
|
| 831 |
+
raise ValueError(f"Invalid character in the sequence: {aa_type}")
|
| 832 |
+
else:
|
| 833 |
+
aa_id = mapping[aa_type]
|
| 834 |
+
one_hot_arr[aa_index, aa_id] = 1
|
| 835 |
+
|
| 836 |
+
return one_hot_arr
|
| 837 |
+
|
| 838 |
+
|
| 839 |
+
restype_1to3 = {
|
| 840 |
+
"A": "ALA",
|
| 841 |
+
"R": "ARG",
|
| 842 |
+
"N": "ASN",
|
| 843 |
+
"D": "ASP",
|
| 844 |
+
"C": "CYS",
|
| 845 |
+
"Q": "GLN",
|
| 846 |
+
"E": "GLU",
|
| 847 |
+
"G": "GLY",
|
| 848 |
+
"H": "HIS",
|
| 849 |
+
"I": "ILE",
|
| 850 |
+
"L": "LEU",
|
| 851 |
+
"K": "LYS",
|
| 852 |
+
"M": "MET",
|
| 853 |
+
"F": "PHE",
|
| 854 |
+
"P": "PRO",
|
| 855 |
+
"S": "SER",
|
| 856 |
+
"T": "THR",
|
| 857 |
+
"W": "TRP",
|
| 858 |
+
"Y": "TYR",
|
| 859 |
+
"V": "VAL",
|
| 860 |
+
"X": "UNK",
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
|
| 864 |
+
# NB: restype_3to1 differs from Bio.PDB.protein_letters_3to1 by being a simple
|
| 865 |
+
# 1-to-1 mapping of 3 letter names to one letter names. The latter contains
|
| 866 |
+
# many more, and less common, three letter names as keys and maps many of these
|
| 867 |
+
# to the same one letter name (including 'X' and 'U' which we don't use here).
|
| 868 |
+
restype_3to1 = {v: k for k, v in restype_1to3.items()}
|
| 869 |
+
|
| 870 |
+
# Define a restype name for all unknown residues.
|
| 871 |
+
unk_restype = "UNK"
|
| 872 |
+
|
| 873 |
+
resnames = [restype_1to3[r] for r in restypes] + [unk_restype]
|
| 874 |
+
resname_to_idx = {resname: i for i, resname in enumerate(resnames)}
|
| 875 |
+
|
| 876 |
+
hydrophobic_resnames = {"VAL", "ILE", "LEU", "PHE", "MET", "TRP"}
|
| 877 |
+
|
| 878 |
+
# The mapping here uses hhblits convention, so that B is mapped to D, J and O
|
| 879 |
+
# are mapped to X, U is mapped to C, and Z is mapped to E. Other than that the
|
| 880 |
+
# remaining 20 amino acids are kept in alphabetical order.
|
| 881 |
+
# There are 2 non-amino acid codes, X (representing any amino acid) and
|
| 882 |
+
# "-" representing a missing amino acid in an alignment. The id for these
|
| 883 |
+
# codes is put at the end (20 and 21) so that they can easily be ignored if
|
| 884 |
+
# desired.
|
| 885 |
+
HHBLITS_AA_TO_ID = {
|
| 886 |
+
"A": 0,
|
| 887 |
+
"B": 2,
|
| 888 |
+
"C": 1,
|
| 889 |
+
"D": 2,
|
| 890 |
+
"E": 3,
|
| 891 |
+
"F": 4,
|
| 892 |
+
"G": 5,
|
| 893 |
+
"H": 6,
|
| 894 |
+
"I": 7,
|
| 895 |
+
"J": 20,
|
| 896 |
+
"K": 8,
|
| 897 |
+
"L": 9,
|
| 898 |
+
"M": 10,
|
| 899 |
+
"N": 11,
|
| 900 |
+
"O": 20,
|
| 901 |
+
"P": 12,
|
| 902 |
+
"Q": 13,
|
| 903 |
+
"R": 14,
|
| 904 |
+
"S": 15,
|
| 905 |
+
"T": 16,
|
| 906 |
+
"U": 1,
|
| 907 |
+
"V": 17,
|
| 908 |
+
"W": 18,
|
| 909 |
+
"X": 20,
|
| 910 |
+
"Y": 19,
|
| 911 |
+
"Z": 3,
|
| 912 |
+
"-": 21,
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
# Partial inversion of HHBLITS_AA_TO_ID.
|
| 916 |
+
ID_TO_HHBLITS_AA = {
|
| 917 |
+
0: "A",
|
| 918 |
+
1: "C", # Also U.
|
| 919 |
+
2: "D", # Also B.
|
| 920 |
+
3: "E", # Also Z.
|
| 921 |
+
4: "F",
|
| 922 |
+
5: "G",
|
| 923 |
+
6: "H",
|
| 924 |
+
7: "I",
|
| 925 |
+
8: "K",
|
| 926 |
+
9: "L",
|
| 927 |
+
10: "M",
|
| 928 |
+
11: "N",
|
| 929 |
+
12: "P",
|
| 930 |
+
13: "Q",
|
| 931 |
+
14: "R",
|
| 932 |
+
15: "S",
|
| 933 |
+
16: "T",
|
| 934 |
+
17: "V",
|
| 935 |
+
18: "W",
|
| 936 |
+
19: "Y",
|
| 937 |
+
20: "X", # Includes J and O.
|
| 938 |
+
21: "-",
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
restypes_with_x_and_gap = restypes + ["X", "-"]
|
| 942 |
+
MAP_HHBLITS_AATYPE_TO_OUR_AATYPE = tuple(
|
| 943 |
+
restypes_with_x_and_gap.index(ID_TO_HHBLITS_AA[i])
|
| 944 |
+
for i in range(len(restypes_with_x_and_gap))
|
| 945 |
+
)
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
def _make_standard_atom_mask() -> np.ndarray:
|
| 949 |
+
"""Returns [num_res_types, num_atom_types] mask array."""
|
| 950 |
+
# +1 to account for unknown (all 0s).
|
| 951 |
+
mask = np.zeros([restype_num + 1, atom_type_num], dtype=np.int32)
|
| 952 |
+
for restype, restype_letter in enumerate(restypes):
|
| 953 |
+
restype_name = restype_1to3[restype_letter]
|
| 954 |
+
atom_names = residue_atoms[restype_name]
|
| 955 |
+
for atom_name in atom_names:
|
| 956 |
+
atom_type = atom_order[atom_name]
|
| 957 |
+
mask[restype, atom_type] = 1
|
| 958 |
+
return mask
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
STANDARD_ATOM_MASK = _make_standard_atom_mask()
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
# A one hot representation for the first and second atoms defining the axis
|
| 965 |
+
# of rotation for each chi-angle in each residue.
|
| 966 |
+
def chi_angle_atom(atom_index: int) -> np.ndarray:
|
| 967 |
+
"""Define chi-angle rigid groups via one-hot representations."""
|
| 968 |
+
chi_angles_index = {}
|
| 969 |
+
one_hots = []
|
| 970 |
+
|
| 971 |
+
for k, v in chi_angles_atoms.items():
|
| 972 |
+
indices = [atom_types.index(s[atom_index]) for s in v]
|
| 973 |
+
indices.extend([-1] * (4 - len(indices)))
|
| 974 |
+
chi_angles_index[k] = indices
|
| 975 |
+
|
| 976 |
+
for r in restypes:
|
| 977 |
+
res3 = restype_1to3[r]
|
| 978 |
+
one_hot = np.eye(atom_type_num)[chi_angles_index[res3]]
|
| 979 |
+
one_hots.append(one_hot)
|
| 980 |
+
|
| 981 |
+
one_hots.append(np.zeros([4, atom_type_num])) # Add zeros for residue `X`.
|
| 982 |
+
one_hot = np.stack(one_hots, axis=0)
|
| 983 |
+
one_hot = np.transpose(one_hot, [0, 2, 1])
|
| 984 |
+
|
| 985 |
+
return one_hot
|
| 986 |
+
|
| 987 |
+
|
| 988 |
+
chi_atom_1_one_hot = chi_angle_atom(1)
|
| 989 |
+
chi_atom_2_one_hot = chi_angle_atom(2)
|
| 990 |
+
|
| 991 |
+
# An array like chi_angles_atoms but using indices rather than names.
|
| 992 |
+
chi_angles_atom_indices = [chi_angles_atoms[restype_1to3[r]] for r in restypes]
|
| 993 |
+
# chi_angles_atom_indices = tree.map_structure(
|
| 994 |
+
# lambda atom_name: atom_order[atom_name], chi_angles_atom_indices
|
| 995 |
+
# )
|
| 996 |
+
chi_angles_atom_indices = np.array(
|
| 997 |
+
[
|
| 998 |
+
chi_atoms + ([[0, 0, 0, 0]] * (4 - len(chi_atoms)))
|
| 999 |
+
for chi_atoms in chi_angles_atom_indices
|
| 1000 |
+
]
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
# Mapping from (res_name, atom_name) pairs to the atom's chi group index
|
| 1004 |
+
# and atom index within that group.
|
| 1005 |
+
chi_groups_for_atom = collections.defaultdict(list)
|
| 1006 |
+
for res_name, chi_angle_atoms_for_res in chi_angles_atoms.items():
|
| 1007 |
+
for chi_group_i, chi_group in enumerate(chi_angle_atoms_for_res):
|
| 1008 |
+
for atom_i, atom in enumerate(chi_group):
|
| 1009 |
+
chi_groups_for_atom[(res_name, atom)].append((chi_group_i, atom_i))
|
| 1010 |
+
chi_groups_for_atom = dict(chi_groups_for_atom)
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
def _make_rigid_transformation_4x4(ex, ey, translation):
|
| 1014 |
+
"""Create a rigid 4x4 transformation matrix from two axes and transl."""
|
| 1015 |
+
# Normalize ex.
|
| 1016 |
+
ex_normalized = ex / np.linalg.norm(ex)
|
| 1017 |
+
|
| 1018 |
+
# make ey perpendicular to ex
|
| 1019 |
+
ey_normalized = ey - np.dot(ey, ex_normalized) * ex_normalized
|
| 1020 |
+
ey_normalized /= np.linalg.norm(ey_normalized)
|
| 1021 |
+
|
| 1022 |
+
# compute ez as cross product
|
| 1023 |
+
eznorm = np.cross(ex_normalized, ey_normalized)
|
| 1024 |
+
m = np.stack([ex_normalized, ey_normalized, eznorm, translation]).transpose()
|
| 1025 |
+
m = np.concatenate([m, [[0.0, 0.0, 0.0, 1.0]]], axis=0)
|
| 1026 |
+
return m
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
# create an array with (restype, atomtype) --> rigid_group_idx
|
| 1030 |
+
# and an array with (restype, atomtype, coord) for the atom positions
|
| 1031 |
+
# and compute affine transformation matrices (4,4) from one rigid group to the
|
| 1032 |
+
# previous group
|
| 1033 |
+
restype_atom37_to_rigid_group = np.zeros([21, 37], dtype=int)
|
| 1034 |
+
restype_atom37_mask = np.zeros([21, 37], dtype=np.float32)
|
| 1035 |
+
restype_atom37_rigid_group_positions = np.zeros([21, 37, 3], dtype=np.float32)
|
| 1036 |
+
restype_atom14_to_rigid_group = np.zeros([21, 14], dtype=int)
|
| 1037 |
+
restype_atom14_mask = np.zeros([21, 14], dtype=np.float32)
|
| 1038 |
+
restype_atom14_rigid_group_positions = np.zeros([21, 14, 3], dtype=np.float32)
|
| 1039 |
+
restype_rigid_group_default_frame = np.zeros([21, 8, 4, 4], dtype=np.float32)
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
+
def _make_rigid_group_constants():
|
| 1043 |
+
"""Fill the arrays above."""
|
| 1044 |
+
for restype, restype_letter in enumerate(restypes_with_x):
|
| 1045 |
+
resname = restype_1to3[restype_letter]
|
| 1046 |
+
for atomname, group_idx, atom_position in rigid_group_atom_positions[resname]:
|
| 1047 |
+
atomtype = atom_order[atomname]
|
| 1048 |
+
restype_atom37_to_rigid_group[restype, atomtype] = group_idx
|
| 1049 |
+
restype_atom37_mask[restype, atomtype] = 1
|
| 1050 |
+
restype_atom37_rigid_group_positions[restype, atomtype, :] = atom_position
|
| 1051 |
+
|
| 1052 |
+
atom14idx = restype_name_to_atom14_names[resname].index(atomname)
|
| 1053 |
+
restype_atom14_to_rigid_group[restype, atom14idx] = group_idx
|
| 1054 |
+
restype_atom14_mask[restype, atom14idx] = 1
|
| 1055 |
+
restype_atom14_rigid_group_positions[restype, atom14idx, :] = atom_position
|
| 1056 |
+
|
| 1057 |
+
for restype, restype_letter in enumerate(restypes_with_x):
|
| 1058 |
+
resname = restype_1to3[restype_letter]
|
| 1059 |
+
atom_positions = {
|
| 1060 |
+
name: np.array(pos) for name, _, pos in rigid_group_atom_positions[resname]
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
# backbone to backbone is the identity transform
|
| 1064 |
+
restype_rigid_group_default_frame[restype, 0, :, :] = np.eye(4)
|
| 1065 |
+
|
| 1066 |
+
# pre-omega-frame to backbone (currently dummy identity matrix)
|
| 1067 |
+
restype_rigid_group_default_frame[restype, 1, :, :] = np.eye(4)
|
| 1068 |
+
|
| 1069 |
+
# phi-frame to backbone
|
| 1070 |
+
mat = _make_rigid_transformation_4x4(
|
| 1071 |
+
ex=atom_positions["N"] - atom_positions["CA"],
|
| 1072 |
+
ey=np.array([1.0, 0.0, 0.0]),
|
| 1073 |
+
translation=atom_positions["N"],
|
| 1074 |
+
)
|
| 1075 |
+
restype_rigid_group_default_frame[restype, 2, :, :] = mat
|
| 1076 |
+
|
| 1077 |
+
# psi-frame to backbone
|
| 1078 |
+
mat = _make_rigid_transformation_4x4(
|
| 1079 |
+
ex=atom_positions["C"] - atom_positions["CA"],
|
| 1080 |
+
ey=atom_positions["CA"] - atom_positions["N"],
|
| 1081 |
+
translation=atom_positions["C"],
|
| 1082 |
+
)
|
| 1083 |
+
restype_rigid_group_default_frame[restype, 3, :, :] = mat
|
| 1084 |
+
|
| 1085 |
+
# chi1-frame to backbone
|
| 1086 |
+
if chi_angles_mask[restype][0]:
|
| 1087 |
+
base_atom_names = chi_angles_atoms[resname][0]
|
| 1088 |
+
base_atom_positions = [atom_positions[name] for name in base_atom_names]
|
| 1089 |
+
mat = _make_rigid_transformation_4x4(
|
| 1090 |
+
ex=base_atom_positions[2] - base_atom_positions[1],
|
| 1091 |
+
ey=base_atom_positions[0] - base_atom_positions[1],
|
| 1092 |
+
translation=base_atom_positions[2],
|
| 1093 |
+
)
|
| 1094 |
+
restype_rigid_group_default_frame[restype, 4, :, :] = mat
|
| 1095 |
+
|
| 1096 |
+
# chi2-frame to chi1-frame
|
| 1097 |
+
# chi3-frame to chi2-frame
|
| 1098 |
+
# chi4-frame to chi3-frame
|
| 1099 |
+
# luckily all rotation axes for the next frame start at (0,0,0) of the
|
| 1100 |
+
# previous frame
|
| 1101 |
+
for chi_idx in range(1, 4):
|
| 1102 |
+
if chi_angles_mask[restype][chi_idx]:
|
| 1103 |
+
axis_end_atom_name = chi_angles_atoms[resname][chi_idx][2]
|
| 1104 |
+
axis_end_atom_position = atom_positions[axis_end_atom_name]
|
| 1105 |
+
mat = _make_rigid_transformation_4x4(
|
| 1106 |
+
ex=axis_end_atom_position,
|
| 1107 |
+
ey=np.array([-1.0, 0.0, 0.0]),
|
| 1108 |
+
translation=axis_end_atom_position,
|
| 1109 |
+
)
|
| 1110 |
+
restype_rigid_group_default_frame[restype, 4 + chi_idx, :, :] = mat
|
| 1111 |
+
|
| 1112 |
+
|
| 1113 |
+
_make_rigid_group_constants()
|
| 1114 |
+
|
| 1115 |
+
|
| 1116 |
+
def make_atom14_dists_bounds(overlap_tolerance=1.5, bond_length_tolerance_factor=15.0):
|
| 1117 |
+
"""compute upper and lower bounds for bonds to assess violations."""
|
| 1118 |
+
restype_atom14_bond_lower_bound = np.zeros([21, 14, 14], np.float32)
|
| 1119 |
+
restype_atom14_bond_upper_bound = np.zeros([21, 14, 14], np.float32)
|
| 1120 |
+
restype_atom14_bond_stddev = np.zeros([21, 14, 14], np.float32)
|
| 1121 |
+
residue_bonds, residue_virtual_bonds, _ = load_stereo_chemical_props()
|
| 1122 |
+
for restype, restype_letter in enumerate(restypes):
|
| 1123 |
+
resname = restype_1to3[restype_letter]
|
| 1124 |
+
atom_list = restype_name_to_atom14_names[resname]
|
| 1125 |
+
|
| 1126 |
+
# create lower and upper bounds for clashes
|
| 1127 |
+
for atom1_idx, atom1_name in enumerate(atom_list):
|
| 1128 |
+
if not atom1_name:
|
| 1129 |
+
continue
|
| 1130 |
+
atom1_radius = van_der_waals_radius[atom1_name[0]]
|
| 1131 |
+
for atom2_idx, atom2_name in enumerate(atom_list):
|
| 1132 |
+
if (not atom2_name) or atom1_idx == atom2_idx:
|
| 1133 |
+
continue
|
| 1134 |
+
atom2_radius = van_der_waals_radius[atom2_name[0]]
|
| 1135 |
+
lower = atom1_radius + atom2_radius - overlap_tolerance
|
| 1136 |
+
upper = 1e10
|
| 1137 |
+
restype_atom14_bond_lower_bound[restype, atom1_idx, atom2_idx] = lower
|
| 1138 |
+
restype_atom14_bond_lower_bound[restype, atom2_idx, atom1_idx] = lower
|
| 1139 |
+
restype_atom14_bond_upper_bound[restype, atom1_idx, atom2_idx] = upper
|
| 1140 |
+
restype_atom14_bond_upper_bound[restype, atom2_idx, atom1_idx] = upper
|
| 1141 |
+
|
| 1142 |
+
# overwrite lower and upper bounds for bonds and angles
|
| 1143 |
+
for b in residue_bonds[resname] + residue_virtual_bonds[resname]:
|
| 1144 |
+
atom1_idx = atom_list.index(b.atom1_name)
|
| 1145 |
+
atom2_idx = atom_list.index(b.atom2_name)
|
| 1146 |
+
lower = b.length - bond_length_tolerance_factor * b.stddev
|
| 1147 |
+
upper = b.length + bond_length_tolerance_factor * b.stddev
|
| 1148 |
+
restype_atom14_bond_lower_bound[restype, atom1_idx, atom2_idx] = lower
|
| 1149 |
+
restype_atom14_bond_lower_bound[restype, atom2_idx, atom1_idx] = lower
|
| 1150 |
+
restype_atom14_bond_upper_bound[restype, atom1_idx, atom2_idx] = upper
|
| 1151 |
+
restype_atom14_bond_upper_bound[restype, atom2_idx, atom1_idx] = upper
|
| 1152 |
+
restype_atom14_bond_stddev[restype, atom1_idx, atom2_idx] = b.stddev
|
| 1153 |
+
restype_atom14_bond_stddev[restype, atom2_idx, atom1_idx] = b.stddev
|
| 1154 |
+
return {
|
| 1155 |
+
"lower_bound": restype_atom14_bond_lower_bound, # shape (21,14,14)
|
| 1156 |
+
"upper_bound": restype_atom14_bond_upper_bound, # shape (21,14,14)
|
| 1157 |
+
"stddev": restype_atom14_bond_stddev, # shape (21,14,14)
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
|
| 1161 |
+
restype_atom14_ambiguous_atoms = np.zeros((21, 14), dtype=np.float32)
|
| 1162 |
+
restype_atom14_ambiguous_atoms_swap_idx = np.tile(np.arange(14, dtype=int), (21, 1))
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
def _make_atom14_ambiguity_feats():
|
| 1166 |
+
for res, pairs in residue_atom_renaming_swaps.items():
|
| 1167 |
+
res_idx = restype_order[restype_3to1[res]]
|
| 1168 |
+
for atom1, atom2 in pairs.items():
|
| 1169 |
+
atom1_idx = restype_name_to_atom14_names[res].index(atom1)
|
| 1170 |
+
atom2_idx = restype_name_to_atom14_names[res].index(atom2)
|
| 1171 |
+
restype_atom14_ambiguous_atoms[res_idx, atom1_idx] = 1
|
| 1172 |
+
restype_atom14_ambiguous_atoms[res_idx, atom2_idx] = 1
|
| 1173 |
+
restype_atom14_ambiguous_atoms_swap_idx[res_idx, atom1_idx] = atom2_idx
|
| 1174 |
+
restype_atom14_ambiguous_atoms_swap_idx[res_idx, atom2_idx] = atom1_idx
|
| 1175 |
+
|
| 1176 |
+
|
| 1177 |
+
_make_atom14_ambiguity_feats()
|
| 1178 |
+
|
| 1179 |
+
|
| 1180 |
+
def aatype_to_str_sequence(aatype):
|
| 1181 |
+
return "".join([restypes_with_x[aatype[i]] for i in range(len(aatype))])
|
| 1182 |
+
|
| 1183 |
+
|
| 1184 |
+
# NOTE(thayes): These are computed based on the average CA->C and CA->N norm from rigid_group_atom_positions
|
| 1185 |
+
CA_TO_N_NORM = 1.4591
|
| 1186 |
+
CA_TO_C_NORM = 1.5252
|
| 1187 |
+
|
| 1188 |
+
|
| 1189 |
+
def _make_restype_atom37_to_atom14():
|
| 1190 |
+
"""Map from atom37 to atom14 per residue type."""
|
| 1191 |
+
restype_atom37_to_atom14 = [] # mapping (restype, atom37) --> atom14
|
| 1192 |
+
for rt in restypes:
|
| 1193 |
+
atom_names = restype_name_to_atom14_names[restype_1to3[rt]]
|
| 1194 |
+
atom_name_to_idx14 = {name: i for i, name in enumerate(atom_names)}
|
| 1195 |
+
restype_atom37_to_atom14.append(
|
| 1196 |
+
[
|
| 1197 |
+
(atom_name_to_idx14[name] if name in atom_name_to_idx14 else 0)
|
| 1198 |
+
for name in atom_types
|
| 1199 |
+
]
|
| 1200 |
+
)
|
| 1201 |
+
|
| 1202 |
+
restype_atom37_to_atom14.append([0] * 37)
|
| 1203 |
+
restype_atom37_to_atom14 = np.array(restype_atom37_to_atom14, dtype=np.int32)
|
| 1204 |
+
return restype_atom37_to_atom14
|
| 1205 |
+
|
| 1206 |
+
|
| 1207 |
+
def _make_restype_atom14_to_atom37():
|
| 1208 |
+
"""Map from atom14 to atom37 per residue type."""
|
| 1209 |
+
restype_atom14_to_atom37 = [] # mapping (restype, atom14) --> atom37
|
| 1210 |
+
for rt in restypes:
|
| 1211 |
+
atom_names = restype_name_to_atom14_names[restype_1to3[rt]]
|
| 1212 |
+
restype_atom14_to_atom37.append(
|
| 1213 |
+
[(atom_order[name] if name else 0) for name in atom_names]
|
| 1214 |
+
)
|
| 1215 |
+
# Add dummy mapping for restype 'UNK'
|
| 1216 |
+
restype_atom14_to_atom37.append([0] * 14)
|
| 1217 |
+
restype_atom14_to_atom37 = np.array(restype_atom14_to_atom37, dtype=np.int32)
|
| 1218 |
+
return restype_atom14_to_atom37
|
| 1219 |
+
|
| 1220 |
+
|
| 1221 |
+
RESTYPE_ATOM14_TO_ATOM37 = _make_restype_atom14_to_atom37()
|
| 1222 |
+
RESTYPE_ATOM37_TO_ATOM14 = _make_restype_atom37_to_atom14()
|
| 1223 |
+
CHAIN_BREAK_TOKEN = "|"
|
esmfold2_sequential_dataclass.py
CHANGED
|
@@ -1,157 +1,157 @@
|
|
| 1 |
-
from abc import ABC, abstractmethod
|
| 2 |
-
from dataclasses import dataclass, fields, replace
|
| 3 |
-
from typing import TypeVar
|
| 4 |
-
|
| 5 |
-
import numpy as np
|
| 6 |
-
|
| 7 |
-
from .esmfold2_misc import concat_objects, slice_any_object
|
| 8 |
-
|
| 9 |
-
T = TypeVar("T")
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@dataclass(frozen=True)
|
| 13 |
-
class SequentialDataclass(ABC):
|
| 14 |
-
"""
|
| 15 |
-
This is a builder on a dataclass that allows for automatic slicing and concatenation.
|
| 16 |
-
|
| 17 |
-
When representing multimodal data, we often have multiple datatypes which have sequence dimensions that are the same (e.g. the length of the protein).
|
| 18 |
-
|
| 19 |
-
When applying a transformation like a crop, we want to apply this to all tensors at the same time (e.g. crop the sequence, structure, and function).
|
| 20 |
-
|
| 21 |
-
We also have some fields that are not sequential (like an id, or data source), which we don't want to crop.
|
| 22 |
-
|
| 23 |
-
The SequentialDataclass abstracts this cropping away, allowing you to define dataclasses that implement `__len__`, `__getitem__` and `concat` automatically.
|
| 24 |
-
|
| 25 |
-
This is done through the `metadata` field, which can take 3 values:
|
| 26 |
-
`sequence` (bool): True or False, tells the dataclass whether this field is a sequential type. Default: False.
|
| 27 |
-
`sequence_dim` (int): Which dimension is the sequential dimension (e.g. for a list of inverse folded sequences, we want to index each sequence in the list, not the list itself). Default: 0.
|
| 28 |
-
`join_token` (Any): What token to use to join when concatenating elements. Default: None.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
Example:
|
| 32 |
-
|
| 33 |
-
@dataclass(frozen=True)
|
| 34 |
-
class Foo(SequentialDataclass):
|
| 35 |
-
id: str
|
| 36 |
-
sequence: str = field(metadata={"sequence": True, "join_token": "|"})
|
| 37 |
-
tensor: torch.Tensor = field(metadata={"sequence": True, "join_token": torch.nan})
|
| 38 |
-
|
| 39 |
-
def __len__(self):
|
| 40 |
-
# Must implement the __len__ method
|
| 41 |
-
return len(self.sequence)
|
| 42 |
-
|
| 43 |
-
>>> foo = Foo(id="foo", sequence="ABCDE", tensor=torch.randn(5))
|
| 44 |
-
Foo(id='foo', sequence='ABCDE', tensor=tensor([ 0.0252, -0.3335, -0.5143, 0.0251, -1.0717]))
|
| 45 |
-
|
| 46 |
-
>>> foo[1:4]
|
| 47 |
-
Foo(id='foo', sequence='BCD', tensor=tensor([-0.3335, -0.5143, 0.0251]))
|
| 48 |
-
|
| 49 |
-
>>> foo[np.arange(5) < 3]
|
| 50 |
-
Foo(id='foo', sequence='ABC', tensor=tensor([ 0.0252, -0.3335, -0.5143]))
|
| 51 |
-
|
| 52 |
-
>>> Foo.concat([foo[:2], foo[3:]])
|
| 53 |
-
Foo(id='foo', sequence='AB|DE', tensor=tensor([ 0.0252, -0.3335, nan, 0.0251, -1.0717]))
|
| 54 |
-
|
| 55 |
-
# Trying to create a type where the sequence lengths do not match raises an error
|
| 56 |
-
>>> foo = Foo(id="foo", sequence="ABCDE", tensor=torch.randn(6))
|
| 57 |
-
ValueError: Mismatch in sequence length for field: tensor. Expected 5, received 6
|
| 58 |
-
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
def __post_init__(self):
|
| 62 |
-
self._check_sequence_lengths_match()
|
| 63 |
-
|
| 64 |
-
@abstractmethod
|
| 65 |
-
def __len__(self):
|
| 66 |
-
raise NotImplementedError
|
| 67 |
-
|
| 68 |
-
def __getitem__(self, idx: int | list[int] | slice | np.ndarray):
|
| 69 |
-
updated_fields = {}
|
| 70 |
-
if isinstance(idx, int):
|
| 71 |
-
# make it so that things remain sequential
|
| 72 |
-
idx = [idx]
|
| 73 |
-
|
| 74 |
-
for fld in fields(self):
|
| 75 |
-
if fld.metadata.get("sequence", False):
|
| 76 |
-
# this is a sequence, should be the same length as all other sequences
|
| 77 |
-
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 78 |
-
value = getattr(self, fld.name)
|
| 79 |
-
if value is None:
|
| 80 |
-
continue
|
| 81 |
-
match sequence_dim:
|
| 82 |
-
case 0:
|
| 83 |
-
# sequence is first dimension
|
| 84 |
-
value = getattr(self, fld.name)
|
| 85 |
-
value = slice_any_object(value, idx)
|
| 86 |
-
updated_fields[fld.name] = value
|
| 87 |
-
case 1:
|
| 88 |
-
new_value = [slice_any_object(item, idx) for item in value]
|
| 89 |
-
updated_fields[fld.name] = value.__class__(new_value)
|
| 90 |
-
case _:
|
| 91 |
-
raise NotImplementedError(
|
| 92 |
-
"Arbitrary slicing for different sequence length fields is not implemented"
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
return replace(self, **updated_fields)
|
| 96 |
-
|
| 97 |
-
def _check_sequence_lengths_match(self):
|
| 98 |
-
"""Checks if sequence lengths of all "sequence" fields match."""
|
| 99 |
-
for fld in fields(self):
|
| 100 |
-
if fld.metadata.get("sequence", False) and fld.name != "complex":
|
| 101 |
-
# this is a sequence, should be the same length as all other sequences
|
| 102 |
-
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 103 |
-
value = getattr(self, fld.name)
|
| 104 |
-
if value is None:
|
| 105 |
-
continue
|
| 106 |
-
match sequence_dim:
|
| 107 |
-
case 0:
|
| 108 |
-
# sequence is first dimension
|
| 109 |
-
value = getattr(self, fld.name)
|
| 110 |
-
if len(value) != len(self):
|
| 111 |
-
raise ValueError(
|
| 112 |
-
f"Mismatch in sequence length for field: {fld.name}. Expected {len(self)}, received {len(value)}"
|
| 113 |
-
)
|
| 114 |
-
case 1:
|
| 115 |
-
for item in value:
|
| 116 |
-
if len(item) != len(self):
|
| 117 |
-
raise ValueError(
|
| 118 |
-
f"Mismatch in sequence length for field: {fld.name}. Expected {len(self)}, received {len(item)}"
|
| 119 |
-
)
|
| 120 |
-
case _:
|
| 121 |
-
raise NotImplementedError(
|
| 122 |
-
"Arbitrary matching for different sequence length fields is not implemented"
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
@classmethod
|
| 126 |
-
def concat(cls, items: list[T], **kwargs) -> T:
|
| 127 |
-
updated_fields = {}
|
| 128 |
-
for fld in fields(cls):
|
| 129 |
-
if fld.metadata.get("sequence", False):
|
| 130 |
-
# this is a sequence, should be the same length as all other sequences
|
| 131 |
-
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 132 |
-
join_value = fld.metadata.get("join_token", None)
|
| 133 |
-
if getattr(items[0], fld.name) is None:
|
| 134 |
-
continue
|
| 135 |
-
values = [getattr(item, fld.name) for item in items]
|
| 136 |
-
match sequence_dim:
|
| 137 |
-
case 0:
|
| 138 |
-
# sequence is first dimension
|
| 139 |
-
value = concat_objects(values, join_value)
|
| 140 |
-
updated_fields[fld.name] = value
|
| 141 |
-
case 1:
|
| 142 |
-
new_value = [
|
| 143 |
-
concat_objects(item, join_value) for item in zip(*values)
|
| 144 |
-
]
|
| 145 |
-
updated_fields[fld.name] = getattr(
|
| 146 |
-
items[0], fld.name
|
| 147 |
-
).__class__(new_value)
|
| 148 |
-
case _:
|
| 149 |
-
raise NotImplementedError(
|
| 150 |
-
"Arbitrary joining for different sequence length fields is not implemented"
|
| 151 |
-
)
|
| 152 |
-
updated_fields.update(kwargs)
|
| 153 |
-
|
| 154 |
-
return replace(
|
| 155 |
-
items[0], # type: ignore
|
| 156 |
-
**updated_fields,
|
| 157 |
-
)
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from dataclasses import dataclass, fields, replace
|
| 3 |
+
from typing import TypeVar
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from .esmfold2_misc import concat_objects, slice_any_object
|
| 8 |
+
|
| 9 |
+
T = TypeVar("T")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class SequentialDataclass(ABC):
|
| 14 |
+
"""
|
| 15 |
+
This is a builder on a dataclass that allows for automatic slicing and concatenation.
|
| 16 |
+
|
| 17 |
+
When representing multimodal data, we often have multiple datatypes which have sequence dimensions that are the same (e.g. the length of the protein).
|
| 18 |
+
|
| 19 |
+
When applying a transformation like a crop, we want to apply this to all tensors at the same time (e.g. crop the sequence, structure, and function).
|
| 20 |
+
|
| 21 |
+
We also have some fields that are not sequential (like an id, or data source), which we don't want to crop.
|
| 22 |
+
|
| 23 |
+
The SequentialDataclass abstracts this cropping away, allowing you to define dataclasses that implement `__len__`, `__getitem__` and `concat` automatically.
|
| 24 |
+
|
| 25 |
+
This is done through the `metadata` field, which can take 3 values:
|
| 26 |
+
`sequence` (bool): True or False, tells the dataclass whether this field is a sequential type. Default: False.
|
| 27 |
+
`sequence_dim` (int): Which dimension is the sequential dimension (e.g. for a list of inverse folded sequences, we want to index each sequence in the list, not the list itself). Default: 0.
|
| 28 |
+
`join_token` (Any): What token to use to join when concatenating elements. Default: None.
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
Example:
|
| 32 |
+
|
| 33 |
+
@dataclass(frozen=True)
|
| 34 |
+
class Foo(SequentialDataclass):
|
| 35 |
+
id: str
|
| 36 |
+
sequence: str = field(metadata={"sequence": True, "join_token": "|"})
|
| 37 |
+
tensor: torch.Tensor = field(metadata={"sequence": True, "join_token": torch.nan})
|
| 38 |
+
|
| 39 |
+
def __len__(self):
|
| 40 |
+
# Must implement the __len__ method
|
| 41 |
+
return len(self.sequence)
|
| 42 |
+
|
| 43 |
+
>>> foo = Foo(id="foo", sequence="ABCDE", tensor=torch.randn(5))
|
| 44 |
+
Foo(id='foo', sequence='ABCDE', tensor=tensor([ 0.0252, -0.3335, -0.5143, 0.0251, -1.0717]))
|
| 45 |
+
|
| 46 |
+
>>> foo[1:4]
|
| 47 |
+
Foo(id='foo', sequence='BCD', tensor=tensor([-0.3335, -0.5143, 0.0251]))
|
| 48 |
+
|
| 49 |
+
>>> foo[np.arange(5) < 3]
|
| 50 |
+
Foo(id='foo', sequence='ABC', tensor=tensor([ 0.0252, -0.3335, -0.5143]))
|
| 51 |
+
|
| 52 |
+
>>> Foo.concat([foo[:2], foo[3:]])
|
| 53 |
+
Foo(id='foo', sequence='AB|DE', tensor=tensor([ 0.0252, -0.3335, nan, 0.0251, -1.0717]))
|
| 54 |
+
|
| 55 |
+
# Trying to create a type where the sequence lengths do not match raises an error
|
| 56 |
+
>>> foo = Foo(id="foo", sequence="ABCDE", tensor=torch.randn(6))
|
| 57 |
+
ValueError: Mismatch in sequence length for field: tensor. Expected 5, received 6
|
| 58 |
+
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __post_init__(self):
|
| 62 |
+
self._check_sequence_lengths_match()
|
| 63 |
+
|
| 64 |
+
@abstractmethod
|
| 65 |
+
def __len__(self):
|
| 66 |
+
raise NotImplementedError
|
| 67 |
+
|
| 68 |
+
def __getitem__(self, idx: int | list[int] | slice | np.ndarray):
|
| 69 |
+
updated_fields = {}
|
| 70 |
+
if isinstance(idx, int):
|
| 71 |
+
# make it so that things remain sequential
|
| 72 |
+
idx = [idx]
|
| 73 |
+
|
| 74 |
+
for fld in fields(self):
|
| 75 |
+
if fld.metadata.get("sequence", False):
|
| 76 |
+
# this is a sequence, should be the same length as all other sequences
|
| 77 |
+
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 78 |
+
value = getattr(self, fld.name)
|
| 79 |
+
if value is None:
|
| 80 |
+
continue
|
| 81 |
+
match sequence_dim:
|
| 82 |
+
case 0:
|
| 83 |
+
# sequence is first dimension
|
| 84 |
+
value = getattr(self, fld.name)
|
| 85 |
+
value = slice_any_object(value, idx)
|
| 86 |
+
updated_fields[fld.name] = value
|
| 87 |
+
case 1:
|
| 88 |
+
new_value = [slice_any_object(item, idx) for item in value]
|
| 89 |
+
updated_fields[fld.name] = value.__class__(new_value)
|
| 90 |
+
case _:
|
| 91 |
+
raise NotImplementedError(
|
| 92 |
+
"Arbitrary slicing for different sequence length fields is not implemented"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return replace(self, **updated_fields)
|
| 96 |
+
|
| 97 |
+
def _check_sequence_lengths_match(self):
|
| 98 |
+
"""Checks if sequence lengths of all "sequence" fields match."""
|
| 99 |
+
for fld in fields(self):
|
| 100 |
+
if fld.metadata.get("sequence", False) and fld.name != "complex":
|
| 101 |
+
# this is a sequence, should be the same length as all other sequences
|
| 102 |
+
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 103 |
+
value = getattr(self, fld.name)
|
| 104 |
+
if value is None:
|
| 105 |
+
continue
|
| 106 |
+
match sequence_dim:
|
| 107 |
+
case 0:
|
| 108 |
+
# sequence is first dimension
|
| 109 |
+
value = getattr(self, fld.name)
|
| 110 |
+
if len(value) != len(self):
|
| 111 |
+
raise ValueError(
|
| 112 |
+
f"Mismatch in sequence length for field: {fld.name}. Expected {len(self)}, received {len(value)}"
|
| 113 |
+
)
|
| 114 |
+
case 1:
|
| 115 |
+
for item in value:
|
| 116 |
+
if len(item) != len(self):
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"Mismatch in sequence length for field: {fld.name}. Expected {len(self)}, received {len(item)}"
|
| 119 |
+
)
|
| 120 |
+
case _:
|
| 121 |
+
raise NotImplementedError(
|
| 122 |
+
"Arbitrary matching for different sequence length fields is not implemented"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
@classmethod
|
| 126 |
+
def concat(cls, items: list[T], **kwargs) -> T:
|
| 127 |
+
updated_fields = {}
|
| 128 |
+
for fld in fields(cls):
|
| 129 |
+
if fld.metadata.get("sequence", False):
|
| 130 |
+
# this is a sequence, should be the same length as all other sequences
|
| 131 |
+
sequence_dim = fld.metadata.get("sequence_dim", 0)
|
| 132 |
+
join_value = fld.metadata.get("join_token", None)
|
| 133 |
+
if getattr(items[0], fld.name) is None:
|
| 134 |
+
continue
|
| 135 |
+
values = [getattr(item, fld.name) for item in items]
|
| 136 |
+
match sequence_dim:
|
| 137 |
+
case 0:
|
| 138 |
+
# sequence is first dimension
|
| 139 |
+
value = concat_objects(values, join_value)
|
| 140 |
+
updated_fields[fld.name] = value
|
| 141 |
+
case 1:
|
| 142 |
+
new_value = [
|
| 143 |
+
concat_objects(item, join_value) for item in zip(*values)
|
| 144 |
+
]
|
| 145 |
+
updated_fields[fld.name] = getattr(
|
| 146 |
+
items[0], fld.name
|
| 147 |
+
).__class__(new_value)
|
| 148 |
+
case _:
|
| 149 |
+
raise NotImplementedError(
|
| 150 |
+
"Arbitrary joining for different sequence length fields is not implemented"
|
| 151 |
+
)
|
| 152 |
+
updated_fields.update(kwargs)
|
| 153 |
+
|
| 154 |
+
return replace(
|
| 155 |
+
items[0], # type: ignore
|
| 156 |
+
**updated_fields,
|
| 157 |
+
)
|
esmfold2_system.py
CHANGED
|
@@ -1,45 +1,45 @@
|
|
| 1 |
-
import io
|
| 2 |
-
import subprocess
|
| 3 |
-
import typing as T
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
|
| 6 |
-
PathLike = T.Union[str, Path]
|
| 7 |
-
PathOrBuffer = T.Union[PathLike, io.StringIO]
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def run_subprocess_with_errorcheck(
|
| 11 |
-
*popenargs,
|
| 12 |
-
capture_output: bool = False,
|
| 13 |
-
quiet: bool = False,
|
| 14 |
-
env: dict[str, str] | None = None,
|
| 15 |
-
shell: bool = False,
|
| 16 |
-
executable: str | None = None,
|
| 17 |
-
**kws,
|
| 18 |
-
) -> subprocess.CompletedProcess:
|
| 19 |
-
"""A command similar to subprocess.run, however the errormessage will
|
| 20 |
-
contain the stderr when using this function. This makes it significantly
|
| 21 |
-
easier to diagnose issues.
|
| 22 |
-
"""
|
| 23 |
-
try:
|
| 24 |
-
if capture_output:
|
| 25 |
-
stdout = subprocess.PIPE
|
| 26 |
-
elif quiet:
|
| 27 |
-
stdout = subprocess.DEVNULL
|
| 28 |
-
else:
|
| 29 |
-
stdout = None
|
| 30 |
-
|
| 31 |
-
p = subprocess.run(
|
| 32 |
-
*popenargs,
|
| 33 |
-
stderr=subprocess.PIPE,
|
| 34 |
-
stdout=stdout,
|
| 35 |
-
check=True,
|
| 36 |
-
env=env,
|
| 37 |
-
shell=shell,
|
| 38 |
-
executable=executable,
|
| 39 |
-
**kws,
|
| 40 |
-
)
|
| 41 |
-
except subprocess.CalledProcessError as e:
|
| 42 |
-
raise RuntimeError(
|
| 43 |
-
f"Command failed with errorcode {e.returncode}." f"\n\n{e.stderr.decode()}"
|
| 44 |
-
)
|
| 45 |
-
return p
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import subprocess
|
| 3 |
+
import typing as T
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
PathLike = T.Union[str, Path]
|
| 7 |
+
PathOrBuffer = T.Union[PathLike, io.StringIO]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def run_subprocess_with_errorcheck(
|
| 11 |
+
*popenargs,
|
| 12 |
+
capture_output: bool = False,
|
| 13 |
+
quiet: bool = False,
|
| 14 |
+
env: dict[str, str] | None = None,
|
| 15 |
+
shell: bool = False,
|
| 16 |
+
executable: str | None = None,
|
| 17 |
+
**kws,
|
| 18 |
+
) -> subprocess.CompletedProcess:
|
| 19 |
+
"""A command similar to subprocess.run, however the errormessage will
|
| 20 |
+
contain the stderr when using this function. This makes it significantly
|
| 21 |
+
easier to diagnose issues.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
if capture_output:
|
| 25 |
+
stdout = subprocess.PIPE
|
| 26 |
+
elif quiet:
|
| 27 |
+
stdout = subprocess.DEVNULL
|
| 28 |
+
else:
|
| 29 |
+
stdout = None
|
| 30 |
+
|
| 31 |
+
p = subprocess.run(
|
| 32 |
+
*popenargs,
|
| 33 |
+
stderr=subprocess.PIPE,
|
| 34 |
+
stdout=stdout,
|
| 35 |
+
check=True,
|
| 36 |
+
env=env,
|
| 37 |
+
shell=shell,
|
| 38 |
+
executable=executable,
|
| 39 |
+
**kws,
|
| 40 |
+
)
|
| 41 |
+
except subprocess.CalledProcessError as e:
|
| 42 |
+
raise RuntimeError(
|
| 43 |
+
f"Command failed with errorcode {e.returncode}." f"\n\n{e.stderr.decode()}"
|
| 44 |
+
)
|
| 45 |
+
return p
|
esmfold2_types.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
-
"""Re-exports of the canonical SPI dataclasses from input_builder.
|
| 2 |
-
|
| 3 |
-
This module exists so the HF processor and downstream code can import the
|
| 4 |
-
ESMFold2 input types from a single namespace without picking up internal-only
|
| 5 |
-
sibling utilities. The actual definitions live in
|
| 6 |
-
``esm.utils.structure.input_builder``.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from .esmfold2_msa import MSA
|
| 10 |
-
from .esmfold2_parsing import FastaEntry
|
| 11 |
-
from .esmfold2_input_builder import (
|
| 12 |
-
CovalentBond,
|
| 13 |
-
DistogramConditioning,
|
| 14 |
-
DNAInput,
|
| 15 |
-
LigandInput,
|
| 16 |
-
Modification,
|
| 17 |
-
ProteinInput,
|
| 18 |
-
RNAInput,
|
| 19 |
-
StructurePredictionInput,
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
__all__ = [
|
| 23 |
-
"FastaEntry",
|
| 24 |
-
"MSA",
|
| 25 |
-
"Modification",
|
| 26 |
-
"ProteinInput",
|
| 27 |
-
"RNAInput",
|
| 28 |
-
"DNAInput",
|
| 29 |
-
"LigandInput",
|
| 30 |
-
"DistogramConditioning",
|
| 31 |
-
"CovalentBond",
|
| 32 |
-
"StructurePredictionInput",
|
| 33 |
-
]
|
|
|
|
| 1 |
+
"""Re-exports of the canonical SPI dataclasses from input_builder.
|
| 2 |
+
|
| 3 |
+
This module exists so the HF processor and downstream code can import the
|
| 4 |
+
ESMFold2 input types from a single namespace without picking up internal-only
|
| 5 |
+
sibling utilities. The actual definitions live in
|
| 6 |
+
``esm.utils.structure.input_builder``.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from .esmfold2_msa import MSA
|
| 10 |
+
from .esmfold2_parsing import FastaEntry
|
| 11 |
+
from .esmfold2_input_builder import (
|
| 12 |
+
CovalentBond,
|
| 13 |
+
DistogramConditioning,
|
| 14 |
+
DNAInput,
|
| 15 |
+
LigandInput,
|
| 16 |
+
Modification,
|
| 17 |
+
ProteinInput,
|
| 18 |
+
RNAInput,
|
| 19 |
+
StructurePredictionInput,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
__all__ = [
|
| 23 |
+
"FastaEntry",
|
| 24 |
+
"MSA",
|
| 25 |
+
"Modification",
|
| 26 |
+
"ProteinInput",
|
| 27 |
+
"RNAInput",
|
| 28 |
+
"DNAInput",
|
| 29 |
+
"LigandInput",
|
| 30 |
+
"DistogramConditioning",
|
| 31 |
+
"CovalentBond",
|
| 32 |
+
"StructurePredictionInput",
|
| 33 |
+
]
|
esmfold2_utils_types.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import io
|
| 4 |
-
from dataclasses import dataclass
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
from typing import Union
|
| 7 |
-
|
| 8 |
-
from cloudpathlib import CloudPath
|
| 9 |
-
|
| 10 |
-
PathLike = Union[str, Path, CloudPath]
|
| 11 |
-
PathOrBuffer = Union[PathLike, io.StringIO]
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
@dataclass
|
| 15 |
-
class FunctionAnnotation:
|
| 16 |
-
"""Represents an annotation of a protein's function over a range of residues.
|
| 17 |
-
|
| 18 |
-
Fields:
|
| 19 |
-
label (str): An entry in either the function_tokens or residue_annotations tokenizer vocabs
|
| 20 |
-
start (int): Start index of this annotation. 1-indexed, inclusive.
|
| 21 |
-
end (int): End index of this annotation. 1-indexed, inclusive.
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
label: str
|
| 25 |
-
start: int
|
| 26 |
-
end: int
|
| 27 |
-
|
| 28 |
-
def to_tuple(self) -> tuple[str, int, int]:
|
| 29 |
-
return self.label, self.start, self.end
|
| 30 |
-
|
| 31 |
-
def __len__(self) -> int:
|
| 32 |
-
"""Length of the annotation."""
|
| 33 |
-
return self.end - self.start + 1
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Union
|
| 7 |
+
|
| 8 |
+
from cloudpathlib import CloudPath
|
| 9 |
+
|
| 10 |
+
PathLike = Union[str, Path, CloudPath]
|
| 11 |
+
PathOrBuffer = Union[PathLike, io.StringIO]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class FunctionAnnotation:
|
| 16 |
+
"""Represents an annotation of a protein's function over a range of residues.
|
| 17 |
+
|
| 18 |
+
Fields:
|
| 19 |
+
label (str): An entry in either the function_tokens or residue_annotations tokenizer vocabs
|
| 20 |
+
start (int): Start index of this annotation. 1-indexed, inclusive.
|
| 21 |
+
end (int): End index of this annotation. 1-indexed, inclusive.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
label: str
|
| 25 |
+
start: int
|
| 26 |
+
end: int
|
| 27 |
+
|
| 28 |
+
def to_tuple(self) -> tuple[str, int, int]:
|
| 29 |
+
return self.label, self.start, self.end
|
| 30 |
+
|
| 31 |
+
def __len__(self) -> int:
|
| 32 |
+
"""Length of the annotation."""
|
| 33 |
+
return self.end - self.start + 1
|
modeling_esmfold2.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_esmfold2_common.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_esmfold2_experimental.py
CHANGED
|
@@ -1,997 +1,1001 @@
|
|
| 1 |
-
"""FastPLMs ESMFold2 experimental architecture.
|
| 2 |
-
|
| 3 |
-
This module supports Biohub's experimental binder-design checkpoints. The
|
| 4 |
-
released ESMFold2 architecture in ``modeling_esmfold2.py`` intentionally
|
| 5 |
-
rejects those configs because the experimental trunk uses explicit pair-loop
|
| 6 |
-
re-injection and a different confidence/MSA stack.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import math
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
from typing import Any, cast
|
| 14 |
-
|
| 15 |
-
import torch
|
| 16 |
-
import torch.nn as nn
|
| 17 |
-
import torch.nn.functional as F
|
| 18 |
-
from torch import Tensor
|
| 19 |
-
from transformers.modeling_utils import PreTrainedModel
|
| 20 |
-
|
| 21 |
-
from .configuration_esmfold2 import ESMFold2Config
|
| 22 |
-
from .modeling_esmfold2 import (
|
| 23 |
-
|
| 24 |
-
_lm_precision_context,
|
| 25 |
-
)
|
| 26 |
-
from .modeling_esmfold2_common import (
|
| 27 |
-
CHAR_VOCAB_SIZE,
|
| 28 |
-
MAX_ATOMIC_NUMBER,
|
| 29 |
-
NUM_RES_TYPES,
|
| 30 |
-
DiffusionModule,
|
| 31 |
-
DiffusionStructureHead,
|
| 32 |
-
DiffusionTransformer,
|
| 33 |
-
FoldingTrunk,
|
| 34 |
-
InputsEmbedder,
|
| 35 |
-
LanguageModelShim,
|
| 36 |
-
MSAPairWeightedAveraging,
|
| 37 |
-
OuterProductMean,
|
| 38 |
-
PairUpdateBlock,
|
| 39 |
-
ResIdxAsymIdSymIdEntityIdEncoding,
|
| 40 |
-
RowAttentionPooling,
|
| 41 |
-
SwiGLUMLP,
|
| 42 |
-
TriangleMultiplicativeUpdate,
|
| 43 |
-
_categorical_mean,
|
| 44 |
-
_compute_intra_token_idx,
|
| 45 |
-
_seed_context,
|
| 46 |
-
compute_lm_hidden_states,
|
| 47 |
-
gather_rep_atom_coords,
|
| 48 |
-
gather_token_to_atom,
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
_EPS = 1e-5
|
| 52 |
-
_NONPOLYMER_ID = 3
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class ConfidenceHead(nn.Module):
|
| 56 |
-
"""Experimental confidence head predicting pLDDT, PAE, pTM, and ipTM."""
|
| 57 |
-
|
| 58 |
-
boundaries: Tensor
|
| 59 |
-
|
| 60 |
-
def __init__(self, config: ESMFold2Config) -> None:
|
| 61 |
-
super().__init__()
|
| 62 |
-
ch = config.confidence_head
|
| 63 |
-
d_single = config.d_single
|
| 64 |
-
d_pair = config.d_pair
|
| 65 |
-
d_inputs = config.inputs.d_inputs
|
| 66 |
-
|
| 67 |
-
boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1)
|
| 68 |
-
self.register_buffer("boundaries", boundaries)
|
| 69 |
-
self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair)
|
| 70 |
-
|
| 71 |
-
self.s_norm = nn.LayerNorm(d_single)
|
| 72 |
-
self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False)
|
| 73 |
-
self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False)
|
| 74 |
-
self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False)
|
| 75 |
-
self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 76 |
-
self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 77 |
-
self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False)
|
| 78 |
-
self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False)
|
| 79 |
-
self.s_inputs_norm = nn.LayerNorm(d_inputs)
|
| 80 |
-
self.z_norm = nn.LayerNorm(d_pair)
|
| 81 |
-
self.row_attention_pooling = RowAttentionPooling(
|
| 82 |
-
d_pair=d_pair, d_single=d_single
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
pf = ch.folding_trunk
|
| 86 |
-
self.folding_trunk = FoldingTrunk(
|
| 87 |
-
n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
self.plddt_ln = nn.LayerNorm(d_single)
|
| 91 |
-
max_atoms_per_token = 23
|
| 92 |
-
self.plddt_weight = nn.Parameter(
|
| 93 |
-
torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)
|
| 94 |
-
)
|
| 95 |
-
self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False)
|
| 96 |
-
|
| 97 |
-
def set_kernel_backend(self, backend: str | None) -> None:
|
| 98 |
-
self.folding_trunk.set_kernel_backend(backend)
|
| 99 |
-
|
| 100 |
-
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 101 |
-
self.folding_trunk.set_chunk_size(chunk_size)
|
| 102 |
-
|
| 103 |
-
@staticmethod
|
| 104 |
-
def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor:
|
| 105 |
-
if num_diffusion_samples == 1:
|
| 106 |
-
return x
|
| 107 |
-
return x.repeat_interleave(num_diffusion_samples, 0)
|
| 108 |
-
|
| 109 |
-
@staticmethod
|
| 110 |
-
def _flatten_sample_axis(x: Tensor) -> Tensor:
|
| 111 |
-
if x.ndim == 4:
|
| 112 |
-
b, mult, n, c = x.shape
|
| 113 |
-
return x.reshape(b * mult, n, c)
|
| 114 |
-
return x
|
| 115 |
-
|
| 116 |
-
def forward(
|
| 117 |
-
self,
|
| 118 |
-
s_inputs: Tensor,
|
| 119 |
-
z: Tensor,
|
| 120 |
-
x_pred: Tensor,
|
| 121 |
-
distogram_atom_idx: Tensor,
|
| 122 |
-
token_attention_mask: Tensor,
|
| 123 |
-
atom_to_token: Tensor,
|
| 124 |
-
atom_attention_mask: Tensor,
|
| 125 |
-
asym_id: Tensor,
|
| 126 |
-
mol_type: Tensor,
|
| 127 |
-
num_diffusion_samples: int = 1,
|
| 128 |
-
relative_position_encoding: Tensor | None = None,
|
| 129 |
-
token_bonds_encoding: Tensor | None = None,
|
| 130 |
-
) -> dict[str, Tensor]:
|
| 131 |
-
s_inputs_normed = self.s_inputs_norm(s_inputs)
|
| 132 |
-
z_base = self.z_norm(z)
|
| 133 |
-
if relative_position_encoding is not None:
|
| 134 |
-
z_base = z_base + relative_position_encoding
|
| 135 |
-
if token_bonds_encoding is not None:
|
| 136 |
-
z_base = z_base + token_bonds_encoding
|
| 137 |
-
z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2)
|
| 138 |
-
z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1)
|
| 139 |
-
z_base = z_base + self.s_to_z_prod_out(
|
| 140 |
-
self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :]
|
| 141 |
-
* self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :]
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
pair = self._repeat_batch(z_base, num_diffusion_samples)
|
| 145 |
-
x_pred_flat = self._flatten_sample_axis(x_pred)
|
| 146 |
-
atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples)
|
| 147 |
-
atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples)
|
| 148 |
-
rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long()
|
| 149 |
-
mask = self._repeat_batch(token_attention_mask, num_diffusion_samples)
|
| 150 |
-
batch_mult = pair.shape[0]
|
| 151 |
-
|
| 152 |
-
rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m)
|
| 153 |
-
rep_distances = torch.cdist(
|
| 154 |
-
rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist"
|
| 155 |
-
)
|
| 156 |
-
distogram_bins = (
|
| 157 |
-
(rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long()
|
| 158 |
-
)
|
| 159 |
-
pair = pair + self.dist_bin_pairwise_embed(distogram_bins)
|
| 160 |
-
|
| 161 |
-
pair_mask = mask[:, :, None].float() * mask[:, None, :].float()
|
| 162 |
-
pair = pair + self.folding_trunk(pair, pair_attention_mask=pair_mask)
|
| 163 |
-
single = self.row_attention_pooling(pair, mask)
|
| 164 |
-
|
| 165 |
-
atom_mask_f = atom_mask_m.float()
|
| 166 |
-
s_at_atoms = gather_token_to_atom(single, atom_to_token_m)
|
| 167 |
-
s_at_atoms = self.plddt_ln(s_at_atoms)
|
| 168 |
-
intra_idx = _compute_intra_token_idx(atom_to_token_m)
|
| 169 |
-
intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1)
|
| 170 |
-
plddt_weight = self.plddt_weight[intra_idx]
|
| 171 |
-
plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms, plddt_weight)
|
| 172 |
-
plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0)
|
| 173 |
-
|
| 174 |
-
length = single.shape[1]
|
| 175 |
-
plddt_sum = torch.zeros(
|
| 176 |
-
batch_mult, length, device=single.device, dtype=plddt_per_atom.dtype
|
| 177 |
-
)
|
| 178 |
-
atom_count = torch.zeros(
|
| 179 |
-
batch_mult, length, device=single.device, dtype=plddt_per_atom.dtype
|
| 180 |
-
)
|
| 181 |
-
atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype)
|
| 182 |
-
plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t)
|
| 183 |
-
atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t)
|
| 184 |
-
plddt = plddt_sum / atom_count.clamp(min=1e-6)
|
| 185 |
-
|
| 186 |
-
complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (
|
| 187 |
-
atom_mask_f.sum(dim=-1) + _EPS
|
| 188 |
-
)
|
| 189 |
-
|
| 190 |
-
expanded_type = self._repeat_batch(mol_type, num_diffusion_samples)
|
| 191 |
-
expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples)
|
| 192 |
-
is_ligand = (expanded_type == _NONPOLYMER_ID).float()
|
| 193 |
-
inter_chain = (
|
| 194 |
-
expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)
|
| 195 |
-
).float()
|
| 196 |
-
near_contact = (rep_distances < 8).float()
|
| 197 |
-
interface_per_token = (
|
| 198 |
-
near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)
|
| 199 |
-
).amax(dim=-1)
|
| 200 |
-
iplddt_weight = torch.where(
|
| 201 |
-
is_ligand.bool(),
|
| 202 |
-
torch.full_like(interface_per_token, 2.0),
|
| 203 |
-
interface_per_token,
|
| 204 |
-
)
|
| 205 |
-
iplddt_weight_atoms = gather_token_to_atom(
|
| 206 |
-
iplddt_weight.unsqueeze(-1), atom_to_token_m
|
| 207 |
-
).squeeze(-1)
|
| 208 |
-
atom_iplddt_w = atom_mask_f * iplddt_weight_atoms
|
| 209 |
-
complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (
|
| 210 |
-
atom_iplddt_w.sum(dim=-1) + _EPS
|
| 211 |
-
)
|
| 212 |
-
plddt_ca = plddt_per_atom.gather(1, rep_idx_m)
|
| 213 |
-
|
| 214 |
-
pae_logits = self.pae_head(pair)
|
| 215 |
-
pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach()
|
| 216 |
-
|
| 217 |
-
n_bins = pae_logits.shape[-1]
|
| 218 |
-
bin_width = 32.0 / n_bins
|
| 219 |
-
bin_centers = torch.arange(
|
| 220 |
-
0.5 * bin_width, 32.0, bin_width, device=pae_logits.device
|
| 221 |
-
)
|
| 222 |
-
mask_f = mask.float()
|
| 223 |
-
n_res = mask_f.sum(dim=-1, keepdim=True)
|
| 224 |
-
d0 = 1.24 * (n_res.clamp(min=19) - 15) ** (1 / 3) - 1.8
|
| 225 |
-
tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2)
|
| 226 |
-
pae_probs = F.softmax(pae_logits, dim=-1)
|
| 227 |
-
tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1)
|
| 228 |
-
|
| 229 |
-
pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2)
|
| 230 |
-
ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (
|
| 231 |
-
pair_mask_2d.sum(dim=-1) + _EPS
|
| 232 |
-
)
|
| 233 |
-
ptm = ptm_per_row.max(dim=-1).values
|
| 234 |
-
|
| 235 |
-
inter_chain_mask = (
|
| 236 |
-
expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)
|
| 237 |
-
).float() * pair_mask_2d
|
| 238 |
-
iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (
|
| 239 |
-
inter_chain_mask.sum(dim=-1) + _EPS
|
| 240 |
-
)
|
| 241 |
-
iptm = iptm_per_row.max(dim=-1).values
|
| 242 |
-
|
| 243 |
-
max_chain_id = int(expanded_asym.max().item()) if batch_mult > 0 else 0
|
| 244 |
-
n_chains = max_chain_id + 1
|
| 245 |
-
pair_chains_iptm = torch.zeros(
|
| 246 |
-
batch_mult,
|
| 247 |
-
n_chains,
|
| 248 |
-
n_chains,
|
| 249 |
-
device=tm_expected.device,
|
| 250 |
-
dtype=tm_expected.dtype,
|
| 251 |
-
)
|
| 252 |
-
for c1 in range(n_chains):
|
| 253 |
-
chain_c1 = (expanded_asym == c1).float() * mask_f
|
| 254 |
-
if chain_c1.sum() == 0:
|
| 255 |
-
continue
|
| 256 |
-
for c2 in range(n_chains):
|
| 257 |
-
chain_c2 = (expanded_asym == c2).float() * mask_f
|
| 258 |
-
pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2)
|
| 259 |
-
denom = pair_m.sum(dim=(-1, -2)) + _EPS
|
| 260 |
-
pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(
|
| 261 |
-
dim=(-1, -2)
|
| 262 |
-
) / denom
|
| 263 |
-
|
| 264 |
-
return {
|
| 265 |
-
"plddt_logits": plddt_logits,
|
| 266 |
-
"plddt": plddt.detach(),
|
| 267 |
-
"plddt_per_atom": plddt_per_atom.detach(),
|
| 268 |
-
"plddt_ca": plddt_ca.detach(),
|
| 269 |
-
"complex_plddt": complex_plddt.detach(),
|
| 270 |
-
"complex_iplddt": complex_iplddt.detach(),
|
| 271 |
-
"pae_logits": pae_logits,
|
| 272 |
-
"pae": pae,
|
| 273 |
-
"ptm": ptm.detach(),
|
| 274 |
-
"iptm": iptm.detach(),
|
| 275 |
-
"pair_chains_iptm": pair_chains_iptm.detach(),
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
class _TransitionFFN(nn.Module):
|
| 280 |
-
def __init__(self, d_model: int, expansion_ratio: int = 4) -> None:
|
| 281 |
-
super().__init__()
|
| 282 |
-
self.norm = nn.LayerNorm(d_model)
|
| 283 |
-
self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False)
|
| 284 |
-
|
| 285 |
-
def forward(self, x: Tensor) -> Tensor:
|
| 286 |
-
return self.ffn(self.norm(x))
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
class MSAEncoderBlock(nn.Module):
|
| 290 |
-
"""One experimental MSA update block."""
|
| 291 |
-
|
| 292 |
-
def __init__(
|
| 293 |
-
self,
|
| 294 |
-
d_msa: int,
|
| 295 |
-
d_pair: int,
|
| 296 |
-
d_hidden: int = 32,
|
| 297 |
-
n_heads_msa: int = 8,
|
| 298 |
-
msa_head_width: int = 32,
|
| 299 |
-
) -> None:
|
| 300 |
-
super().__init__()
|
| 301 |
-
self.outer_product_mean = OuterProductMean(
|
| 302 |
-
d_msa, d_hidden, d_pair, divide_outer_before_proj=True
|
| 303 |
-
)
|
| 304 |
-
self.msa_pair_weighted_averaging = MSAPairWeightedAveraging(
|
| 305 |
-
d_msa, d_pair, n_heads_msa, msa_head_width
|
| 306 |
-
)
|
| 307 |
-
self.msa_transition = _TransitionFFN(d_msa, expansion_ratio=4)
|
| 308 |
-
self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True)
|
| 309 |
-
self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False)
|
| 310 |
-
self.pair_transition = _TransitionFFN(d_pair, expansion_ratio=4)
|
| 311 |
-
|
| 312 |
-
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 313 |
-
self.outer_product_mean.set_chunk_size(chunk_size)
|
| 314 |
-
self.tri_mul_out.set_chunk_size(chunk_size)
|
| 315 |
-
self.tri_mul_in.set_chunk_size(chunk_size)
|
| 316 |
-
|
| 317 |
-
def forward(
|
| 318 |
-
self,
|
| 319 |
-
msa_repr: Tensor,
|
| 320 |
-
pair_repr: Tensor,
|
| 321 |
-
msa_attention_mask: Tensor,
|
| 322 |
-
pair_attention_mask: Tensor,
|
| 323 |
-
msa_track_mask: Tensor | None = None,
|
| 324 |
-
) -> tuple[Tensor, Tensor]:
|
| 325 |
-
mask4d = (
|
| 326 |
-
msa_track_mask[:, None, None, None].to(dtype=msa_repr.dtype)
|
| 327 |
-
if msa_track_mask is not None
|
| 328 |
-
else None
|
| 329 |
-
)
|
| 330 |
-
|
| 331 |
-
pair_mask4d = mask4d[:, :, :1] if mask4d is not None else None
|
| 332 |
-
|
| 333 |
-
msa_update = self.msa_pair_weighted_averaging(
|
| 334 |
-
msa_repr, pair_repr, pair_attention_mask
|
| 335 |
-
)
|
| 336 |
-
if mask4d is not None:
|
| 337 |
-
msa_update = msa_update * mask4d
|
| 338 |
-
msa_repr = msa_repr + msa_update
|
| 339 |
-
|
| 340 |
-
msa_transition = self.msa_transition(msa_repr)
|
| 341 |
-
if mask4d is not None:
|
| 342 |
-
msa_transition = msa_transition * mask4d
|
| 343 |
-
msa_repr = msa_repr + msa_transition
|
| 344 |
-
|
| 345 |
-
pair_opm = self.outer_product_mean(msa_repr, msa_attention_mask)
|
| 346 |
-
if pair_mask4d is not None:
|
| 347 |
-
pair_opm = pair_opm * pair_mask4d
|
| 348 |
-
pair_repr = pair_repr + pair_opm
|
| 349 |
-
|
| 350 |
-
pair_out = self.tri_mul_out(pair_repr, mask=pair_attention_mask)
|
| 351 |
-
if pair_mask4d is not None:
|
| 352 |
-
pair_out = pair_out * pair_mask4d
|
| 353 |
-
pair_repr = pair_repr + pair_out
|
| 354 |
-
|
| 355 |
-
pair_in = self.tri_mul_in(pair_repr, mask=pair_attention_mask)
|
| 356 |
-
if pair_mask4d is not None:
|
| 357 |
-
pair_in = pair_in * pair_mask4d
|
| 358 |
-
pair_repr = pair_repr + pair_in
|
| 359 |
-
|
| 360 |
-
pair_transition = self.pair_transition(pair_repr)
|
| 361 |
-
if pair_mask4d is not None:
|
| 362 |
-
pair_transition = pair_transition * pair_mask4d
|
| 363 |
-
pair_repr = pair_repr + pair_transition
|
| 364 |
-
return msa_repr, pair_repr
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
class MSAEncoder(nn.Module):
|
| 368 |
-
def __init__(
|
| 369 |
-
self,
|
| 370 |
-
d_msa: int,
|
| 371 |
-
d_pair: int,
|
| 372 |
-
d_inputs: int,
|
| 373 |
-
d_hidden: int = 32,
|
| 374 |
-
n_layers: int = 4,
|
| 375 |
-
n_heads_msa: int = 8,
|
| 376 |
-
msa_head_width: int = 32,
|
| 377 |
-
) -> None:
|
| 378 |
-
super().__init__()
|
| 379 |
-
self.embed = nn.Linear(35, d_msa, bias=False)
|
| 380 |
-
self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False)
|
| 381 |
-
self.blocks = nn.ModuleList(
|
| 382 |
-
[
|
| 383 |
-
MSAEncoderBlock(
|
| 384 |
-
d_msa=d_msa,
|
| 385 |
-
d_pair=d_pair,
|
| 386 |
-
d_hidden=d_hidden,
|
| 387 |
-
n_heads_msa=n_heads_msa,
|
| 388 |
-
msa_head_width=msa_head_width,
|
| 389 |
-
)
|
| 390 |
-
for _ in range(n_layers)
|
| 391 |
-
]
|
| 392 |
-
)
|
| 393 |
-
|
| 394 |
-
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 395 |
-
for block in self.blocks:
|
| 396 |
-
cast(MSAEncoderBlock, block).set_chunk_size(chunk_size)
|
| 397 |
-
|
| 398 |
-
def forward(
|
| 399 |
-
self,
|
| 400 |
-
x_pair: Tensor,
|
| 401 |
-
x_inputs: Tensor,
|
| 402 |
-
msa_oh: Tensor,
|
| 403 |
-
has_deletion: Tensor,
|
| 404 |
-
deletion_value: Tensor,
|
| 405 |
-
msa_attention_mask: Tensor,
|
| 406 |
-
) -> Tensor:
|
| 407 |
-
batch_size, _, depth = msa_attention_mask.shape
|
| 408 |
-
m_feat = torch.cat(
|
| 409 |
-
[msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)],
|
| 410 |
-
dim=-1,
|
| 411 |
-
)
|
| 412 |
-
m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2)
|
| 413 |
-
if depth > 1:
|
| 414 |
-
msa_track_mask = msa_attention_mask[:, :, 1:].any(dim=(1, 2))
|
| 415 |
-
else:
|
| 416 |
-
msa_track_mask = torch.zeros(
|
| 417 |
-
batch_size, dtype=torch.bool, device=x_pair.device
|
| 418 |
-
)
|
| 419 |
-
tok_mask = msa_attention_mask[:, :, 0]
|
| 420 |
-
pair_attention_mask = tok_mask.unsqueeze(2) * tok_mask.unsqueeze(1)
|
| 421 |
-
for block in self.blocks:
|
| 422 |
-
m, x_pair = cast(MSAEncoderBlock, block)(
|
| 423 |
-
m,
|
| 424 |
-
x_pair,
|
| 425 |
-
msa_attention_mask,
|
| 426 |
-
pair_attention_mask,
|
| 427 |
-
msa_track_mask,
|
| 428 |
-
)
|
| 429 |
-
return x_pair * msa_track_mask[:, None, None, None].to(dtype=x_pair.dtype)
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
class ESMFold2ExperimentalModel(PreTrainedModel):
|
| 433 |
-
"""Experimental ESMFold2 architecture used by binder-design checkpoints."""
|
| 434 |
-
|
| 435 |
-
config_class = ESMFold2Config
|
| 436 |
-
_keys_to_ignore_on_load_unexpected = [r"\._extra_state$"]
|
| 437 |
-
|
| 438 |
-
def __init__(self, config: ESMFold2Config) -> None:
|
| 439 |
-
super().__init__(config)
|
| 440 |
-
d_inputs = config.inputs.d_inputs
|
| 441 |
-
d_pair = config.d_pair
|
| 442 |
-
|
| 443 |
-
self.inputs_embedder = InputsEmbedder(config)
|
| 444 |
-
self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 445 |
-
self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 446 |
-
self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding(
|
| 447 |
-
n_relative_residx_bins=config.n_relative_residx_bins,
|
| 448 |
-
n_relative_chain_bins=config.n_relative_chain_bins,
|
| 449 |
-
d_pair=d_pair,
|
| 450 |
-
)
|
| 451 |
-
self.token_bonds = nn.Linear(1, d_pair, bias=False)
|
| 452 |
-
self.language_model = LanguageModelShim(
|
| 453 |
-
d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers
|
| 454 |
-
)
|
| 455 |
-
self._esmc: nn.Module | None = None
|
| 456 |
-
self._esmc_fp8 = False
|
| 457 |
-
self._esmfold2_input_builder: Any | None = None
|
| 458 |
-
|
| 459 |
-
pf = config.folding_trunk
|
| 460 |
-
self.folding_trunk = FoldingTrunk(
|
| 461 |
-
n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4
|
| 462 |
-
)
|
| 463 |
-
self.pair_loop_proj = nn.Sequential(
|
| 464 |
-
nn.LayerNorm(d_pair), nn.Linear(d_pair, d_pair, bias=False)
|
| 465 |
-
)
|
| 466 |
-
nn.init.zeros_(cast(nn.Linear, self.pair_loop_proj[1]).weight)
|
| 467 |
-
|
| 468 |
-
self.structure_head = DiffusionStructureHead(config)
|
| 469 |
-
self.distogram_head = nn.Linear(
|
| 470 |
-
d_pair, config.structure_head.distogram_bins, bias=True
|
| 471 |
-
)
|
| 472 |
-
self.confidence_head: ConfidenceHead | None = (
|
| 473 |
-
ConfidenceHead(config) if config.confidence_head.enabled else None
|
| 474 |
-
)
|
| 475 |
-
|
| 476 |
-
msa_cfg = config.msa_encoder
|
| 477 |
-
self.msa_encoder: MSAEncoder | None = None
|
| 478 |
-
if msa_cfg.enabled:
|
| 479 |
-
self.msa_encoder = MSAEncoder(
|
| 480 |
-
d_msa=msa_cfg.d_msa,
|
| 481 |
-
d_pair=d_pair,
|
| 482 |
-
d_inputs=d_inputs,
|
| 483 |
-
d_hidden=msa_cfg.d_hidden,
|
| 484 |
-
n_layers=msa_cfg.n_layers,
|
| 485 |
-
n_heads_msa=msa_cfg.n_heads_msa,
|
| 486 |
-
msa_head_width=msa_cfg.msa_head_width,
|
| 487 |
-
)
|
| 488 |
-
|
| 489 |
-
self.post_init()
|
| 490 |
-
|
| 491 |
-
@property
|
| 492 |
-
def device(self) -> torch.device:
|
| 493 |
-
return next(self.parameters()).device
|
| 494 |
-
|
| 495 |
-
def set_kernel_backend(self, backend: str | None) -> None:
|
| 496 |
-
self.folding_trunk.set_kernel_backend(backend)
|
| 497 |
-
if self.confidence_head is not None:
|
| 498 |
-
self.confidence_head.set_kernel_backend(backend)
|
| 499 |
-
self.structure_head.set_kernel_backend(backend)
|
| 500 |
-
|
| 501 |
-
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 502 |
-
self.folding_trunk.set_chunk_size(chunk_size)
|
| 503 |
-
if self.confidence_head is not None:
|
| 504 |
-
self.confidence_head.set_chunk_size(chunk_size)
|
| 505 |
-
if self.msa_encoder is not None:
|
| 506 |
-
self.msa_encoder.set_chunk_size(chunk_size)
|
| 507 |
-
|
| 508 |
-
def configure_lm_dropout(
|
| 509 |
-
self,
|
| 510 |
-
lm_dropout: float,
|
| 511 |
-
*,
|
| 512 |
-
force_lm_dropout_during_inference: bool = True,
|
| 513 |
-
) -> None:
|
| 514 |
-
self.config.lm_dropout = lm_dropout
|
| 515 |
-
self.config.force_lm_dropout_during_inference = (
|
| 516 |
-
force_lm_dropout_during_inference
|
| 517 |
-
)
|
| 518 |
-
|
| 519 |
-
def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None:
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
"fp32": torch.float32,
|
| 525 |
-
"fp8": torch.bfloat16,
|
| 526 |
-
}
|
| 527 |
if precision not in dtype_map:
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
.to(device=self.device, dtype=dtype_map[precision])
|
| 534 |
-
.eval()
|
| 535 |
-
)
|
| 536 |
-
for parameter in esmc.parameters():
|
| 537 |
-
parameter.requires_grad_(False)
|
| 538 |
-
if precision == "fp8":
|
| 539 |
-
with torch.no_grad():
|
| 540 |
-
_convert_te_modules_to_fp8_inplace(esmc)
|
| 541 |
-
self._esmc_fp8 = True
|
| 542 |
-
else:
|
| 543 |
-
self._esmc_fp8 = False
|
| 544 |
-
self._esmc = esmc
|
| 545 |
-
|
| 546 |
-
@classmethod
|
| 547 |
-
def from_pretrained(
|
| 548 |
-
cls,
|
| 549 |
-
pretrained_model_name_or_path,
|
| 550 |
-
*model_args,
|
| 551 |
-
load_esmc: bool = True,
|
| 552 |
-
**kwargs,
|
| 553 |
-
):
|
| 554 |
-
if "config" not in kwargs:
|
| 555 |
-
kwargs["config"] = ESMFold2Config.from_pretrained(
|
| 556 |
-
pretrained_model_name_or_path, **kwargs
|
| 557 |
-
)
|
| 558 |
-
esmc_precision = kwargs.pop("esmc_precision", "bf16")
|
| 559 |
-
model = super().from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
|
| 560 |
-
if load_esmc:
|
| 561 |
-
model.load_esmc(model.config.esmc_id, precision=esmc_precision)
|
| 562 |
-
return model
|
| 563 |
-
|
| 564 |
-
def apply_torch_compile(
|
| 565 |
-
self, mode: str = "fixed_seqlen", dynamic: bool | None = None
|
| 566 |
-
) -> None:
|
| 567 |
-
import torch._dynamo
|
| 568 |
-
|
| 569 |
-
torch._dynamo.config.cache_size_limit = 512
|
| 570 |
-
torch._dynamo.config.accumulated_cache_size_limit = 512
|
| 571 |
-
torch._dynamo.config.capture_scalar_outputs = True
|
| 572 |
-
|
| 573 |
-
if dynamic is None:
|
| 574 |
-
dynamic = mode == "dynamic_seqlen"
|
| 575 |
-
compile_kwargs: dict[str, bool] = {"dynamic": dynamic}
|
| 576 |
-
compile_targets = (
|
| 577 |
-
PairUpdateBlock,
|
| 578 |
-
DiffusionTransformer,
|
| 579 |
-
DiffusionModule,
|
| 580 |
-
MSAEncoderBlock,
|
| 581 |
-
)
|
| 582 |
-
|
| 583 |
-
def _maybe_compile(module: nn.Module) -> None:
|
| 584 |
-
if isinstance(module, compile_targets):
|
| 585 |
-
module.forward = torch.compile(module.forward, **compile_kwargs)
|
| 586 |
-
|
| 587 |
-
self.apply(_maybe_compile)
|
| 588 |
-
|
| 589 |
-
def _compute_lm_hidden_states(
|
| 590 |
-
self,
|
| 591 |
-
input_ids: Tensor,
|
| 592 |
-
asym_id: Tensor,
|
| 593 |
-
residue_index: Tensor,
|
| 594 |
-
mol_type: Tensor,
|
| 595 |
-
tok_mask: Tensor,
|
| 596 |
-
) -> Tensor:
|
| 597 |
-
assert self._esmc is not None
|
| 598 |
-
pad_to = 8 if self._esmc_fp8 else None
|
| 599 |
-
with _lm_precision_context(self._esmc_fp8):
|
| 600 |
-
return compute_lm_hidden_states(
|
| 601 |
-
self._esmc,
|
| 602 |
-
input_ids,
|
| 603 |
-
asym_id,
|
| 604 |
-
residue_index,
|
| 605 |
-
mol_type,
|
| 606 |
-
tok_mask,
|
| 607 |
-
pad_to_multiple=pad_to,
|
| 608 |
-
)
|
| 609 |
-
|
| 610 |
-
def forward(
|
| 611 |
-
self,
|
| 612 |
-
token_index: Tensor,
|
| 613 |
-
residue_index: Tensor,
|
| 614 |
-
asym_id: Tensor,
|
| 615 |
-
sym_id: Tensor,
|
| 616 |
-
entity_id: Tensor,
|
| 617 |
-
mol_type: Tensor,
|
| 618 |
-
res_type: Tensor,
|
| 619 |
-
token_bonds: Tensor,
|
| 620 |
-
token_attention_mask: Tensor,
|
| 621 |
-
ref_pos: Tensor,
|
| 622 |
-
ref_element: Tensor,
|
| 623 |
-
ref_charge: Tensor,
|
| 624 |
-
ref_atom_name_chars: Tensor,
|
| 625 |
-
ref_space_uid: Tensor,
|
| 626 |
-
atom_attention_mask: Tensor,
|
| 627 |
-
atom_to_token: Tensor,
|
| 628 |
-
distogram_atom_idx: Tensor,
|
| 629 |
-
deletion_mean: Tensor | None = None,
|
| 630 |
-
msa: Tensor | None = None,
|
| 631 |
-
has_deletion: Tensor | None = None,
|
| 632 |
-
deletion_value: Tensor | None = None,
|
| 633 |
-
msa_attention_mask: Tensor | None = None,
|
| 634 |
-
input_ids: Tensor | None = None,
|
| 635 |
-
lm_hidden_states: Tensor | None = None,
|
| 636 |
-
res_type_soft: Tensor | None = None,
|
| 637 |
-
num_loops: int | None = None,
|
| 638 |
-
num_diffusion_samples: int | None = None,
|
| 639 |
-
num_sampling_steps: int | None = None,
|
| 640 |
-
early_exit: bool = False,
|
| 641 |
-
seed: int | None = None,
|
| 642 |
-
calculate_confidence: bool = True,
|
| 643 |
-
provide_soft_sequence_to_msa_and_profile: bool = True,
|
| 644 |
-
noise_scale: float | None = None,
|
| 645 |
-
step_scale: float | None = None,
|
| 646 |
-
max_inference_sigma: int | None = None,
|
| 647 |
-
) -> dict[str, Tensor]:
|
| 648 |
-
del noise_scale, step_scale, max_inference_sigma
|
| 649 |
-
tok_mask = token_attention_mask
|
| 650 |
-
atm_mask = atom_attention_mask
|
| 651 |
-
n_loops = num_loops if num_loops is not None else self.config.num_loops
|
| 652 |
-
n_samples = (
|
| 653 |
-
num_diffusion_samples
|
| 654 |
-
if num_diffusion_samples is not None
|
| 655 |
-
else self.config.num_diffusion_samples
|
| 656 |
-
)
|
| 657 |
-
|
| 658 |
-
if res_type.dim() == 2:
|
| 659 |
-
res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float()
|
| 660 |
-
res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float()
|
| 661 |
-
else:
|
| 662 |
-
res_type_oh = res_type.float()
|
| 663 |
-
|
| 664 |
-
if msa is not None:
|
| 665 |
-
msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float()
|
| 666 |
-
if msa_attention_mask is not None:
|
| 667 |
-
mask_f = msa_attention_mask.float().unsqueeze(-1)
|
| 668 |
-
msa_oh_profile = msa_oh_profile * mask_f
|
| 669 |
-
valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1)
|
| 670 |
-
profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1)
|
| 671 |
-
else:
|
| 672 |
-
profile = msa_oh_profile.mean(dim=1)
|
| 673 |
-
else:
|
| 674 |
-
profile = res_type_oh
|
| 675 |
-
|
| 676 |
-
if res_type_soft is not None:
|
| 677 |
-
res_type_oh = res_type_soft.float()
|
| 678 |
-
if (
|
| 679 |
-
not self.config.disable_msa_features
|
| 680 |
-
and provide_soft_sequence_to_msa_and_profile
|
| 681 |
-
):
|
| 682 |
-
profile = res_type_oh
|
| 683 |
-
msa = res_type_oh.unsqueeze(1)
|
| 684 |
-
msa_attention_mask = tok_mask.unsqueeze(1)
|
| 685 |
-
|
| 686 |
-
if deletion_mean is None:
|
| 687 |
-
deletion_mean = torch.zeros(
|
| 688 |
-
res_type.shape[0], res_type.shape[1], device=res_type.device
|
| 689 |
-
)
|
| 690 |
-
if self.config.disable_msa_features:
|
| 691 |
-
profile = torch.zeros_like(profile)
|
| 692 |
-
deletion_mean = torch.zeros_like(deletion_mean)
|
| 693 |
-
|
| 694 |
-
ref_element_oh = F.one_hot(
|
| 695 |
-
ref_element.long(), num_classes=MAX_ATOMIC_NUMBER
|
| 696 |
-
).float()
|
| 697 |
-
ref_atom_name_chars_oh = F.one_hot(
|
| 698 |
-
ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE
|
| 699 |
-
).float()
|
| 700 |
-
atm_mask_f = atm_mask.float()
|
| 701 |
-
ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1)
|
| 702 |
-
ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(
|
| 703 |
-
-1
|
| 704 |
-
).unsqueeze(-1)
|
| 705 |
-
atom_to_token = atom_to_token * atm_mask.long()
|
| 706 |
-
|
| 707 |
-
use_amp = ref_pos.device.type == "cuda"
|
| 708 |
-
with (
|
| 709 |
-
torch.set_grad_enabled(res_type_soft is not None),
|
| 710 |
-
torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16),
|
| 711 |
-
):
|
| 712 |
-
x_inputs = self.inputs_embedder(
|
| 713 |
-
aatype=res_type_oh,
|
| 714 |
-
profile=profile.float(),
|
| 715 |
-
deletion_mean=deletion_mean.float(),
|
| 716 |
-
ref_pos=ref_pos,
|
| 717 |
-
atom_attention_mask=atm_mask,
|
| 718 |
-
ref_space_uid=ref_space_uid,
|
| 719 |
-
ref_charge=ref_charge,
|
| 720 |
-
ref_element=ref_element_oh,
|
| 721 |
-
ref_atom_name_chars=ref_atom_name_chars_oh,
|
| 722 |
-
atom_to_token=atom_to_token,
|
| 723 |
-
)
|
| 724 |
-
|
| 725 |
-
z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(
|
| 726 |
-
x_inputs
|
| 727 |
-
).unsqueeze(1)
|
| 728 |
-
relative_position_encoding = self.rel_pos(
|
| 729 |
-
residue_index=residue_index,
|
| 730 |
-
asym_id=asym_id,
|
| 731 |
-
sym_id=sym_id,
|
| 732 |
-
entity_id=entity_id,
|
| 733 |
-
token_index=token_index,
|
| 734 |
-
)
|
| 735 |
-
token_bonds_encoding = self.token_bonds(token_bonds.float())
|
| 736 |
-
z_init = z_init + relative_position_encoding + token_bonds_encoding
|
| 737 |
-
|
| 738 |
-
if (
|
| 739 |
-
lm_hidden_states is None
|
| 740 |
-
and input_ids is not None
|
| 741 |
-
and self._esmc is not None
|
| 742 |
-
):
|
| 743 |
-
lm_hidden_states = self._compute_lm_hidden_states(
|
| 744 |
-
input_ids, asym_id, residue_index, mol_type, tok_mask
|
| 745 |
-
)
|
| 746 |
-
if lm_hidden_states is not None:
|
| 747 |
-
lm_dropout = (
|
| 748 |
-
self.config.lm_dropout
|
| 749 |
-
if self.config.force_lm_dropout_during_inference or self.training
|
| 750 |
-
else 0.0
|
| 751 |
-
)
|
| 752 |
-
lm_z = self.language_model(
|
| 753 |
-
lm_hidden_states.detach(), lm_dropout=lm_dropout
|
| 754 |
-
)
|
| 755 |
-
z_init = z_init + lm_z.to(z_init.dtype)
|
| 756 |
-
|
| 757 |
-
msa_kwargs: dict[str, Tensor] | None = None
|
| 758 |
-
if self.msa_encoder is not None and msa is not None:
|
| 759 |
-
if msa.dim() == 4:
|
| 760 |
-
batch_msa, depth, length_msa, _ = msa.shape
|
| 761 |
-
msa_oh = msa.permute(0, 2, 1, 3).float()
|
| 762 |
-
else:
|
| 763 |
-
batch_msa, depth, length_msa = msa.shape
|
| 764 |
-
msa_oh = F.one_hot(
|
| 765 |
-
msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES
|
| 766 |
-
).float()
|
| 767 |
-
msa_attn = (
|
| 768 |
-
msa_attention_mask.permute(0, 2, 1).float()
|
| 769 |
-
if msa_attention_mask is not None
|
| 770 |
-
else tok_mask[:, :, None].expand(-1, -1, depth).float()
|
| 771 |
)
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastPLMs ESMFold2 experimental architecture.
|
| 2 |
+
|
| 3 |
+
This module supports Biohub's experimental binder-design checkpoints. The
|
| 4 |
+
released ESMFold2 architecture in ``modeling_esmfold2.py`` intentionally
|
| 5 |
+
rejects those configs because the experimental trunk uses explicit pair-loop
|
| 6 |
+
re-injection and a different confidence/MSA stack.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import math
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any, cast
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
from torch import Tensor
|
| 19 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 20 |
+
|
| 21 |
+
from .configuration_esmfold2 import ESMFold2Config
|
| 22 |
+
from .modeling_esmfold2 import (
|
| 23 |
+
_load_fastplms_esmplusplus_for_esmfold2,
|
| 24 |
+
_lm_precision_context,
|
| 25 |
+
)
|
| 26 |
+
from .modeling_esmfold2_common import (
|
| 27 |
+
CHAR_VOCAB_SIZE,
|
| 28 |
+
MAX_ATOMIC_NUMBER,
|
| 29 |
+
NUM_RES_TYPES,
|
| 30 |
+
DiffusionModule,
|
| 31 |
+
DiffusionStructureHead,
|
| 32 |
+
DiffusionTransformer,
|
| 33 |
+
FoldingTrunk,
|
| 34 |
+
InputsEmbedder,
|
| 35 |
+
LanguageModelShim,
|
| 36 |
+
MSAPairWeightedAveraging,
|
| 37 |
+
OuterProductMean,
|
| 38 |
+
PairUpdateBlock,
|
| 39 |
+
ResIdxAsymIdSymIdEntityIdEncoding,
|
| 40 |
+
RowAttentionPooling,
|
| 41 |
+
SwiGLUMLP,
|
| 42 |
+
TriangleMultiplicativeUpdate,
|
| 43 |
+
_categorical_mean,
|
| 44 |
+
_compute_intra_token_idx,
|
| 45 |
+
_seed_context,
|
| 46 |
+
compute_lm_hidden_states,
|
| 47 |
+
gather_rep_atom_coords,
|
| 48 |
+
gather_token_to_atom,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
_EPS = 1e-5
|
| 52 |
+
_NONPOLYMER_ID = 3
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class ConfidenceHead(nn.Module):
|
| 56 |
+
"""Experimental confidence head predicting pLDDT, PAE, pTM, and ipTM."""
|
| 57 |
+
|
| 58 |
+
boundaries: Tensor
|
| 59 |
+
|
| 60 |
+
def __init__(self, config: ESMFold2Config) -> None:
|
| 61 |
+
super().__init__()
|
| 62 |
+
ch = config.confidence_head
|
| 63 |
+
d_single = config.d_single
|
| 64 |
+
d_pair = config.d_pair
|
| 65 |
+
d_inputs = config.inputs.d_inputs
|
| 66 |
+
|
| 67 |
+
boundaries = torch.linspace(ch.min_dist, ch.max_dist, ch.distogram_bins - 1)
|
| 68 |
+
self.register_buffer("boundaries", boundaries)
|
| 69 |
+
self.dist_bin_pairwise_embed = nn.Embedding(ch.distogram_bins, d_pair)
|
| 70 |
+
|
| 71 |
+
self.s_norm = nn.LayerNorm(d_single)
|
| 72 |
+
self.s_inputs_to_single = nn.Linear(d_inputs, d_single, bias=False)
|
| 73 |
+
self.s_to_z = nn.Linear(d_inputs, d_pair, bias=False)
|
| 74 |
+
self.s_to_z_transpose = nn.Linear(d_inputs, d_pair, bias=False)
|
| 75 |
+
self.s_to_z_prod_in1 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 76 |
+
self.s_to_z_prod_in2 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 77 |
+
self.s_to_z_prod_out = nn.Linear(d_pair, d_pair, bias=False)
|
| 78 |
+
self.s_input_to_s = nn.Linear(d_inputs, d_single, bias=False)
|
| 79 |
+
self.s_inputs_norm = nn.LayerNorm(d_inputs)
|
| 80 |
+
self.z_norm = nn.LayerNorm(d_pair)
|
| 81 |
+
self.row_attention_pooling = RowAttentionPooling(
|
| 82 |
+
d_pair=d_pair, d_single=d_single
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
pf = ch.folding_trunk
|
| 86 |
+
self.folding_trunk = FoldingTrunk(
|
| 87 |
+
n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
self.plddt_ln = nn.LayerNorm(d_single)
|
| 91 |
+
max_atoms_per_token = 23
|
| 92 |
+
self.plddt_weight = nn.Parameter(
|
| 93 |
+
torch.zeros(max_atoms_per_token, d_single, ch.num_plddt_bins)
|
| 94 |
+
)
|
| 95 |
+
self.pae_head = nn.Linear(d_pair, ch.num_pae_bins, bias=False)
|
| 96 |
+
|
| 97 |
+
def set_kernel_backend(self, backend: str | None) -> None:
|
| 98 |
+
self.folding_trunk.set_kernel_backend(backend)
|
| 99 |
+
|
| 100 |
+
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 101 |
+
self.folding_trunk.set_chunk_size(chunk_size)
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def _repeat_batch(x: Tensor, num_diffusion_samples: int) -> Tensor:
|
| 105 |
+
if num_diffusion_samples == 1:
|
| 106 |
+
return x
|
| 107 |
+
return x.repeat_interleave(num_diffusion_samples, 0)
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
def _flatten_sample_axis(x: Tensor) -> Tensor:
|
| 111 |
+
if x.ndim == 4:
|
| 112 |
+
b, mult, n, c = x.shape
|
| 113 |
+
return x.reshape(b * mult, n, c)
|
| 114 |
+
return x
|
| 115 |
+
|
| 116 |
+
def forward(
|
| 117 |
+
self,
|
| 118 |
+
s_inputs: Tensor,
|
| 119 |
+
z: Tensor,
|
| 120 |
+
x_pred: Tensor,
|
| 121 |
+
distogram_atom_idx: Tensor,
|
| 122 |
+
token_attention_mask: Tensor,
|
| 123 |
+
atom_to_token: Tensor,
|
| 124 |
+
atom_attention_mask: Tensor,
|
| 125 |
+
asym_id: Tensor,
|
| 126 |
+
mol_type: Tensor,
|
| 127 |
+
num_diffusion_samples: int = 1,
|
| 128 |
+
relative_position_encoding: Tensor | None = None,
|
| 129 |
+
token_bonds_encoding: Tensor | None = None,
|
| 130 |
+
) -> dict[str, Tensor]:
|
| 131 |
+
s_inputs_normed = self.s_inputs_norm(s_inputs)
|
| 132 |
+
z_base = self.z_norm(z)
|
| 133 |
+
if relative_position_encoding is not None:
|
| 134 |
+
z_base = z_base + relative_position_encoding
|
| 135 |
+
if token_bonds_encoding is not None:
|
| 136 |
+
z_base = z_base + token_bonds_encoding
|
| 137 |
+
z_base = z_base + self.s_to_z(s_inputs_normed).unsqueeze(2)
|
| 138 |
+
z_base = z_base + self.s_to_z_transpose(s_inputs_normed).unsqueeze(1)
|
| 139 |
+
z_base = z_base + self.s_to_z_prod_out(
|
| 140 |
+
self.s_to_z_prod_in1(s_inputs_normed)[:, :, None, :]
|
| 141 |
+
* self.s_to_z_prod_in2(s_inputs_normed)[:, None, :, :]
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
pair = self._repeat_batch(z_base, num_diffusion_samples)
|
| 145 |
+
x_pred_flat = self._flatten_sample_axis(x_pred)
|
| 146 |
+
atom_to_token_m = self._repeat_batch(atom_to_token, num_diffusion_samples)
|
| 147 |
+
atom_mask_m = self._repeat_batch(atom_attention_mask, num_diffusion_samples)
|
| 148 |
+
rep_idx_m = self._repeat_batch(distogram_atom_idx, num_diffusion_samples).long()
|
| 149 |
+
mask = self._repeat_batch(token_attention_mask, num_diffusion_samples)
|
| 150 |
+
batch_mult = pair.shape[0]
|
| 151 |
+
|
| 152 |
+
rep_coords = gather_rep_atom_coords(x_pred_flat, rep_idx_m)
|
| 153 |
+
rep_distances = torch.cdist(
|
| 154 |
+
rep_coords, rep_coords, compute_mode="donot_use_mm_for_euclid_dist"
|
| 155 |
+
)
|
| 156 |
+
distogram_bins = (
|
| 157 |
+
(rep_distances.unsqueeze(-1) > self.boundaries).sum(dim=-1).long()
|
| 158 |
+
)
|
| 159 |
+
pair = pair + self.dist_bin_pairwise_embed(distogram_bins)
|
| 160 |
+
|
| 161 |
+
pair_mask = mask[:, :, None].float() * mask[:, None, :].float()
|
| 162 |
+
pair = pair + self.folding_trunk(pair, pair_attention_mask=pair_mask)
|
| 163 |
+
single = self.row_attention_pooling(pair, mask)
|
| 164 |
+
|
| 165 |
+
atom_mask_f = atom_mask_m.float()
|
| 166 |
+
s_at_atoms = gather_token_to_atom(single, atom_to_token_m)
|
| 167 |
+
s_at_atoms = self.plddt_ln(s_at_atoms)
|
| 168 |
+
intra_idx = _compute_intra_token_idx(atom_to_token_m)
|
| 169 |
+
intra_idx = intra_idx.clamp(max=self.plddt_weight.shape[0] - 1)
|
| 170 |
+
plddt_weight = self.plddt_weight[intra_idx]
|
| 171 |
+
plddt_logits = torch.einsum("...c,...cb->...b", s_at_atoms, plddt_weight)
|
| 172 |
+
plddt_per_atom = _categorical_mean(plddt_logits, start=0.0, end=1.0)
|
| 173 |
+
|
| 174 |
+
length = single.shape[1]
|
| 175 |
+
plddt_sum = torch.zeros(
|
| 176 |
+
batch_mult, length, device=single.device, dtype=plddt_per_atom.dtype
|
| 177 |
+
)
|
| 178 |
+
atom_count = torch.zeros(
|
| 179 |
+
batch_mult, length, device=single.device, dtype=plddt_per_atom.dtype
|
| 180 |
+
)
|
| 181 |
+
atom_mask_t = atom_mask_f.to(plddt_per_atom.dtype)
|
| 182 |
+
plddt_sum.scatter_add_(1, atom_to_token_m, plddt_per_atom * atom_mask_t)
|
| 183 |
+
atom_count.scatter_add_(1, atom_to_token_m, atom_mask_t)
|
| 184 |
+
plddt = plddt_sum / atom_count.clamp(min=1e-6)
|
| 185 |
+
|
| 186 |
+
complex_plddt = (plddt_per_atom * atom_mask_f).sum(dim=-1) / (
|
| 187 |
+
atom_mask_f.sum(dim=-1) + _EPS
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
expanded_type = self._repeat_batch(mol_type, num_diffusion_samples)
|
| 191 |
+
expanded_asym = self._repeat_batch(asym_id, num_diffusion_samples)
|
| 192 |
+
is_ligand = (expanded_type == _NONPOLYMER_ID).float()
|
| 193 |
+
inter_chain = (
|
| 194 |
+
expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)
|
| 195 |
+
).float()
|
| 196 |
+
near_contact = (rep_distances < 8).float()
|
| 197 |
+
interface_per_token = (
|
| 198 |
+
near_contact * inter_chain * (1.0 - is_ligand).unsqueeze(-1)
|
| 199 |
+
).amax(dim=-1)
|
| 200 |
+
iplddt_weight = torch.where(
|
| 201 |
+
is_ligand.bool(),
|
| 202 |
+
torch.full_like(interface_per_token, 2.0),
|
| 203 |
+
interface_per_token,
|
| 204 |
+
)
|
| 205 |
+
iplddt_weight_atoms = gather_token_to_atom(
|
| 206 |
+
iplddt_weight.unsqueeze(-1), atom_to_token_m
|
| 207 |
+
).squeeze(-1)
|
| 208 |
+
atom_iplddt_w = atom_mask_f * iplddt_weight_atoms
|
| 209 |
+
complex_iplddt = (plddt_per_atom * atom_iplddt_w).sum(dim=-1) / (
|
| 210 |
+
atom_iplddt_w.sum(dim=-1) + _EPS
|
| 211 |
+
)
|
| 212 |
+
plddt_ca = plddt_per_atom.gather(1, rep_idx_m)
|
| 213 |
+
|
| 214 |
+
pae_logits = self.pae_head(pair)
|
| 215 |
+
pae = _categorical_mean(pae_logits, start=0.0, end=32.0).detach()
|
| 216 |
+
|
| 217 |
+
n_bins = pae_logits.shape[-1]
|
| 218 |
+
bin_width = 32.0 / n_bins
|
| 219 |
+
bin_centers = torch.arange(
|
| 220 |
+
0.5 * bin_width, 32.0, bin_width, device=pae_logits.device
|
| 221 |
+
)
|
| 222 |
+
mask_f = mask.float()
|
| 223 |
+
n_res = mask_f.sum(dim=-1, keepdim=True)
|
| 224 |
+
d0 = 1.24 * (n_res.clamp(min=19) - 15) ** (1 / 3) - 1.8
|
| 225 |
+
tm_per_bin = 1 / (1 + (bin_centers / d0) ** 2)
|
| 226 |
+
pae_probs = F.softmax(pae_logits, dim=-1)
|
| 227 |
+
tm_expected = (pae_probs * tm_per_bin[:, None, None, :]).sum(dim=-1)
|
| 228 |
+
|
| 229 |
+
pair_mask_2d = mask_f.unsqueeze(-1) * mask_f.unsqueeze(-2)
|
| 230 |
+
ptm_per_row = (tm_expected * pair_mask_2d).sum(dim=-1) / (
|
| 231 |
+
pair_mask_2d.sum(dim=-1) + _EPS
|
| 232 |
+
)
|
| 233 |
+
ptm = ptm_per_row.max(dim=-1).values
|
| 234 |
+
|
| 235 |
+
inter_chain_mask = (
|
| 236 |
+
expanded_asym.unsqueeze(-1) != expanded_asym.unsqueeze(-2)
|
| 237 |
+
).float() * pair_mask_2d
|
| 238 |
+
iptm_per_row = (tm_expected * inter_chain_mask).sum(dim=-1) / (
|
| 239 |
+
inter_chain_mask.sum(dim=-1) + _EPS
|
| 240 |
+
)
|
| 241 |
+
iptm = iptm_per_row.max(dim=-1).values
|
| 242 |
+
|
| 243 |
+
max_chain_id = int(expanded_asym.max().item()) if batch_mult > 0 else 0
|
| 244 |
+
n_chains = max_chain_id + 1
|
| 245 |
+
pair_chains_iptm = torch.zeros(
|
| 246 |
+
batch_mult,
|
| 247 |
+
n_chains,
|
| 248 |
+
n_chains,
|
| 249 |
+
device=tm_expected.device,
|
| 250 |
+
dtype=tm_expected.dtype,
|
| 251 |
+
)
|
| 252 |
+
for c1 in range(n_chains):
|
| 253 |
+
chain_c1 = (expanded_asym == c1).float() * mask_f
|
| 254 |
+
if chain_c1.sum() == 0:
|
| 255 |
+
continue
|
| 256 |
+
for c2 in range(n_chains):
|
| 257 |
+
chain_c2 = (expanded_asym == c2).float() * mask_f
|
| 258 |
+
pair_m = chain_c1.unsqueeze(-1) * chain_c2.unsqueeze(-2)
|
| 259 |
+
denom = pair_m.sum(dim=(-1, -2)) + _EPS
|
| 260 |
+
pair_chains_iptm[:, c1, c2] = (tm_expected * pair_m).sum(
|
| 261 |
+
dim=(-1, -2)
|
| 262 |
+
) / denom
|
| 263 |
+
|
| 264 |
+
return {
|
| 265 |
+
"plddt_logits": plddt_logits,
|
| 266 |
+
"plddt": plddt.detach(),
|
| 267 |
+
"plddt_per_atom": plddt_per_atom.detach(),
|
| 268 |
+
"plddt_ca": plddt_ca.detach(),
|
| 269 |
+
"complex_plddt": complex_plddt.detach(),
|
| 270 |
+
"complex_iplddt": complex_iplddt.detach(),
|
| 271 |
+
"pae_logits": pae_logits,
|
| 272 |
+
"pae": pae,
|
| 273 |
+
"ptm": ptm.detach(),
|
| 274 |
+
"iptm": iptm.detach(),
|
| 275 |
+
"pair_chains_iptm": pair_chains_iptm.detach(),
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class _TransitionFFN(nn.Module):
|
| 280 |
+
def __init__(self, d_model: int, expansion_ratio: int = 4) -> None:
|
| 281 |
+
super().__init__()
|
| 282 |
+
self.norm = nn.LayerNorm(d_model)
|
| 283 |
+
self.ffn = SwiGLUMLP(d_model, expansion_ratio=expansion_ratio, bias=False)
|
| 284 |
+
|
| 285 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 286 |
+
return self.ffn(self.norm(x))
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class MSAEncoderBlock(nn.Module):
|
| 290 |
+
"""One experimental MSA update block."""
|
| 291 |
+
|
| 292 |
+
def __init__(
|
| 293 |
+
self,
|
| 294 |
+
d_msa: int,
|
| 295 |
+
d_pair: int,
|
| 296 |
+
d_hidden: int = 32,
|
| 297 |
+
n_heads_msa: int = 8,
|
| 298 |
+
msa_head_width: int = 32,
|
| 299 |
+
) -> None:
|
| 300 |
+
super().__init__()
|
| 301 |
+
self.outer_product_mean = OuterProductMean(
|
| 302 |
+
d_msa, d_hidden, d_pair, divide_outer_before_proj=True
|
| 303 |
+
)
|
| 304 |
+
self.msa_pair_weighted_averaging = MSAPairWeightedAveraging(
|
| 305 |
+
d_msa, d_pair, n_heads_msa, msa_head_width
|
| 306 |
+
)
|
| 307 |
+
self.msa_transition = _TransitionFFN(d_msa, expansion_ratio=4)
|
| 308 |
+
self.tri_mul_out = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=True)
|
| 309 |
+
self.tri_mul_in = TriangleMultiplicativeUpdate(dim=d_pair, _outgoing=False)
|
| 310 |
+
self.pair_transition = _TransitionFFN(d_pair, expansion_ratio=4)
|
| 311 |
+
|
| 312 |
+
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 313 |
+
self.outer_product_mean.set_chunk_size(chunk_size)
|
| 314 |
+
self.tri_mul_out.set_chunk_size(chunk_size)
|
| 315 |
+
self.tri_mul_in.set_chunk_size(chunk_size)
|
| 316 |
+
|
| 317 |
+
def forward(
|
| 318 |
+
self,
|
| 319 |
+
msa_repr: Tensor,
|
| 320 |
+
pair_repr: Tensor,
|
| 321 |
+
msa_attention_mask: Tensor,
|
| 322 |
+
pair_attention_mask: Tensor,
|
| 323 |
+
msa_track_mask: Tensor | None = None,
|
| 324 |
+
) -> tuple[Tensor, Tensor]:
|
| 325 |
+
mask4d = (
|
| 326 |
+
msa_track_mask[:, None, None, None].to(dtype=msa_repr.dtype)
|
| 327 |
+
if msa_track_mask is not None
|
| 328 |
+
else None
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
pair_mask4d = mask4d[:, :, :1] if mask4d is not None else None
|
| 332 |
+
|
| 333 |
+
msa_update = self.msa_pair_weighted_averaging(
|
| 334 |
+
msa_repr, pair_repr, pair_attention_mask
|
| 335 |
+
)
|
| 336 |
+
if mask4d is not None:
|
| 337 |
+
msa_update = msa_update * mask4d
|
| 338 |
+
msa_repr = msa_repr + msa_update
|
| 339 |
+
|
| 340 |
+
msa_transition = self.msa_transition(msa_repr)
|
| 341 |
+
if mask4d is not None:
|
| 342 |
+
msa_transition = msa_transition * mask4d
|
| 343 |
+
msa_repr = msa_repr + msa_transition
|
| 344 |
+
|
| 345 |
+
pair_opm = self.outer_product_mean(msa_repr, msa_attention_mask)
|
| 346 |
+
if pair_mask4d is not None:
|
| 347 |
+
pair_opm = pair_opm * pair_mask4d
|
| 348 |
+
pair_repr = pair_repr + pair_opm
|
| 349 |
+
|
| 350 |
+
pair_out = self.tri_mul_out(pair_repr, mask=pair_attention_mask)
|
| 351 |
+
if pair_mask4d is not None:
|
| 352 |
+
pair_out = pair_out * pair_mask4d
|
| 353 |
+
pair_repr = pair_repr + pair_out
|
| 354 |
+
|
| 355 |
+
pair_in = self.tri_mul_in(pair_repr, mask=pair_attention_mask)
|
| 356 |
+
if pair_mask4d is not None:
|
| 357 |
+
pair_in = pair_in * pair_mask4d
|
| 358 |
+
pair_repr = pair_repr + pair_in
|
| 359 |
+
|
| 360 |
+
pair_transition = self.pair_transition(pair_repr)
|
| 361 |
+
if pair_mask4d is not None:
|
| 362 |
+
pair_transition = pair_transition * pair_mask4d
|
| 363 |
+
pair_repr = pair_repr + pair_transition
|
| 364 |
+
return msa_repr, pair_repr
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
class MSAEncoder(nn.Module):
|
| 368 |
+
def __init__(
|
| 369 |
+
self,
|
| 370 |
+
d_msa: int,
|
| 371 |
+
d_pair: int,
|
| 372 |
+
d_inputs: int,
|
| 373 |
+
d_hidden: int = 32,
|
| 374 |
+
n_layers: int = 4,
|
| 375 |
+
n_heads_msa: int = 8,
|
| 376 |
+
msa_head_width: int = 32,
|
| 377 |
+
) -> None:
|
| 378 |
+
super().__init__()
|
| 379 |
+
self.embed = nn.Linear(35, d_msa, bias=False)
|
| 380 |
+
self.project_inputs = nn.Linear(d_inputs, d_msa, bias=False)
|
| 381 |
+
self.blocks = nn.ModuleList(
|
| 382 |
+
[
|
| 383 |
+
MSAEncoderBlock(
|
| 384 |
+
d_msa=d_msa,
|
| 385 |
+
d_pair=d_pair,
|
| 386 |
+
d_hidden=d_hidden,
|
| 387 |
+
n_heads_msa=n_heads_msa,
|
| 388 |
+
msa_head_width=msa_head_width,
|
| 389 |
+
)
|
| 390 |
+
for _ in range(n_layers)
|
| 391 |
+
]
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 395 |
+
for block in self.blocks:
|
| 396 |
+
cast(MSAEncoderBlock, block).set_chunk_size(chunk_size)
|
| 397 |
+
|
| 398 |
+
def forward(
|
| 399 |
+
self,
|
| 400 |
+
x_pair: Tensor,
|
| 401 |
+
x_inputs: Tensor,
|
| 402 |
+
msa_oh: Tensor,
|
| 403 |
+
has_deletion: Tensor,
|
| 404 |
+
deletion_value: Tensor,
|
| 405 |
+
msa_attention_mask: Tensor,
|
| 406 |
+
) -> Tensor:
|
| 407 |
+
batch_size, _, depth = msa_attention_mask.shape
|
| 408 |
+
m_feat = torch.cat(
|
| 409 |
+
[msa_oh, has_deletion.unsqueeze(-1), deletion_value.unsqueeze(-1)],
|
| 410 |
+
dim=-1,
|
| 411 |
+
)
|
| 412 |
+
m = self.embed(m_feat) + self.project_inputs(x_inputs).unsqueeze(2)
|
| 413 |
+
if depth > 1:
|
| 414 |
+
msa_track_mask = msa_attention_mask[:, :, 1:].any(dim=(1, 2))
|
| 415 |
+
else:
|
| 416 |
+
msa_track_mask = torch.zeros(
|
| 417 |
+
batch_size, dtype=torch.bool, device=x_pair.device
|
| 418 |
+
)
|
| 419 |
+
tok_mask = msa_attention_mask[:, :, 0]
|
| 420 |
+
pair_attention_mask = tok_mask.unsqueeze(2) * tok_mask.unsqueeze(1)
|
| 421 |
+
for block in self.blocks:
|
| 422 |
+
m, x_pair = cast(MSAEncoderBlock, block)(
|
| 423 |
+
m,
|
| 424 |
+
x_pair,
|
| 425 |
+
msa_attention_mask,
|
| 426 |
+
pair_attention_mask,
|
| 427 |
+
msa_track_mask,
|
| 428 |
+
)
|
| 429 |
+
return x_pair * msa_track_mask[:, None, None, None].to(dtype=x_pair.dtype)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
class ESMFold2ExperimentalModel(PreTrainedModel):
|
| 433 |
+
"""Experimental ESMFold2 architecture used by binder-design checkpoints."""
|
| 434 |
+
|
| 435 |
+
config_class = ESMFold2Config
|
| 436 |
+
_keys_to_ignore_on_load_unexpected = [r"\._extra_state$"]
|
| 437 |
+
|
| 438 |
+
def __init__(self, config: ESMFold2Config) -> None:
|
| 439 |
+
super().__init__(config)
|
| 440 |
+
d_inputs = config.inputs.d_inputs
|
| 441 |
+
d_pair = config.d_pair
|
| 442 |
+
|
| 443 |
+
self.inputs_embedder = InputsEmbedder(config)
|
| 444 |
+
self.z_init_1 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 445 |
+
self.z_init_2 = nn.Linear(d_inputs, d_pair, bias=False)
|
| 446 |
+
self.rel_pos = ResIdxAsymIdSymIdEntityIdEncoding(
|
| 447 |
+
n_relative_residx_bins=config.n_relative_residx_bins,
|
| 448 |
+
n_relative_chain_bins=config.n_relative_chain_bins,
|
| 449 |
+
d_pair=d_pair,
|
| 450 |
+
)
|
| 451 |
+
self.token_bonds = nn.Linear(1, d_pair, bias=False)
|
| 452 |
+
self.language_model = LanguageModelShim(
|
| 453 |
+
d_z=d_pair, d_model=config.lm_d_model, num_layers=config.lm_num_layers
|
| 454 |
+
)
|
| 455 |
+
self._esmc: nn.Module | None = None
|
| 456 |
+
self._esmc_fp8 = False
|
| 457 |
+
self._esmfold2_input_builder: Any | None = None
|
| 458 |
+
|
| 459 |
+
pf = config.folding_trunk
|
| 460 |
+
self.folding_trunk = FoldingTrunk(
|
| 461 |
+
n_layers=pf.n_layers, d_pair=d_pair, expansion_ratio=4
|
| 462 |
+
)
|
| 463 |
+
self.pair_loop_proj = nn.Sequential(
|
| 464 |
+
nn.LayerNorm(d_pair), nn.Linear(d_pair, d_pair, bias=False)
|
| 465 |
+
)
|
| 466 |
+
nn.init.zeros_(cast(nn.Linear, self.pair_loop_proj[1]).weight)
|
| 467 |
+
|
| 468 |
+
self.structure_head = DiffusionStructureHead(config)
|
| 469 |
+
self.distogram_head = nn.Linear(
|
| 470 |
+
d_pair, config.structure_head.distogram_bins, bias=True
|
| 471 |
+
)
|
| 472 |
+
self.confidence_head: ConfidenceHead | None = (
|
| 473 |
+
ConfidenceHead(config) if config.confidence_head.enabled else None
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
msa_cfg = config.msa_encoder
|
| 477 |
+
self.msa_encoder: MSAEncoder | None = None
|
| 478 |
+
if msa_cfg.enabled:
|
| 479 |
+
self.msa_encoder = MSAEncoder(
|
| 480 |
+
d_msa=msa_cfg.d_msa,
|
| 481 |
+
d_pair=d_pair,
|
| 482 |
+
d_inputs=d_inputs,
|
| 483 |
+
d_hidden=msa_cfg.d_hidden,
|
| 484 |
+
n_layers=msa_cfg.n_layers,
|
| 485 |
+
n_heads_msa=msa_cfg.n_heads_msa,
|
| 486 |
+
msa_head_width=msa_cfg.msa_head_width,
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
self.post_init()
|
| 490 |
+
|
| 491 |
+
@property
|
| 492 |
+
def device(self) -> torch.device:
|
| 493 |
+
return next(self.parameters()).device
|
| 494 |
+
|
| 495 |
+
def set_kernel_backend(self, backend: str | None) -> None:
|
| 496 |
+
self.folding_trunk.set_kernel_backend(backend)
|
| 497 |
+
if self.confidence_head is not None:
|
| 498 |
+
self.confidence_head.set_kernel_backend(backend)
|
| 499 |
+
self.structure_head.set_kernel_backend(backend)
|
| 500 |
+
|
| 501 |
+
def set_chunk_size(self, chunk_size: int | None) -> None:
|
| 502 |
+
self.folding_trunk.set_chunk_size(chunk_size)
|
| 503 |
+
if self.confidence_head is not None:
|
| 504 |
+
self.confidence_head.set_chunk_size(chunk_size)
|
| 505 |
+
if self.msa_encoder is not None:
|
| 506 |
+
self.msa_encoder.set_chunk_size(chunk_size)
|
| 507 |
+
|
| 508 |
+
def configure_lm_dropout(
|
| 509 |
+
self,
|
| 510 |
+
lm_dropout: float,
|
| 511 |
+
*,
|
| 512 |
+
force_lm_dropout_during_inference: bool = True,
|
| 513 |
+
) -> None:
|
| 514 |
+
self.config.lm_dropout = lm_dropout
|
| 515 |
+
self.config.force_lm_dropout_during_inference = (
|
| 516 |
+
force_lm_dropout_during_inference
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
def load_esmc(self, esmc_model_path: str, precision: str = "bf16") -> None:
|
| 520 |
+
dtype_map = {
|
| 521 |
+
"bf16": torch.bfloat16,
|
| 522 |
+
"fp32": torch.float32,
|
| 523 |
+
}
|
|
|
|
|
|
|
|
|
|
| 524 |
if precision not in dtype_map:
|
| 525 |
+
if precision == "fp8":
|
| 526 |
+
raise RuntimeError(
|
| 527 |
+
"esmc_precision='fp8' is supported only by the standard "
|
| 528 |
+
"released ESMFold2 model. The experimental binder-design "
|
| 529 |
+
"model keeps the FastPLMs ESM++ backbone in bf16 or fp32."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
)
|
| 531 |
+
raise ValueError(f"precision must be one of {list(dtype_map)}, got {precision!r}")
|
| 532 |
+
esmc = _load_fastplms_esmplusplus_for_esmfold2(
|
| 533 |
+
esmc_model_path=esmc_model_path,
|
| 534 |
+
attn_backend=self.config.esmc_attn_backend,
|
| 535 |
+
device=self.device,
|
| 536 |
+
dtype=dtype_map[precision],
|
| 537 |
+
)
|
| 538 |
+
assert esmc.config.hidden_size == self.config.lm_d_model, (
|
| 539 |
+
f"ESMFold2 expected lm_d_model={self.config.lm_d_model}, "
|
| 540 |
+
f"but loaded ESM++ hidden_size={esmc.config.hidden_size}."
|
| 541 |
+
)
|
| 542 |
+
assert esmc.config.num_hidden_layers == self.config.lm_num_layers, (
|
| 543 |
+
f"ESMFold2 expected lm_num_layers={self.config.lm_num_layers}, "
|
| 544 |
+
f"but loaded ESM++ num_hidden_layers={esmc.config.num_hidden_layers}."
|
| 545 |
+
)
|
| 546 |
+
for parameter in esmc.parameters():
|
| 547 |
+
parameter.requires_grad_(False)
|
| 548 |
+
self._esmc_fp8 = False
|
| 549 |
+
self._esmc = esmc
|
| 550 |
+
|
| 551 |
+
@classmethod
|
| 552 |
+
def from_pretrained(
|
| 553 |
+
cls,
|
| 554 |
+
pretrained_model_name_or_path,
|
| 555 |
+
*model_args,
|
| 556 |
+
load_esmc: bool = True,
|
| 557 |
+
**kwargs,
|
| 558 |
+
):
|
| 559 |
+
if "config" not in kwargs:
|
| 560 |
+
kwargs["config"] = ESMFold2Config.from_pretrained(
|
| 561 |
+
pretrained_model_name_or_path, **kwargs
|
| 562 |
+
)
|
| 563 |
+
esmc_precision = kwargs.pop("esmc_precision", "bf16")
|
| 564 |
+
model = super().from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
|
| 565 |
+
if load_esmc:
|
| 566 |
+
model.load_esmc(model.config.esmc_id, precision=esmc_precision)
|
| 567 |
+
return model
|
| 568 |
+
|
| 569 |
+
def apply_torch_compile(
|
| 570 |
+
self, mode: str = "fixed_seqlen", dynamic: bool | None = None
|
| 571 |
+
) -> None:
|
| 572 |
+
import torch._dynamo
|
| 573 |
+
|
| 574 |
+
torch._dynamo.config.cache_size_limit = 512
|
| 575 |
+
torch._dynamo.config.accumulated_cache_size_limit = 512
|
| 576 |
+
torch._dynamo.config.capture_scalar_outputs = True
|
| 577 |
+
|
| 578 |
+
if dynamic is None:
|
| 579 |
+
dynamic = mode == "dynamic_seqlen"
|
| 580 |
+
compile_kwargs: dict[str, bool] = {"dynamic": dynamic}
|
| 581 |
+
compile_targets = (
|
| 582 |
+
PairUpdateBlock,
|
| 583 |
+
DiffusionTransformer,
|
| 584 |
+
DiffusionModule,
|
| 585 |
+
MSAEncoderBlock,
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
def _maybe_compile(module: nn.Module) -> None:
|
| 589 |
+
if isinstance(module, compile_targets):
|
| 590 |
+
module.forward = torch.compile(module.forward, **compile_kwargs)
|
| 591 |
+
|
| 592 |
+
self.apply(_maybe_compile)
|
| 593 |
+
|
| 594 |
+
def _compute_lm_hidden_states(
|
| 595 |
+
self,
|
| 596 |
+
input_ids: Tensor,
|
| 597 |
+
asym_id: Tensor,
|
| 598 |
+
residue_index: Tensor,
|
| 599 |
+
mol_type: Tensor,
|
| 600 |
+
tok_mask: Tensor,
|
| 601 |
+
) -> Tensor:
|
| 602 |
+
assert self._esmc is not None
|
| 603 |
+
pad_to = 8 if self._esmc_fp8 else None
|
| 604 |
+
with _lm_precision_context(self._esmc_fp8):
|
| 605 |
+
return compute_lm_hidden_states(
|
| 606 |
+
self._esmc,
|
| 607 |
+
input_ids,
|
| 608 |
+
asym_id,
|
| 609 |
+
residue_index,
|
| 610 |
+
mol_type,
|
| 611 |
+
tok_mask,
|
| 612 |
+
pad_to_multiple=pad_to,
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
def forward(
|
| 616 |
+
self,
|
| 617 |
+
token_index: Tensor,
|
| 618 |
+
residue_index: Tensor,
|
| 619 |
+
asym_id: Tensor,
|
| 620 |
+
sym_id: Tensor,
|
| 621 |
+
entity_id: Tensor,
|
| 622 |
+
mol_type: Tensor,
|
| 623 |
+
res_type: Tensor,
|
| 624 |
+
token_bonds: Tensor,
|
| 625 |
+
token_attention_mask: Tensor,
|
| 626 |
+
ref_pos: Tensor,
|
| 627 |
+
ref_element: Tensor,
|
| 628 |
+
ref_charge: Tensor,
|
| 629 |
+
ref_atom_name_chars: Tensor,
|
| 630 |
+
ref_space_uid: Tensor,
|
| 631 |
+
atom_attention_mask: Tensor,
|
| 632 |
+
atom_to_token: Tensor,
|
| 633 |
+
distogram_atom_idx: Tensor,
|
| 634 |
+
deletion_mean: Tensor | None = None,
|
| 635 |
+
msa: Tensor | None = None,
|
| 636 |
+
has_deletion: Tensor | None = None,
|
| 637 |
+
deletion_value: Tensor | None = None,
|
| 638 |
+
msa_attention_mask: Tensor | None = None,
|
| 639 |
+
input_ids: Tensor | None = None,
|
| 640 |
+
lm_hidden_states: Tensor | None = None,
|
| 641 |
+
res_type_soft: Tensor | None = None,
|
| 642 |
+
num_loops: int | None = None,
|
| 643 |
+
num_diffusion_samples: int | None = None,
|
| 644 |
+
num_sampling_steps: int | None = None,
|
| 645 |
+
early_exit: bool = False,
|
| 646 |
+
seed: int | None = None,
|
| 647 |
+
calculate_confidence: bool = True,
|
| 648 |
+
provide_soft_sequence_to_msa_and_profile: bool = True,
|
| 649 |
+
noise_scale: float | None = None,
|
| 650 |
+
step_scale: float | None = None,
|
| 651 |
+
max_inference_sigma: int | None = None,
|
| 652 |
+
) -> dict[str, Tensor]:
|
| 653 |
+
del noise_scale, step_scale, max_inference_sigma
|
| 654 |
+
tok_mask = token_attention_mask
|
| 655 |
+
atm_mask = atom_attention_mask
|
| 656 |
+
n_loops = num_loops if num_loops is not None else self.config.num_loops
|
| 657 |
+
n_samples = (
|
| 658 |
+
num_diffusion_samples
|
| 659 |
+
if num_diffusion_samples is not None
|
| 660 |
+
else self.config.num_diffusion_samples
|
| 661 |
+
)
|
| 662 |
+
|
| 663 |
+
if res_type.dim() == 2:
|
| 664 |
+
res_type_oh = F.one_hot(res_type.long(), num_classes=NUM_RES_TYPES).float()
|
| 665 |
+
res_type_oh = res_type_oh * tok_mask.unsqueeze(-1).float()
|
| 666 |
+
else:
|
| 667 |
+
res_type_oh = res_type.float()
|
| 668 |
+
|
| 669 |
+
if msa is not None:
|
| 670 |
+
msa_oh_profile = F.one_hot(msa.long(), num_classes=NUM_RES_TYPES).float()
|
| 671 |
+
if msa_attention_mask is not None:
|
| 672 |
+
mask_f = msa_attention_mask.float().unsqueeze(-1)
|
| 673 |
+
msa_oh_profile = msa_oh_profile * mask_f
|
| 674 |
+
valid_seq_count = msa_attention_mask.float().sum(dim=1).clamp(min=1)
|
| 675 |
+
profile = msa_oh_profile.sum(dim=1) / valid_seq_count.unsqueeze(-1)
|
| 676 |
+
else:
|
| 677 |
+
profile = msa_oh_profile.mean(dim=1)
|
| 678 |
+
else:
|
| 679 |
+
profile = res_type_oh
|
| 680 |
+
|
| 681 |
+
if res_type_soft is not None:
|
| 682 |
+
res_type_oh = res_type_soft.float()
|
| 683 |
+
if (
|
| 684 |
+
not self.config.disable_msa_features
|
| 685 |
+
and provide_soft_sequence_to_msa_and_profile
|
| 686 |
+
):
|
| 687 |
+
profile = res_type_oh
|
| 688 |
+
msa = res_type_oh.unsqueeze(1)
|
| 689 |
+
msa_attention_mask = tok_mask.unsqueeze(1)
|
| 690 |
+
|
| 691 |
+
if deletion_mean is None:
|
| 692 |
+
deletion_mean = torch.zeros(
|
| 693 |
+
res_type.shape[0], res_type.shape[1], device=res_type.device
|
| 694 |
+
)
|
| 695 |
+
if self.config.disable_msa_features:
|
| 696 |
+
profile = torch.zeros_like(profile)
|
| 697 |
+
deletion_mean = torch.zeros_like(deletion_mean)
|
| 698 |
+
|
| 699 |
+
ref_element_oh = F.one_hot(
|
| 700 |
+
ref_element.long(), num_classes=MAX_ATOMIC_NUMBER
|
| 701 |
+
).float()
|
| 702 |
+
ref_atom_name_chars_oh = F.one_hot(
|
| 703 |
+
ref_atom_name_chars.long(), num_classes=CHAR_VOCAB_SIZE
|
| 704 |
+
).float()
|
| 705 |
+
atm_mask_f = atm_mask.float()
|
| 706 |
+
ref_element_oh = ref_element_oh * atm_mask_f.unsqueeze(-1)
|
| 707 |
+
ref_atom_name_chars_oh = ref_atom_name_chars_oh * atm_mask_f.unsqueeze(
|
| 708 |
+
-1
|
| 709 |
+
).unsqueeze(-1)
|
| 710 |
+
atom_to_token = atom_to_token * atm_mask.long()
|
| 711 |
+
|
| 712 |
+
use_amp = ref_pos.device.type == "cuda"
|
| 713 |
+
with (
|
| 714 |
+
torch.set_grad_enabled(res_type_soft is not None),
|
| 715 |
+
torch.amp.autocast("cuda", enabled=use_amp, dtype=torch.bfloat16),
|
| 716 |
+
):
|
| 717 |
+
x_inputs = self.inputs_embedder(
|
| 718 |
+
aatype=res_type_oh,
|
| 719 |
+
profile=profile.float(),
|
| 720 |
+
deletion_mean=deletion_mean.float(),
|
| 721 |
+
ref_pos=ref_pos,
|
| 722 |
+
atom_attention_mask=atm_mask,
|
| 723 |
+
ref_space_uid=ref_space_uid,
|
| 724 |
+
ref_charge=ref_charge,
|
| 725 |
+
ref_element=ref_element_oh,
|
| 726 |
+
ref_atom_name_chars=ref_atom_name_chars_oh,
|
| 727 |
+
atom_to_token=atom_to_token,
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
z_init = self.z_init_1(x_inputs).unsqueeze(2) + self.z_init_2(
|
| 731 |
+
x_inputs
|
| 732 |
+
).unsqueeze(1)
|
| 733 |
+
relative_position_encoding = self.rel_pos(
|
| 734 |
+
residue_index=residue_index,
|
| 735 |
+
asym_id=asym_id,
|
| 736 |
+
sym_id=sym_id,
|
| 737 |
+
entity_id=entity_id,
|
| 738 |
+
token_index=token_index,
|
| 739 |
+
)
|
| 740 |
+
token_bonds_encoding = self.token_bonds(token_bonds.float())
|
| 741 |
+
z_init = z_init + relative_position_encoding + token_bonds_encoding
|
| 742 |
+
|
| 743 |
+
if (
|
| 744 |
+
lm_hidden_states is None
|
| 745 |
+
and input_ids is not None
|
| 746 |
+
and self._esmc is not None
|
| 747 |
+
):
|
| 748 |
+
lm_hidden_states = self._compute_lm_hidden_states(
|
| 749 |
+
input_ids, asym_id, residue_index, mol_type, tok_mask
|
| 750 |
+
)
|
| 751 |
+
if lm_hidden_states is not None:
|
| 752 |
+
lm_dropout = (
|
| 753 |
+
self.config.lm_dropout
|
| 754 |
+
if self.config.force_lm_dropout_during_inference or self.training
|
| 755 |
+
else 0.0
|
| 756 |
+
)
|
| 757 |
+
lm_z = self.language_model(
|
| 758 |
+
lm_hidden_states.detach(), lm_dropout=lm_dropout
|
| 759 |
+
)
|
| 760 |
+
z_init = z_init + lm_z.to(z_init.dtype)
|
| 761 |
+
|
| 762 |
+
msa_kwargs: dict[str, Tensor] | None = None
|
| 763 |
+
if self.msa_encoder is not None and msa is not None:
|
| 764 |
+
if msa.dim() == 4:
|
| 765 |
+
batch_msa, depth, length_msa, _ = msa.shape
|
| 766 |
+
msa_oh = msa.permute(0, 2, 1, 3).float()
|
| 767 |
+
else:
|
| 768 |
+
batch_msa, depth, length_msa = msa.shape
|
| 769 |
+
msa_oh = F.one_hot(
|
| 770 |
+
msa.permute(0, 2, 1).long(), num_classes=NUM_RES_TYPES
|
| 771 |
+
).float()
|
| 772 |
+
msa_attn = (
|
| 773 |
+
msa_attention_mask.permute(0, 2, 1).float()
|
| 774 |
+
if msa_attention_mask is not None
|
| 775 |
+
else tok_mask[:, :, None].expand(-1, -1, depth).float()
|
| 776 |
+
)
|
| 777 |
+
msa_oh = msa_oh * msa_attn.unsqueeze(-1)
|
| 778 |
+
hd = (
|
| 779 |
+
has_deletion.permute(0, 2, 1).float()
|
| 780 |
+
if has_deletion is not None
|
| 781 |
+
else torch.zeros(batch_msa, length_msa, depth, device=msa.device)
|
| 782 |
+
)
|
| 783 |
+
dv = (
|
| 784 |
+
deletion_value.permute(0, 2, 1).float()
|
| 785 |
+
if deletion_value is not None
|
| 786 |
+
else torch.zeros(batch_msa, length_msa, depth, device=msa.device)
|
| 787 |
+
)
|
| 788 |
+
msa_kwargs = {
|
| 789 |
+
"x_inputs": x_inputs,
|
| 790 |
+
"msa_oh": msa_oh,
|
| 791 |
+
"has_deletion": hd,
|
| 792 |
+
"deletion_value": dv,
|
| 793 |
+
"msa_attention_mask": msa_attn,
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
pair_mask = tok_mask[:, :, None].float() * tok_mask[:, None, :].float()
|
| 797 |
+
z = torch.zeros_like(z_init)
|
| 798 |
+
prev_pair: Tensor | None = None
|
| 799 |
+
prev_disto_probs: Tensor | None = None
|
| 800 |
+
for loop_num in range(n_loops + 1):
|
| 801 |
+
z = z_init + self.pair_loop_proj(z)
|
| 802 |
+
if msa_kwargs is not None and self.msa_encoder is not None:
|
| 803 |
+
z = z + self.msa_encoder(x_pair=z, **msa_kwargs).to(z.dtype)
|
| 804 |
+
z = self.folding_trunk(z, pair_attention_mask=pair_mask)
|
| 805 |
+
|
| 806 |
+
if early_exit and loop_num < n_loops:
|
| 807 |
+
l2_converged = False
|
| 808 |
+
if prev_pair is not None and loop_num > 0:
|
| 809 |
+
rel_l2 = (z.float() - prev_pair.float()).norm() / prev_pair.float().norm().clamp(
|
| 810 |
+
min=1e-8
|
| 811 |
+
)
|
| 812 |
+
l2_converged = rel_l2.item() < 0.25
|
| 813 |
+
prev_pair = z.detach().clone()
|
| 814 |
+
sym_z = z.float() + z.float().transpose(-2, -3)
|
| 815 |
+
cur_probs = F.softmax(self.distogram_head(sym_z).float(), dim=-1)
|
| 816 |
+
if prev_disto_probs is not None and loop_num > 0:
|
| 817 |
+
kl_per_pair = (
|
| 818 |
+
cur_probs
|
| 819 |
+
* (
|
| 820 |
+
cur_probs.clamp(min=1e-8)
|
| 821 |
+
/ prev_disto_probs.clamp(min=1e-8)
|
| 822 |
+
).log()
|
| 823 |
+
).sum(-1)
|
| 824 |
+
kl = (kl_per_pair + kl_per_pair.transpose(-1, -2)).mean() / 2
|
| 825 |
+
if l2_converged or kl.item() < 0.05:
|
| 826 |
+
break
|
| 827 |
+
prev_disto_probs = cur_probs.detach()
|
| 828 |
+
|
| 829 |
+
distogram_logits = self.distogram_head(z + z.transpose(-2, -3))
|
| 830 |
+
|
| 831 |
+
with torch.no_grad(), _seed_context(seed):
|
| 832 |
+
structure_output = self.structure_head.sample(
|
| 833 |
+
z_trunk=z.float(),
|
| 834 |
+
s_inputs=x_inputs,
|
| 835 |
+
s_trunk=None,
|
| 836 |
+
relative_position_encoding=relative_position_encoding,
|
| 837 |
+
ref_pos=ref_pos,
|
| 838 |
+
ref_charge=ref_charge,
|
| 839 |
+
ref_mask=atm_mask,
|
| 840 |
+
ref_element=ref_element_oh,
|
| 841 |
+
ref_atom_name_chars=ref_atom_name_chars_oh,
|
| 842 |
+
ref_space_uid=ref_space_uid,
|
| 843 |
+
tok_idx=atom_to_token,
|
| 844 |
+
asym_id=asym_id,
|
| 845 |
+
residue_index=residue_index,
|
| 846 |
+
entity_id=entity_id,
|
| 847 |
+
token_index=token_index,
|
| 848 |
+
sym_id=sym_id,
|
| 849 |
+
token_attention_mask=tok_mask,
|
| 850 |
+
num_diffusion_samples=n_samples,
|
| 851 |
+
num_sampling_steps=num_sampling_steps,
|
| 852 |
+
return_atom_repr=False,
|
| 853 |
+
denoising_early_exit_rmsd=(0.10 if early_exit else None),
|
| 854 |
+
)
|
| 855 |
+
sample_coords = structure_output["sample_atom_coords"]
|
| 856 |
+
assert sample_coords is not None
|
| 857 |
+
|
| 858 |
+
output: dict[str, Tensor] = {
|
| 859 |
+
"distogram_logits": distogram_logits,
|
| 860 |
+
"sample_atom_coords": sample_coords,
|
| 861 |
+
}
|
| 862 |
+
if calculate_confidence and self.confidence_head is not None:
|
| 863 |
+
confidence_output = self.confidence_head(
|
| 864 |
+
s_inputs=x_inputs.detach(),
|
| 865 |
+
z=z.detach().float(),
|
| 866 |
+
x_pred=sample_coords.detach(),
|
| 867 |
+
distogram_atom_idx=distogram_atom_idx,
|
| 868 |
+
token_attention_mask=tok_mask,
|
| 869 |
+
atom_to_token=atom_to_token,
|
| 870 |
+
atom_attention_mask=atm_mask,
|
| 871 |
+
asym_id=asym_id,
|
| 872 |
+
mol_type=mol_type,
|
| 873 |
+
num_diffusion_samples=n_samples,
|
| 874 |
+
relative_position_encoding=relative_position_encoding.detach(),
|
| 875 |
+
token_bonds_encoding=token_bonds_encoding.detach(),
|
| 876 |
+
)
|
| 877 |
+
output.update(confidence_output)
|
| 878 |
+
output["atom_pad_mask"] = (
|
| 879 |
+
atm_mask.unsqueeze(0) if atm_mask.dim() == 1 else atm_mask
|
| 880 |
+
)
|
| 881 |
+
output["residue_index"] = residue_index
|
| 882 |
+
output["entity_id"] = entity_id
|
| 883 |
+
return output
|
| 884 |
+
|
| 885 |
+
@property
|
| 886 |
+
def input_builder(self):
|
| 887 |
+
if self._esmfold2_input_builder is None:
|
| 888 |
+
from .esmfold2_processor import ESMFold2InputBuilder
|
| 889 |
+
|
| 890 |
+
self._esmfold2_input_builder = ESMFold2InputBuilder()
|
| 891 |
+
return self._esmfold2_input_builder
|
| 892 |
+
|
| 893 |
+
@property
|
| 894 |
+
def input_types(self):
|
| 895 |
+
from . import esmfold2_types
|
| 896 |
+
|
| 897 |
+
return esmfold2_types
|
| 898 |
+
|
| 899 |
+
def prepare_structure_input(self, input, seed: int | None = None):
|
| 900 |
+
return self.input_builder.prepare_input(input, seed=seed, device=self.device)
|
| 901 |
+
|
| 902 |
+
@torch.no_grad()
|
| 903 |
+
def infer_protein(self, seq: str, **forward_kwargs) -> dict[str, Tensor]:
|
| 904 |
+
from .protein_utils import prepare_protein_features
|
| 905 |
+
|
| 906 |
+
features = prepare_protein_features(seq)
|
| 907 |
+
features = {name: tensor.to(self.device) for name, tensor in features.items()}
|
| 908 |
+
output = self(**features, **forward_kwargs)
|
| 909 |
+
for name in (
|
| 910 |
+
"res_type",
|
| 911 |
+
"atom_to_token",
|
| 912 |
+
"ref_atom_name_chars",
|
| 913 |
+
"atom_attention_mask",
|
| 914 |
+
"token_attention_mask",
|
| 915 |
+
"residue_index",
|
| 916 |
+
):
|
| 917 |
+
output[name] = features[name]
|
| 918 |
+
return output
|
| 919 |
+
|
| 920 |
+
def fold(
|
| 921 |
+
self,
|
| 922 |
+
input,
|
| 923 |
+
*,
|
| 924 |
+
num_loops: int = 3,
|
| 925 |
+
num_sampling_steps: int = 50,
|
| 926 |
+
num_diffusion_samples: int = 1,
|
| 927 |
+
seed: int | None = None,
|
| 928 |
+
noise_scale: float | None = None,
|
| 929 |
+
step_scale: float | None = None,
|
| 930 |
+
max_inference_sigma: int | None = None,
|
| 931 |
+
early_exit: bool = False,
|
| 932 |
+
complex_id: str = "pred",
|
| 933 |
+
):
|
| 934 |
+
return self.input_builder.fold(
|
| 935 |
+
self,
|
| 936 |
+
input,
|
| 937 |
+
num_loops=num_loops,
|
| 938 |
+
num_sampling_steps=num_sampling_steps,
|
| 939 |
+
num_diffusion_samples=num_diffusion_samples,
|
| 940 |
+
seed=seed,
|
| 941 |
+
noise_scale=noise_scale,
|
| 942 |
+
step_scale=step_scale,
|
| 943 |
+
max_inference_sigma=max_inference_sigma,
|
| 944 |
+
early_exit=early_exit,
|
| 945 |
+
complex_id=complex_id,
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
def fold_protein(
|
| 949 |
+
self,
|
| 950 |
+
sequence: str,
|
| 951 |
+
*,
|
| 952 |
+
chain_id: str = "A",
|
| 953 |
+
num_loops: int = 3,
|
| 954 |
+
num_sampling_steps: int = 50,
|
| 955 |
+
num_diffusion_samples: int = 1,
|
| 956 |
+
seed: int | None = None,
|
| 957 |
+
complex_id: str = "pred",
|
| 958 |
+
):
|
| 959 |
+
from .esmfold2_types import ProteinInput, StructurePredictionInput
|
| 960 |
+
|
| 961 |
+
input = StructurePredictionInput(
|
| 962 |
+
sequences=[ProteinInput(id=chain_id, sequence=sequence)]
|
| 963 |
+
)
|
| 964 |
+
return self.fold(
|
| 965 |
+
input,
|
| 966 |
+
num_loops=num_loops,
|
| 967 |
+
num_sampling_steps=num_sampling_steps,
|
| 968 |
+
num_diffusion_samples=num_diffusion_samples,
|
| 969 |
+
seed=seed,
|
| 970 |
+
complex_id=complex_id,
|
| 971 |
+
)
|
| 972 |
+
|
| 973 |
+
@staticmethod
|
| 974 |
+
def result_to_cif(result) -> str:
|
| 975 |
+
assert not isinstance(result, list), "Pass one MolecularComplexResult at a time."
|
| 976 |
+
return result.complex.to_mmcif()
|
| 977 |
+
|
| 978 |
+
@staticmethod
|
| 979 |
+
def result_to_pdb(result) -> str:
|
| 980 |
+
assert not isinstance(result, list), "Pass one MolecularComplexResult at a time."
|
| 981 |
+
return result.complex.to_protein_complex().to_pdb_string()
|
| 982 |
+
|
| 983 |
+
def save_as_cif(self, result, output_path: str | Path) -> None:
|
| 984 |
+
Path(output_path).write_text(self.result_to_cif(result))
|
| 985 |
+
|
| 986 |
+
def save_as_pdb(self, result, output_path: str | Path) -> None:
|
| 987 |
+
Path(output_path).write_text(self.result_to_pdb(result))
|
| 988 |
+
|
| 989 |
+
def infer_protein_as_cif(self, seq: str, **forward_kwargs) -> str:
|
| 990 |
+
return self.result_to_cif(self.fold_protein(seq, **forward_kwargs))
|
| 991 |
+
|
| 992 |
+
def infer_protein_as_pdb(self, seq: str, **forward_kwargs) -> str:
|
| 993 |
+
return self.result_to_pdb(self.fold_protein(seq, **forward_kwargs))
|
| 994 |
+
|
| 995 |
|
| 996 |
+
__all__ = [
|
| 997 |
+
"ConfidenceHead",
|
| 998 |
+
"MSAEncoder",
|
| 999 |
+
"MSAEncoderBlock",
|
| 1000 |
+
"ESMFold2ExperimentalModel",
|
| 1001 |
+
]
|
protein_utils.py
CHANGED
|
@@ -1,488 +1,488 @@
|
|
| 1 |
-
# coding=utf-8
|
| 2 |
-
# Copyright 2026 Biohub. All rights reserved.
|
| 3 |
-
#
|
| 4 |
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
-
# you may not use this file except in compliance with the License.
|
| 6 |
-
# You may obtain a copy of the License at
|
| 7 |
-
#
|
| 8 |
-
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
-
#
|
| 10 |
-
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
-
# See the License for the specific language governing permissions and
|
| 14 |
-
# limitations under the License.
|
| 15 |
-
"""Self-contained protein featurization for ESMFold2 inference.
|
| 16 |
-
|
| 17 |
-
Lets ``ESMFold2ExperimentalModel.infer_protein_as_pdb`` fold a protein sequence
|
| 18 |
-
ESMFold-style without the ``esm`` companion package. The featurization
|
| 19 |
-
mirrors ``ESMFold2InputBuilder.prepare_input`` for the protein-only path —
|
| 20 |
-
``test_prepare_protein_features.py`` enforces tensor-exact parity.
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
from __future__ import annotations
|
| 24 |
-
|
| 25 |
-
import math
|
| 26 |
-
|
| 27 |
-
import torch
|
| 28 |
-
from torch import Tensor
|
| 29 |
-
|
| 30 |
-
MOL_TYPE_PROTEIN = 0
|
| 31 |
-
PROTEIN_UNK_RES_TYPE = 22
|
| 32 |
-
MSA_GAP_TOKEN_ID = 1
|
| 33 |
-
|
| 34 |
-
PROTEIN_RESIDUE_TO_RES_TYPE: dict[str, int] = {
|
| 35 |
-
"ALA": 2,
|
| 36 |
-
"ARG": 3,
|
| 37 |
-
"ASN": 4,
|
| 38 |
-
"ASP": 5,
|
| 39 |
-
"CYS": 6,
|
| 40 |
-
"GLN": 7,
|
| 41 |
-
"GLU": 8,
|
| 42 |
-
"GLY": 9,
|
| 43 |
-
"HIS": 10,
|
| 44 |
-
"ILE": 11,
|
| 45 |
-
"LEU": 12,
|
| 46 |
-
"LYS": 13,
|
| 47 |
-
"MET": 14,
|
| 48 |
-
"PHE": 15,
|
| 49 |
-
"PRO": 16,
|
| 50 |
-
"SER": 17,
|
| 51 |
-
"THR": 18,
|
| 52 |
-
"TRP": 19,
|
| 53 |
-
"TYR": 20,
|
| 54 |
-
"VAL": 21,
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
PROTEIN_1TO3: dict[str, str] = {
|
| 58 |
-
"A": "ALA",
|
| 59 |
-
"R": "ARG",
|
| 60 |
-
"N": "ASN",
|
| 61 |
-
"D": "ASP",
|
| 62 |
-
"C": "CYS",
|
| 63 |
-
"Q": "GLN",
|
| 64 |
-
"E": "GLU",
|
| 65 |
-
"G": "GLY",
|
| 66 |
-
"H": "HIS",
|
| 67 |
-
"I": "ILE",
|
| 68 |
-
"L": "LEU",
|
| 69 |
-
"K": "LYS",
|
| 70 |
-
"M": "MET",
|
| 71 |
-
"F": "PHE",
|
| 72 |
-
"P": "PRO",
|
| 73 |
-
"S": "SER",
|
| 74 |
-
"T": "THR",
|
| 75 |
-
"W": "TRP",
|
| 76 |
-
"Y": "TYR",
|
| 77 |
-
"V": "VAL",
|
| 78 |
-
"X": "UNK",
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
ESM_PROTEIN_VOCAB: dict[str, int] = {
|
| 82 |
-
"L": 4,
|
| 83 |
-
"A": 5,
|
| 84 |
-
"G": 6,
|
| 85 |
-
"V": 7,
|
| 86 |
-
"S": 8,
|
| 87 |
-
"E": 9,
|
| 88 |
-
"R": 10,
|
| 89 |
-
"T": 11,
|
| 90 |
-
"I": 12,
|
| 91 |
-
"D": 13,
|
| 92 |
-
"P": 14,
|
| 93 |
-
"K": 15,
|
| 94 |
-
"Q": 16,
|
| 95 |
-
"N": 17,
|
| 96 |
-
"F": 18,
|
| 97 |
-
"Y": 19,
|
| 98 |
-
"M": 20,
|
| 99 |
-
"H": 21,
|
| 100 |
-
"W": 22,
|
| 101 |
-
"C": 23,
|
| 102 |
-
"X": 3,
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
# Heavy atoms per canonical residue, in training-time order.
|
| 106 |
-
PROTEIN_HEAVY_ATOMS: dict[str, list[str]] = {
|
| 107 |
-
"ALA": ["N", "CA", "C", "O", "CB"],
|
| 108 |
-
"ARG": ["N", "CA", "C", "O", "CB", "CG", "CD", "NE", "CZ", "NH1", "NH2"],
|
| 109 |
-
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2"],
|
| 110 |
-
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2"],
|
| 111 |
-
"CYS": ["N", "CA", "C", "O", "CB", "SG"],
|
| 112 |
-
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2"],
|
| 113 |
-
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2"],
|
| 114 |
-
"GLY": ["N", "CA", "C", "O"],
|
| 115 |
-
"HIS": ["N", "CA", "C", "O", "CB", "CG", "ND1", "CD2", "CE1", "NE2"],
|
| 116 |
-
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1"],
|
| 117 |
-
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2"],
|
| 118 |
-
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ"],
|
| 119 |
-
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 120 |
-
"PHE": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ"],
|
| 121 |
-
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD"],
|
| 122 |
-
"SER": ["N", "CA", "C", "O", "CB", "OG"],
|
| 123 |
-
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2"],
|
| 124 |
-
"TRP": [
|
| 125 |
-
"N",
|
| 126 |
-
"CA",
|
| 127 |
-
"C",
|
| 128 |
-
"O",
|
| 129 |
-
"CB",
|
| 130 |
-
"CG",
|
| 131 |
-
"CD1",
|
| 132 |
-
"CD2",
|
| 133 |
-
"NE1",
|
| 134 |
-
"CE2",
|
| 135 |
-
"CE3",
|
| 136 |
-
"CZ2",
|
| 137 |
-
"CZ3",
|
| 138 |
-
"CH2",
|
| 139 |
-
],
|
| 140 |
-
"TYR": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "OH"],
|
| 141 |
-
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2"],
|
| 142 |
-
"UNK": ["N", "CA", "C", "O"],
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
PROTEIN_REF_POS: dict[str, dict[str, tuple[float, float, float]]] = {
|
| 146 |
-
"ALA": {
|
| 147 |
-
"N": (-0.01003183238208294, -1.2073018550872803, -1.0555061101913452),
|
| 148 |
-
"CA": (-0.04190138354897499, 0.17447763681411743, -0.5729365348815918),
|
| 149 |
-
"C": (1.2127548456192017, 0.4737588167190552, 0.19521640241146088),
|
| 150 |
-
"O": (1.9390329122543335, 1.4484562873840332, -0.13759790360927582),
|
| 151 |
-
"CB": (-1.276943325996399, 0.4288230538368225, 0.29937705397605896),
|
| 152 |
-
},
|
| 153 |
-
"ARG": {
|
| 154 |
-
"N": (-2.0170421600341797, 0.6717798113822937, -1.1794233322143555),
|
| 155 |
-
"CA": (-2.0503084659576416, -0.5735036730766296, -0.4097220301628113),
|
| 156 |
-
"C": (-3.469440460205078, -1.0612813234329224, -0.2755832374095917),
|
| 157 |
-
"O": (-3.8218462467193604, -2.1369943618774414, -0.8294969797134399),
|
| 158 |
-
"CB": (-1.4193516969680786, -0.3735991418361664, 0.9852858781814575),
|
| 159 |
-
"CG": (0.11878877878189087, -0.3112654983997345, 0.963895857334137),
|
| 160 |
-
"CD": (0.6643245816230774, 1.0068185329437256, 0.3963329493999481),
|
| 161 |
-
"NE": (2.1090238094329834, 1.0977025032043457, 0.6120952367782593),
|
| 162 |
-
"CZ": (3.098905324935913, 0.3215920031070709, -0.09047172218561172),
|
| 163 |
-
"NH1": (4.461230278015137, 0.3844667971134186, 0.34141138195991516),
|
| 164 |
-
"NH2": (2.7856509685516357, -0.4166366159915924, -1.1148239374160767),
|
| 165 |
-
},
|
| 166 |
-
"ASN": {
|
| 167 |
-
"N": (-0.7595629096031189, 0.7503494620323181, 1.1369825601577759),
|
| 168 |
-
"CA": (-0.76087886095047, 0.23876343667507172, -0.23573364317417145),
|
| 169 |
-
"C": (-1.9211044311523438, -0.6982439160346985, -0.42196929454803467),
|
| 170 |
-
"O": (-2.677666187286377, -0.5753439664840698, -1.4223182201385498),
|
| 171 |
-
"CB": (0.5504899024963379, -0.5078350305557251, -0.5390339493751526),
|
| 172 |
-
"CG": (1.7250099182128906, 0.4264017939567566, -0.5778228640556335),
|
| 173 |
-
"OD1": (1.9470350742340088, 1.1086392402648926, -1.613560438156128),
|
| 174 |
-
"ND2": (2.57365345954895, 0.5730618834495544, 0.5608599781990051),
|
| 175 |
-
},
|
| 176 |
-
"ASP": {
|
| 177 |
-
"N": (-1.8452696800231934, -1.2169504165649414, 0.19437327980995178),
|
| 178 |
-
"CA": (-0.6379959583282471, -0.41974392533302307, 0.41681644320487976),
|
| 179 |
-
"C": (-0.9431572556495667, 1.0356197357177734, 0.18555717170238495),
|
| 180 |
-
"O": (-1.5183608531951904, 1.4045922756195068, -0.8739855885505676),
|
| 181 |
-
"CB": (0.48594576120376587, -0.8970447778701782, -0.5209363698959351),
|
| 182 |
-
"CG": (1.780342936515808, -0.19918935000896454, -0.2310730367898941),
|
| 183 |
-
"OD1": (2.5202910900115967, -0.6044584512710571, 0.7049641013145447),
|
| 184 |
-
"OD2": (2.1454880237579346, 0.9208861589431763, -0.9712985157966614),
|
| 185 |
-
},
|
| 186 |
-
"CYS": {
|
| 187 |
-
"N": (0.0469963513314724, 1.190075159072876, -1.1607273817062378),
|
| 188 |
-
"CA": (0.11344368755817413, -0.09400428831577301, -0.45952197909355164),
|
| 189 |
-
"C": (-1.2652032375335693, -0.6832379698753357, -0.3594406247138977),
|
| 190 |
-
"O": (-1.4631439447402954, -1.8851220607757568, -0.6826791763305664),
|
| 191 |
-
"CB": (0.6919880509376526, 0.09034398198127747, 0.952482283115387),
|
| 192 |
-
"SG": (2.4619927406311035, 0.5235707759857178, 0.9020372629165649),
|
| 193 |
-
},
|
| 194 |
-
"GLN": {
|
| 195 |
-
"N": (-2.370004653930664, -0.9637529850006104, -0.7942749261856079),
|
| 196 |
-
"CA": (-1.370002269744873, -0.6000258922576904, 0.2103111445903778),
|
| 197 |
-
"C": (-1.7545503377914429, 0.7091967463493347, 0.8433493971824646),
|
| 198 |
-
"O": (-1.8520662784576416, 0.7999289631843567, 2.0964975357055664),
|
| 199 |
-
"CB": (0.02040259726345539, -0.5004461407661438, -0.44764479994773865),
|
| 200 |
-
"CG": (1.1377512216567993, -0.28680720925331116, 0.582992434501648),
|
| 201 |
-
"CD": (2.4745187759399414, -0.24800164997577667, -0.09364881366491318),
|
| 202 |
-
"OE1": (3.1685523986816406, -1.2966246604919434, -0.1717153936624527),
|
| 203 |
-
"NE2": (2.947425603866577, 0.9601329565048218, -0.6888364553451538),
|
| 204 |
-
},
|
| 205 |
-
"GLU": {
|
| 206 |
-
"N": (-1.5850872993469238, -1.337684154510498, 0.9490851163864136),
|
| 207 |
-
"CA": (-1.0560977458953857, 0.027459044009447098, 1.0306966304779053),
|
| 208 |
-
"C": (-1.7741456031799316, 0.9664392471313477, 0.09259600937366486),
|
| 209 |
-
"O": (-1.9012441635131836, 2.181349992752075, 0.402479350566864),
|
| 210 |
-
"CB": (0.4706551432609558, 0.048803869634866714, 0.8114414811134338),
|
| 211 |
-
"CG": (0.9133604764938354, -0.4219329059123993, -0.5830985307693481),
|
| 212 |
-
"CD": (2.398822069168091, -0.3097084164619446, -0.7210537791252136),
|
| 213 |
-
"OE1": (3.1389315128326416, -1.274524450302124, -0.39029765129089355),
|
| 214 |
-
"OE2": (2.9647817611694336, 0.8781346082687378, -1.1732689142227173),
|
| 215 |
-
},
|
| 216 |
-
"GLY": {
|
| 217 |
-
"N": (-1.3942985534667969, -0.39875128865242004, -0.3370324671268463),
|
| 218 |
-
"CA": (-0.39974430203437805, 0.5488945245742798, 0.15242962539196014),
|
| 219 |
-
"C": (0.9440054893493652, -0.10314033925533295, 0.19859643280506134),
|
| 220 |
-
"O": (1.3352899551391602, -0.669218122959137, 1.2541258335113525),
|
| 221 |
-
},
|
| 222 |
-
"HIS": {
|
| 223 |
-
"N": (-1.4532867670059204, -1.0689626932144165, 0.881072461605072),
|
| 224 |
-
"CA": (-1.3396095037460327, 0.24797579646110535, 0.24960045516490936),
|
| 225 |
-
"C": (-2.675257921218872, 0.6571555733680725, -0.30441102385520935),
|
| 226 |
-
"O": (-3.1311378479003906, 1.8079776763916016, -0.06785715371370316),
|
| 227 |
-
"CB": (-0.3041955828666687, 0.21721023321151733, -0.8885309100151062),
|
| 228 |
-
"CG": (1.0887513160705566, 0.028941065073013306, -0.36419469118118286),
|
| 229 |
-
"ND1": (1.840459942817688, 1.0411773920059204, 0.29804590344429016),
|
| 230 |
-
"CD2": (1.780855417251587, -1.1011489629745483, -0.3814258575439453),
|
| 231 |
-
"CE1": (2.9566943645477295, 0.4924798905849457, 0.6477115750312805),
|
| 232 |
-
"NE2": (3.0280203819274902, -0.8751969337463379, 0.26084381341934204),
|
| 233 |
-
},
|
| 234 |
-
"ILE": {
|
| 235 |
-
"N": (-0.7167549729347229, -1.5426139831542969, -0.9983330368995667),
|
| 236 |
-
"CA": (-1.0636085271835327, -0.35169270634651184, -0.21393552422523499),
|
| 237 |
-
"C": (-1.3896740674972534, 0.8142145276069641, -1.1164065599441528),
|
| 238 |
-
"O": (-1.2377792596817017, 0.7302915453910828, -2.3656840324401855),
|
| 239 |
-
"CB": (0.061667006462812424, 0.01599610224366188, 0.8057394623756409),
|
| 240 |
-
"CG1": (1.502519965171814, -0.08899776637554169, 0.24154816567897797),
|
| 241 |
-
"CG2": (-0.053174979984760284, -0.8521055579185486, 2.0702083110809326),
|
| 242 |
-
"CD1": (1.7929610013961792, 0.899773120880127, -0.8863027691841125),
|
| 243 |
-
},
|
| 244 |
-
"LEU": {
|
| 245 |
-
"N": (1.9657520055770874, -1.9763224124908447, -0.18391533195972443),
|
| 246 |
-
"CA": (1.3077669143676758, -0.6677430868148804, -0.19492436945438385),
|
| 247 |
-
"C": (1.9905058145523071, 0.24182087182998657, 0.7879968285560608),
|
| 248 |
-
"O": (2.06896710395813, -0.07880014181137085, 2.0048046112060547),
|
| 249 |
-
"CB": (-0.20306941866874695, -0.8093230128288269, 0.11243502795696259),
|
| 250 |
-
"CG": (-0.9916267395019531, 0.5234957337379456, 0.06723011285066605),
|
| 251 |
-
"CD1": (-2.4228057861328125, 0.29949337244033813, 0.573042094707489),
|
| 252 |
-
"CD2": (-1.0282856225967407, 1.1250264644622803, -1.346014380455017),
|
| 253 |
-
},
|
| 254 |
-
"LYS": {
|
| 255 |
-
"N": (2.4221372604370117, -0.6473312377929688, 0.6370573043823242),
|
| 256 |
-
"CA": (2.0314927101135254, 0.2786507308483124, -0.4298512041568756),
|
| 257 |
-
"C": (2.7168593406677246, 1.595757246017456, -0.20924785733222961),
|
| 258 |
-
"O": (3.397681713104248, 2.116427421569824, -1.1332510709762573),
|
| 259 |
-
"CB": (0.5018402934074402, 0.4873858690261841, -0.49062973260879517),
|
| 260 |
-
"CG": (-0.25062066316604614, -0.7894009947776794, -0.9055535793304443),
|
| 261 |
-
"CD": (-1.769762635231018, -0.5552700161933899, -1.040329933166504),
|
| 262 |
-
"CE": (-2.576533555984497, -1.0221366882324219, 0.18493641912937164),
|
| 263 |
-
"NZ": (-2.269151210784912, -0.24293844401836395, 1.3849012851715088),
|
| 264 |
-
},
|
| 265 |
-
"MET": {
|
| 266 |
-
"N": (1.8903918266296387, -1.5252995491027832, -0.42638593912124634),
|
| 267 |
-
"CA": (1.2630571126937866, -0.24417810142040253, -0.7626462578773499),
|
| 268 |
-
"C": (2.30391001701355, 0.8367712497711182, -0.7254616618156433),
|
| 269 |
-
"O": (2.465414524078369, 1.5928632020950317, -1.7207728624343872),
|
| 270 |
-
"CB": (0.10567972809076309, 0.10861825942993164, 0.19741646945476532),
|
| 271 |
-
"CG": (-1.0658042430877686, -0.8736631274223328, 0.08811883628368378),
|
| 272 |
-
"SD": (-2.4557132720947266, -0.3332225978374481, 1.1461700201034546),
|
| 273 |
-
"CE": (-3.265165090560913, 0.7033554911613464, -0.11588376015424728),
|
| 274 |
-
},
|
| 275 |
-
"PHE": {
|
| 276 |
-
"N": (-2.8484435081481934, -1.525790810585022, 0.01789816841483116),
|
| 277 |
-
"CA": (-1.591969609260559, -0.8545162677764893, 0.35214468836784363),
|
| 278 |
-
"C": (-1.8900631666183472, 0.45833414793014526, 1.0232222080230713),
|
| 279 |
-
"O": (-1.3424992561340332, 0.74432373046875, 2.121629476547241),
|
| 280 |
-
"CB": (-0.760358452796936, -0.6342853307723999, -0.9257160425186157),
|
| 281 |
-
"CG": (0.604112982749939, -0.07200468331575394, -0.6148118376731873),
|
| 282 |
-
"CD1": (0.8468314409255981, 1.2480632066726685, -0.7146694660186768),
|
| 283 |
-
"CD2": (1.6827683448791504, -0.9758077263832092, -0.1423054188489914),
|
| 284 |
-
"CE1": (2.1801748275756836, 1.7875733375549316, -0.3744623064994812),
|
| 285 |
-
"CE2": (2.888307809829712, -0.48277512192726135, 0.16804970800876617),
|
| 286 |
-
"CZ": (3.149812936782837, 0.9656873941421509, 0.04440271109342575),
|
| 287 |
-
},
|
| 288 |
-
"PRO": {
|
| 289 |
-
"N": (-0.836250364780426, -0.9899801015853882, 0.5561304688453674),
|
| 290 |
-
"CA": (0.32722190022468567, -0.6164458394050598, -0.25072571635246277),
|
| 291 |
-
"C": (1.6121541261672974, -1.1711241006851196, 0.31082412600517273),
|
| 292 |
-
"O": (1.6127740144729614, -2.2771971225738525, 0.9156193733215332),
|
| 293 |
-
"CB": (0.3248198926448822, 0.9028244018554688, -0.33368146419525146),
|
| 294 |
-
"CG": (-1.1425083875656128, 1.2730128765106201, -0.2590600252151489),
|
| 295 |
-
"CD": (-1.8495968580245972, 0.026575811207294464, 0.2681289613246918),
|
| 296 |
-
},
|
| 297 |
-
"SER": {
|
| 298 |
-
"N": (0.674650251865387, 1.5018702745437622, -0.5367295145988464),
|
| 299 |
-
"CA": (0.00013792862591799349, 0.4966467022895813, 0.28510504961013794),
|
| 300 |
-
"C": (0.9941009879112244, -0.5374617576599121, 0.73505038022995),
|
| 301 |
-
"O": (1.0545241832733154, -0.8683545589447021, 1.9495396614074707),
|
| 302 |
-
"CB": (-1.1279288530349731, -0.1659376323223114, -0.5160963535308838),
|
| 303 |
-
"OG": (-1.8135979175567627, -1.085249662399292, 0.28947514295578003),
|
| 304 |
-
},
|
| 305 |
-
"THR": {
|
| 306 |
-
"N": (-1.325830340385437, -1.3728225231170654, 0.6882233023643494),
|
| 307 |
-
"CA": (-0.5433306097984314, -0.16364754736423492, 0.41697052121162415),
|
| 308 |
-
"C": (-1.294381856918335, 0.7077372074127197, -0.5549946427345276),
|
| 309 |
-
"O": (-1.6939635276794434, 0.23654410243034363, -1.6540418863296509),
|
| 310 |
-
"CB": (0.853203296661377, -0.5363803505897522, -0.14109353721141815),
|
| 311 |
-
"OG1": (1.5220820903778076, -1.379003643989563, 0.7635167837142944),
|
| 312 |
-
"CG2": (1.7225933074951172, 0.7054727077484131, -0.3651331067085266),
|
| 313 |
-
},
|
| 314 |
-
"TRP": {
|
| 315 |
-
"N": (3.686030864715576, 0.7599999904632568, 0.496155709028244),
|
| 316 |
-
"CA": (2.384092092514038, 0.09079249948263168, 0.5325262546539307),
|
| 317 |
-
"C": (2.1113572120666504, -0.6121063232421875, -0.7733646035194397),
|
| 318 |
-
"O": (1.796526312828064, -1.8323148488998413, -0.7775964140892029),
|
| 319 |
-
"CB": (1.281521201133728, 1.1139036417007446, 0.8559791445732117),
|
| 320 |
-
"CG": (-0.04292375594377518, 0.44645074009895325, 1.0942792892456055),
|
| 321 |
-
"CD1": (-0.42329534888267517, -0.15470874309539795, 2.2227554321289062),
|
| 322 |
-
"CD2": (-1.1023900508880615, 0.2158389836549759, 0.11529432237148285),
|
| 323 |
-
"NE1": (-1.7030320167541504, -0.7665823101997375, 2.0595016479492188),
|
| 324 |
-
"CE2": (-2.045644998550415, -0.4881173074245453, 0.710669219493866),
|
| 325 |
-
"CE3": (-1.2173502445220947, 0.6102271676063538, -1.300106406211853),
|
| 326 |
-
"CZ2": (-3.256009340286255, -0.9164394736289978, -0.00984987337142229),
|
| 327 |
-
"CZ3": (-2.315925121307373, 0.2306906282901764, -1.9776310920715332),
|
| 328 |
-
"CH2": (-3.3817875385284424, -0.5677337646484375, -1.3032053709030151),
|
| 329 |
-
},
|
| 330 |
-
"TYR": {
|
| 331 |
-
"N": (-1.7900604009628296, -0.8409399390220642, 1.3180142641067505),
|
| 332 |
-
"CA": (-1.913882851600647, 0.23552845418453217, 0.330669641494751),
|
| 333 |
-
"C": (-3.347280740737915, 0.3588399887084961, -0.09830684959888458),
|
| 334 |
-
"O": (-3.967811346054077, -0.6449354290962219, -0.5423302054405212),
|
| 335 |
-
"CB": (-1.0093992948532104, 0.0004731413209810853, -0.8981552124023438),
|
| 336 |
-
"CG": (0.4520410895347595, 0.021162061020731926, -0.5305932760238647),
|
| 337 |
-
"CD1": (1.0992432832717896, 1.1877919435501099, -0.3579142987728119),
|
| 338 |
-
"CD2": (1.1803174018859863, -1.253401279449463, -0.31122180819511414),
|
| 339 |
-
"CE1": (2.5253450870513916, 1.1990256309509277, 0.029804613441228867),
|
| 340 |
-
"CE2": (2.471151113510132, -1.240687608718872, 0.043534230440855026),
|
| 341 |
-
"CZ": (3.180687665939331, 0.04672492295503616, 0.2214856892824173),
|
| 342 |
-
"OH": (4.523719787597656, 0.0671030730009079, 0.5877485871315002),
|
| 343 |
-
},
|
| 344 |
-
"VAL": {
|
| 345 |
-
"N": (0.5987519025802612, -1.569443702697754, -0.7379124760627747),
|
| 346 |
-
"CA": (0.6014357209205627, -0.10503966361284256, -0.6336286664009094),
|
| 347 |
-
"C": (1.8391697406768799, 0.4067850410938263, 0.06351757049560547),
|
| 348 |
-
"O": (2.3952062129974365, -0.2666190266609192, 0.9731166958808899),
|
| 349 |
-
"CB": (-0.694736897945404, 0.4259096384048462, 0.03581475466489792),
|
| 350 |
-
"CG1": (-1.9276031255722046, 0.09515828639268875, -0.8172357082366943),
|
| 351 |
-
"CG2": (-0.8938426971435547, -0.08640842139720917, 1.472349762916565),
|
| 352 |
-
},
|
| 353 |
-
"UNK": {
|
| 354 |
-
"N": (0.0, 0.0, 0.0),
|
| 355 |
-
"CA": (0.0, 0.0, 0.0),
|
| 356 |
-
"C": (0.0, 0.0, 0.0),
|
| 357 |
-
"O": (0.0, 0.0, 0.0),
|
| 358 |
-
},
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
# Protonated nitrogens at physiological pH (matches CHARGED_ATOMS in the
|
| 362 |
-
# opensource constants for the protein subset).
|
| 363 |
-
PROTEIN_CHARGED_ATOMS: dict[tuple[str, str], int] = {
|
| 364 |
-
("LYS", "NZ"): 1,
|
| 365 |
-
("ARG", "NH2"): 1,
|
| 366 |
-
("HIS", "ND1"): 1,
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
# Only the elements that appear in canonical protein heavy atoms.
|
| 370 |
-
_PROTEIN_ELEMENT_TO_ATOMIC_NUM: dict[str, int] = {"C": 6, "N": 7, "O": 8, "S": 16}
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
def _encode_atom_name(name: str) -> list[int]:
|
| 374 |
-
padded = name.ljust(4)[:4]
|
| 375 |
-
return [ord(c) - 32 if c != " " else 0 for c in padded]
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
def prepare_protein_features(sequence: str) -> dict[str, Tensor]:
|
| 379 |
-
"""Featurize a single protein sequence for ESMFold2ExperimentalModel.forward.
|
| 380 |
-
|
| 381 |
-
Returns the same keys with the same dtypes/shapes as
|
| 382 |
-
``ESMFold2InputBuilder.prepare_input(StructurePredictionInput(...))``
|
| 383 |
-
restricted to a single-chain protein with no MSA, modifications,
|
| 384 |
-
distogram conditioning, or covalent bonds. All tensors have a
|
| 385 |
-
leading batch dim of 1; the caller is responsible for moving them
|
| 386 |
-
to the model device.
|
| 387 |
-
"""
|
| 388 |
-
if not sequence:
|
| 389 |
-
raise ValueError("sequence must be non-empty")
|
| 390 |
-
|
| 391 |
-
res_3letter = [PROTEIN_1TO3.get(c, "UNK") for c in sequence]
|
| 392 |
-
L = len(sequence)
|
| 393 |
-
|
| 394 |
-
token_atom_starts: list[int] = []
|
| 395 |
-
atom_records: list[tuple[int, str, str, int, tuple[float, float, float]]] = []
|
| 396 |
-
res_type_vals: list[int] = []
|
| 397 |
-
input_id_vals: list[int] = []
|
| 398 |
-
distogram_rep_atom_idx: list[int] = []
|
| 399 |
-
|
| 400 |
-
atom_cursor = 0
|
| 401 |
-
for t_idx, (letter, res_3) in enumerate(zip(sequence, res_3letter)):
|
| 402 |
-
atom_names = PROTEIN_HEAVY_ATOMS[res_3]
|
| 403 |
-
res_type = PROTEIN_RESIDUE_TO_RES_TYPE.get(res_3, PROTEIN_UNK_RES_TYPE)
|
| 404 |
-
input_id = ESM_PROTEIN_VOCAB.get(letter, ESM_PROTEIN_VOCAB["X"])
|
| 405 |
-
|
| 406 |
-
token_atom_starts.append(atom_cursor)
|
| 407 |
-
for name in atom_names:
|
| 408 |
-
charge = PROTEIN_CHARGED_ATOMS.get((res_3, name), 0)
|
| 409 |
-
element = name[0] # protein heavy atoms are all single-letter C/N/O/S
|
| 410 |
-
ref_pos = PROTEIN_REF_POS[res_3][name]
|
| 411 |
-
atom_records.append((t_idx, name, element, charge, ref_pos))
|
| 412 |
-
atom_cursor += 1
|
| 413 |
-
|
| 414 |
-
rep_name = "CB" if "CB" in atom_names else "CA"
|
| 415 |
-
distogram_rep_atom_idx.append(
|
| 416 |
-
token_atom_starts[t_idx] + atom_names.index(rep_name)
|
| 417 |
-
)
|
| 418 |
-
|
| 419 |
-
res_type_vals.append(res_type)
|
| 420 |
-
input_id_vals.append(input_id)
|
| 421 |
-
|
| 422 |
-
n_real_atoms = len(atom_records)
|
| 423 |
-
n_atoms = math.ceil(n_real_atoms / 32) * 32 if n_real_atoms > 0 else 32
|
| 424 |
-
|
| 425 |
-
ref_pos = torch.zeros(n_atoms, 3, dtype=torch.float32)
|
| 426 |
-
ref_element = torch.zeros(n_atoms, dtype=torch.int64)
|
| 427 |
-
ref_charge = torch.zeros(n_atoms, dtype=torch.int8)
|
| 428 |
-
ref_atom_name_chars = torch.zeros(n_atoms, 4, dtype=torch.int64)
|
| 429 |
-
ref_space_uid = torch.zeros(n_atoms, dtype=torch.int64)
|
| 430 |
-
atom_attention_mask = torch.zeros(n_atoms, dtype=torch.bool)
|
| 431 |
-
atom_to_token = torch.zeros(n_atoms, dtype=torch.int64)
|
| 432 |
-
|
| 433 |
-
for i, (t_idx, name, element, charge, pos) in enumerate(atom_records):
|
| 434 |
-
ref_pos[i] = torch.tensor(pos, dtype=torch.float32)
|
| 435 |
-
ref_element[i] = _PROTEIN_ELEMENT_TO_ATOMIC_NUM[element]
|
| 436 |
-
ref_charge[i] = charge
|
| 437 |
-
ref_atom_name_chars[i] = torch.tensor(
|
| 438 |
-
_encode_atom_name(name), dtype=torch.int64
|
| 439 |
-
)
|
| 440 |
-
ref_space_uid[i] = t_idx
|
| 441 |
-
atom_attention_mask[i] = True
|
| 442 |
-
atom_to_token[i] = t_idx
|
| 443 |
-
|
| 444 |
-
token_index = torch.arange(L, dtype=torch.int64)
|
| 445 |
-
residue_index = torch.arange(L, dtype=torch.int64)
|
| 446 |
-
asym_id = torch.zeros(L, dtype=torch.int64)
|
| 447 |
-
sym_id = torch.zeros(L, dtype=torch.int64)
|
| 448 |
-
entity_id = torch.ones(L, dtype=torch.int64)
|
| 449 |
-
mol_type = torch.full((L,), MOL_TYPE_PROTEIN, dtype=torch.int64)
|
| 450 |
-
res_type = torch.tensor(res_type_vals, dtype=torch.int64)
|
| 451 |
-
input_ids = torch.tensor(input_id_vals, dtype=torch.int64)
|
| 452 |
-
token_bonds = torch.zeros(L, L, 1, dtype=torch.float32)
|
| 453 |
-
token_attention_mask = torch.ones(L, dtype=torch.bool)
|
| 454 |
-
distogram_atom_idx = torch.tensor(distogram_rep_atom_idx, dtype=torch.int64)
|
| 455 |
-
|
| 456 |
-
# Single-sequence MSA: depth 1, row 0 is the sequence itself.
|
| 457 |
-
msa = res_type.unsqueeze(0)
|
| 458 |
-
msa_attention_mask = torch.ones(1, L, dtype=torch.bool)
|
| 459 |
-
has_deletion = torch.zeros(1, L, dtype=torch.bool)
|
| 460 |
-
deletion_value = torch.zeros(1, L, dtype=torch.float32)
|
| 461 |
-
deletion_mean = torch.zeros(L, dtype=torch.float32)
|
| 462 |
-
|
| 463 |
-
features = {
|
| 464 |
-
"token_index": token_index,
|
| 465 |
-
"residue_index": residue_index,
|
| 466 |
-
"asym_id": asym_id,
|
| 467 |
-
"sym_id": sym_id,
|
| 468 |
-
"entity_id": entity_id,
|
| 469 |
-
"mol_type": mol_type,
|
| 470 |
-
"res_type": res_type,
|
| 471 |
-
"input_ids": input_ids,
|
| 472 |
-
"token_bonds": token_bonds,
|
| 473 |
-
"token_attention_mask": token_attention_mask,
|
| 474 |
-
"ref_pos": ref_pos,
|
| 475 |
-
"ref_element": ref_element,
|
| 476 |
-
"ref_charge": ref_charge,
|
| 477 |
-
"ref_atom_name_chars": ref_atom_name_chars,
|
| 478 |
-
"ref_space_uid": ref_space_uid,
|
| 479 |
-
"atom_attention_mask": atom_attention_mask,
|
| 480 |
-
"atom_to_token": atom_to_token,
|
| 481 |
-
"distogram_atom_idx": distogram_atom_idx,
|
| 482 |
-
"msa": msa,
|
| 483 |
-
"msa_attention_mask": msa_attention_mask,
|
| 484 |
-
"has_deletion": has_deletion,
|
| 485 |
-
"deletion_value": deletion_value,
|
| 486 |
-
"deletion_mean": deletion_mean,
|
| 487 |
-
}
|
| 488 |
-
return {k: v.unsqueeze(0) for k, v in features.items()}
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2026 Biohub. All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""Self-contained protein featurization for ESMFold2 inference.
|
| 16 |
+
|
| 17 |
+
Lets ``ESMFold2ExperimentalModel.infer_protein_as_pdb`` fold a protein sequence
|
| 18 |
+
ESMFold-style without the ``esm`` companion package. The featurization
|
| 19 |
+
mirrors ``ESMFold2InputBuilder.prepare_input`` for the protein-only path —
|
| 20 |
+
``test_prepare_protein_features.py`` enforces tensor-exact parity.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import math
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
from torch import Tensor
|
| 29 |
+
|
| 30 |
+
MOL_TYPE_PROTEIN = 0
|
| 31 |
+
PROTEIN_UNK_RES_TYPE = 22
|
| 32 |
+
MSA_GAP_TOKEN_ID = 1
|
| 33 |
+
|
| 34 |
+
PROTEIN_RESIDUE_TO_RES_TYPE: dict[str, int] = {
|
| 35 |
+
"ALA": 2,
|
| 36 |
+
"ARG": 3,
|
| 37 |
+
"ASN": 4,
|
| 38 |
+
"ASP": 5,
|
| 39 |
+
"CYS": 6,
|
| 40 |
+
"GLN": 7,
|
| 41 |
+
"GLU": 8,
|
| 42 |
+
"GLY": 9,
|
| 43 |
+
"HIS": 10,
|
| 44 |
+
"ILE": 11,
|
| 45 |
+
"LEU": 12,
|
| 46 |
+
"LYS": 13,
|
| 47 |
+
"MET": 14,
|
| 48 |
+
"PHE": 15,
|
| 49 |
+
"PRO": 16,
|
| 50 |
+
"SER": 17,
|
| 51 |
+
"THR": 18,
|
| 52 |
+
"TRP": 19,
|
| 53 |
+
"TYR": 20,
|
| 54 |
+
"VAL": 21,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
PROTEIN_1TO3: dict[str, str] = {
|
| 58 |
+
"A": "ALA",
|
| 59 |
+
"R": "ARG",
|
| 60 |
+
"N": "ASN",
|
| 61 |
+
"D": "ASP",
|
| 62 |
+
"C": "CYS",
|
| 63 |
+
"Q": "GLN",
|
| 64 |
+
"E": "GLU",
|
| 65 |
+
"G": "GLY",
|
| 66 |
+
"H": "HIS",
|
| 67 |
+
"I": "ILE",
|
| 68 |
+
"L": "LEU",
|
| 69 |
+
"K": "LYS",
|
| 70 |
+
"M": "MET",
|
| 71 |
+
"F": "PHE",
|
| 72 |
+
"P": "PRO",
|
| 73 |
+
"S": "SER",
|
| 74 |
+
"T": "THR",
|
| 75 |
+
"W": "TRP",
|
| 76 |
+
"Y": "TYR",
|
| 77 |
+
"V": "VAL",
|
| 78 |
+
"X": "UNK",
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
ESM_PROTEIN_VOCAB: dict[str, int] = {
|
| 82 |
+
"L": 4,
|
| 83 |
+
"A": 5,
|
| 84 |
+
"G": 6,
|
| 85 |
+
"V": 7,
|
| 86 |
+
"S": 8,
|
| 87 |
+
"E": 9,
|
| 88 |
+
"R": 10,
|
| 89 |
+
"T": 11,
|
| 90 |
+
"I": 12,
|
| 91 |
+
"D": 13,
|
| 92 |
+
"P": 14,
|
| 93 |
+
"K": 15,
|
| 94 |
+
"Q": 16,
|
| 95 |
+
"N": 17,
|
| 96 |
+
"F": 18,
|
| 97 |
+
"Y": 19,
|
| 98 |
+
"M": 20,
|
| 99 |
+
"H": 21,
|
| 100 |
+
"W": 22,
|
| 101 |
+
"C": 23,
|
| 102 |
+
"X": 3,
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
# Heavy atoms per canonical residue, in training-time order.
|
| 106 |
+
PROTEIN_HEAVY_ATOMS: dict[str, list[str]] = {
|
| 107 |
+
"ALA": ["N", "CA", "C", "O", "CB"],
|
| 108 |
+
"ARG": ["N", "CA", "C", "O", "CB", "CG", "CD", "NE", "CZ", "NH1", "NH2"],
|
| 109 |
+
"ASN": ["N", "CA", "C", "O", "CB", "CG", "OD1", "ND2"],
|
| 110 |
+
"ASP": ["N", "CA", "C", "O", "CB", "CG", "OD1", "OD2"],
|
| 111 |
+
"CYS": ["N", "CA", "C", "O", "CB", "SG"],
|
| 112 |
+
"GLN": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "NE2"],
|
| 113 |
+
"GLU": ["N", "CA", "C", "O", "CB", "CG", "CD", "OE1", "OE2"],
|
| 114 |
+
"GLY": ["N", "CA", "C", "O"],
|
| 115 |
+
"HIS": ["N", "CA", "C", "O", "CB", "CG", "ND1", "CD2", "CE1", "NE2"],
|
| 116 |
+
"ILE": ["N", "CA", "C", "O", "CB", "CG1", "CG2", "CD1"],
|
| 117 |
+
"LEU": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2"],
|
| 118 |
+
"LYS": ["N", "CA", "C", "O", "CB", "CG", "CD", "CE", "NZ"],
|
| 119 |
+
"MET": ["N", "CA", "C", "O", "CB", "CG", "SD", "CE"],
|
| 120 |
+
"PHE": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ"],
|
| 121 |
+
"PRO": ["N", "CA", "C", "O", "CB", "CG", "CD"],
|
| 122 |
+
"SER": ["N", "CA", "C", "O", "CB", "OG"],
|
| 123 |
+
"THR": ["N", "CA", "C", "O", "CB", "OG1", "CG2"],
|
| 124 |
+
"TRP": [
|
| 125 |
+
"N",
|
| 126 |
+
"CA",
|
| 127 |
+
"C",
|
| 128 |
+
"O",
|
| 129 |
+
"CB",
|
| 130 |
+
"CG",
|
| 131 |
+
"CD1",
|
| 132 |
+
"CD2",
|
| 133 |
+
"NE1",
|
| 134 |
+
"CE2",
|
| 135 |
+
"CE3",
|
| 136 |
+
"CZ2",
|
| 137 |
+
"CZ3",
|
| 138 |
+
"CH2",
|
| 139 |
+
],
|
| 140 |
+
"TYR": ["N", "CA", "C", "O", "CB", "CG", "CD1", "CD2", "CE1", "CE2", "CZ", "OH"],
|
| 141 |
+
"VAL": ["N", "CA", "C", "O", "CB", "CG1", "CG2"],
|
| 142 |
+
"UNK": ["N", "CA", "C", "O"],
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
PROTEIN_REF_POS: dict[str, dict[str, tuple[float, float, float]]] = {
|
| 146 |
+
"ALA": {
|
| 147 |
+
"N": (-0.01003183238208294, -1.2073018550872803, -1.0555061101913452),
|
| 148 |
+
"CA": (-0.04190138354897499, 0.17447763681411743, -0.5729365348815918),
|
| 149 |
+
"C": (1.2127548456192017, 0.4737588167190552, 0.19521640241146088),
|
| 150 |
+
"O": (1.9390329122543335, 1.4484562873840332, -0.13759790360927582),
|
| 151 |
+
"CB": (-1.276943325996399, 0.4288230538368225, 0.29937705397605896),
|
| 152 |
+
},
|
| 153 |
+
"ARG": {
|
| 154 |
+
"N": (-2.0170421600341797, 0.6717798113822937, -1.1794233322143555),
|
| 155 |
+
"CA": (-2.0503084659576416, -0.5735036730766296, -0.4097220301628113),
|
| 156 |
+
"C": (-3.469440460205078, -1.0612813234329224, -0.2755832374095917),
|
| 157 |
+
"O": (-3.8218462467193604, -2.1369943618774414, -0.8294969797134399),
|
| 158 |
+
"CB": (-1.4193516969680786, -0.3735991418361664, 0.9852858781814575),
|
| 159 |
+
"CG": (0.11878877878189087, -0.3112654983997345, 0.963895857334137),
|
| 160 |
+
"CD": (0.6643245816230774, 1.0068185329437256, 0.3963329493999481),
|
| 161 |
+
"NE": (2.1090238094329834, 1.0977025032043457, 0.6120952367782593),
|
| 162 |
+
"CZ": (3.098905324935913, 0.3215920031070709, -0.09047172218561172),
|
| 163 |
+
"NH1": (4.461230278015137, 0.3844667971134186, 0.34141138195991516),
|
| 164 |
+
"NH2": (2.7856509685516357, -0.4166366159915924, -1.1148239374160767),
|
| 165 |
+
},
|
| 166 |
+
"ASN": {
|
| 167 |
+
"N": (-0.7595629096031189, 0.7503494620323181, 1.1369825601577759),
|
| 168 |
+
"CA": (-0.76087886095047, 0.23876343667507172, -0.23573364317417145),
|
| 169 |
+
"C": (-1.9211044311523438, -0.6982439160346985, -0.42196929454803467),
|
| 170 |
+
"O": (-2.677666187286377, -0.5753439664840698, -1.4223182201385498),
|
| 171 |
+
"CB": (0.5504899024963379, -0.5078350305557251, -0.5390339493751526),
|
| 172 |
+
"CG": (1.7250099182128906, 0.4264017939567566, -0.5778228640556335),
|
| 173 |
+
"OD1": (1.9470350742340088, 1.1086392402648926, -1.613560438156128),
|
| 174 |
+
"ND2": (2.57365345954895, 0.5730618834495544, 0.5608599781990051),
|
| 175 |
+
},
|
| 176 |
+
"ASP": {
|
| 177 |
+
"N": (-1.8452696800231934, -1.2169504165649414, 0.19437327980995178),
|
| 178 |
+
"CA": (-0.6379959583282471, -0.41974392533302307, 0.41681644320487976),
|
| 179 |
+
"C": (-0.9431572556495667, 1.0356197357177734, 0.18555717170238495),
|
| 180 |
+
"O": (-1.5183608531951904, 1.4045922756195068, -0.8739855885505676),
|
| 181 |
+
"CB": (0.48594576120376587, -0.8970447778701782, -0.5209363698959351),
|
| 182 |
+
"CG": (1.780342936515808, -0.19918935000896454, -0.2310730367898941),
|
| 183 |
+
"OD1": (2.5202910900115967, -0.6044584512710571, 0.7049641013145447),
|
| 184 |
+
"OD2": (2.1454880237579346, 0.9208861589431763, -0.9712985157966614),
|
| 185 |
+
},
|
| 186 |
+
"CYS": {
|
| 187 |
+
"N": (0.0469963513314724, 1.190075159072876, -1.1607273817062378),
|
| 188 |
+
"CA": (0.11344368755817413, -0.09400428831577301, -0.45952197909355164),
|
| 189 |
+
"C": (-1.2652032375335693, -0.6832379698753357, -0.3594406247138977),
|
| 190 |
+
"O": (-1.4631439447402954, -1.8851220607757568, -0.6826791763305664),
|
| 191 |
+
"CB": (0.6919880509376526, 0.09034398198127747, 0.952482283115387),
|
| 192 |
+
"SG": (2.4619927406311035, 0.5235707759857178, 0.9020372629165649),
|
| 193 |
+
},
|
| 194 |
+
"GLN": {
|
| 195 |
+
"N": (-2.370004653930664, -0.9637529850006104, -0.7942749261856079),
|
| 196 |
+
"CA": (-1.370002269744873, -0.6000258922576904, 0.2103111445903778),
|
| 197 |
+
"C": (-1.7545503377914429, 0.7091967463493347, 0.8433493971824646),
|
| 198 |
+
"O": (-1.8520662784576416, 0.7999289631843567, 2.0964975357055664),
|
| 199 |
+
"CB": (0.02040259726345539, -0.5004461407661438, -0.44764479994773865),
|
| 200 |
+
"CG": (1.1377512216567993, -0.28680720925331116, 0.582992434501648),
|
| 201 |
+
"CD": (2.4745187759399414, -0.24800164997577667, -0.09364881366491318),
|
| 202 |
+
"OE1": (3.1685523986816406, -1.2966246604919434, -0.1717153936624527),
|
| 203 |
+
"NE2": (2.947425603866577, 0.9601329565048218, -0.6888364553451538),
|
| 204 |
+
},
|
| 205 |
+
"GLU": {
|
| 206 |
+
"N": (-1.5850872993469238, -1.337684154510498, 0.9490851163864136),
|
| 207 |
+
"CA": (-1.0560977458953857, 0.027459044009447098, 1.0306966304779053),
|
| 208 |
+
"C": (-1.7741456031799316, 0.9664392471313477, 0.09259600937366486),
|
| 209 |
+
"O": (-1.9012441635131836, 2.181349992752075, 0.402479350566864),
|
| 210 |
+
"CB": (0.4706551432609558, 0.048803869634866714, 0.8114414811134338),
|
| 211 |
+
"CG": (0.9133604764938354, -0.4219329059123993, -0.5830985307693481),
|
| 212 |
+
"CD": (2.398822069168091, -0.3097084164619446, -0.7210537791252136),
|
| 213 |
+
"OE1": (3.1389315128326416, -1.274524450302124, -0.39029765129089355),
|
| 214 |
+
"OE2": (2.9647817611694336, 0.8781346082687378, -1.1732689142227173),
|
| 215 |
+
},
|
| 216 |
+
"GLY": {
|
| 217 |
+
"N": (-1.3942985534667969, -0.39875128865242004, -0.3370324671268463),
|
| 218 |
+
"CA": (-0.39974430203437805, 0.5488945245742798, 0.15242962539196014),
|
| 219 |
+
"C": (0.9440054893493652, -0.10314033925533295, 0.19859643280506134),
|
| 220 |
+
"O": (1.3352899551391602, -0.669218122959137, 1.2541258335113525),
|
| 221 |
+
},
|
| 222 |
+
"HIS": {
|
| 223 |
+
"N": (-1.4532867670059204, -1.0689626932144165, 0.881072461605072),
|
| 224 |
+
"CA": (-1.3396095037460327, 0.24797579646110535, 0.24960045516490936),
|
| 225 |
+
"C": (-2.675257921218872, 0.6571555733680725, -0.30441102385520935),
|
| 226 |
+
"O": (-3.1311378479003906, 1.8079776763916016, -0.06785715371370316),
|
| 227 |
+
"CB": (-0.3041955828666687, 0.21721023321151733, -0.8885309100151062),
|
| 228 |
+
"CG": (1.0887513160705566, 0.028941065073013306, -0.36419469118118286),
|
| 229 |
+
"ND1": (1.840459942817688, 1.0411773920059204, 0.29804590344429016),
|
| 230 |
+
"CD2": (1.780855417251587, -1.1011489629745483, -0.3814258575439453),
|
| 231 |
+
"CE1": (2.9566943645477295, 0.4924798905849457, 0.6477115750312805),
|
| 232 |
+
"NE2": (3.0280203819274902, -0.8751969337463379, 0.26084381341934204),
|
| 233 |
+
},
|
| 234 |
+
"ILE": {
|
| 235 |
+
"N": (-0.7167549729347229, -1.5426139831542969, -0.9983330368995667),
|
| 236 |
+
"CA": (-1.0636085271835327, -0.35169270634651184, -0.21393552422523499),
|
| 237 |
+
"C": (-1.3896740674972534, 0.8142145276069641, -1.1164065599441528),
|
| 238 |
+
"O": (-1.2377792596817017, 0.7302915453910828, -2.3656840324401855),
|
| 239 |
+
"CB": (0.061667006462812424, 0.01599610224366188, 0.8057394623756409),
|
| 240 |
+
"CG1": (1.502519965171814, -0.08899776637554169, 0.24154816567897797),
|
| 241 |
+
"CG2": (-0.053174979984760284, -0.8521055579185486, 2.0702083110809326),
|
| 242 |
+
"CD1": (1.7929610013961792, 0.899773120880127, -0.8863027691841125),
|
| 243 |
+
},
|
| 244 |
+
"LEU": {
|
| 245 |
+
"N": (1.9657520055770874, -1.9763224124908447, -0.18391533195972443),
|
| 246 |
+
"CA": (1.3077669143676758, -0.6677430868148804, -0.19492436945438385),
|
| 247 |
+
"C": (1.9905058145523071, 0.24182087182998657, 0.7879968285560608),
|
| 248 |
+
"O": (2.06896710395813, -0.07880014181137085, 2.0048046112060547),
|
| 249 |
+
"CB": (-0.20306941866874695, -0.8093230128288269, 0.11243502795696259),
|
| 250 |
+
"CG": (-0.9916267395019531, 0.5234957337379456, 0.06723011285066605),
|
| 251 |
+
"CD1": (-2.4228057861328125, 0.29949337244033813, 0.573042094707489),
|
| 252 |
+
"CD2": (-1.0282856225967407, 1.1250264644622803, -1.346014380455017),
|
| 253 |
+
},
|
| 254 |
+
"LYS": {
|
| 255 |
+
"N": (2.4221372604370117, -0.6473312377929688, 0.6370573043823242),
|
| 256 |
+
"CA": (2.0314927101135254, 0.2786507308483124, -0.4298512041568756),
|
| 257 |
+
"C": (2.7168593406677246, 1.595757246017456, -0.20924785733222961),
|
| 258 |
+
"O": (3.397681713104248, 2.116427421569824, -1.1332510709762573),
|
| 259 |
+
"CB": (0.5018402934074402, 0.4873858690261841, -0.49062973260879517),
|
| 260 |
+
"CG": (-0.25062066316604614, -0.7894009947776794, -0.9055535793304443),
|
| 261 |
+
"CD": (-1.769762635231018, -0.5552700161933899, -1.040329933166504),
|
| 262 |
+
"CE": (-2.576533555984497, -1.0221366882324219, 0.18493641912937164),
|
| 263 |
+
"NZ": (-2.269151210784912, -0.24293844401836395, 1.3849012851715088),
|
| 264 |
+
},
|
| 265 |
+
"MET": {
|
| 266 |
+
"N": (1.8903918266296387, -1.5252995491027832, -0.42638593912124634),
|
| 267 |
+
"CA": (1.2630571126937866, -0.24417810142040253, -0.7626462578773499),
|
| 268 |
+
"C": (2.30391001701355, 0.8367712497711182, -0.7254616618156433),
|
| 269 |
+
"O": (2.465414524078369, 1.5928632020950317, -1.7207728624343872),
|
| 270 |
+
"CB": (0.10567972809076309, 0.10861825942993164, 0.19741646945476532),
|
| 271 |
+
"CG": (-1.0658042430877686, -0.8736631274223328, 0.08811883628368378),
|
| 272 |
+
"SD": (-2.4557132720947266, -0.3332225978374481, 1.1461700201034546),
|
| 273 |
+
"CE": (-3.265165090560913, 0.7033554911613464, -0.11588376015424728),
|
| 274 |
+
},
|
| 275 |
+
"PHE": {
|
| 276 |
+
"N": (-2.8484435081481934, -1.525790810585022, 0.01789816841483116),
|
| 277 |
+
"CA": (-1.591969609260559, -0.8545162677764893, 0.35214468836784363),
|
| 278 |
+
"C": (-1.8900631666183472, 0.45833414793014526, 1.0232222080230713),
|
| 279 |
+
"O": (-1.3424992561340332, 0.74432373046875, 2.121629476547241),
|
| 280 |
+
"CB": (-0.760358452796936, -0.6342853307723999, -0.9257160425186157),
|
| 281 |
+
"CG": (0.604112982749939, -0.07200468331575394, -0.6148118376731873),
|
| 282 |
+
"CD1": (0.8468314409255981, 1.2480632066726685, -0.7146694660186768),
|
| 283 |
+
"CD2": (1.6827683448791504, -0.9758077263832092, -0.1423054188489914),
|
| 284 |
+
"CE1": (2.1801748275756836, 1.7875733375549316, -0.3744623064994812),
|
| 285 |
+
"CE2": (2.888307809829712, -0.48277512192726135, 0.16804970800876617),
|
| 286 |
+
"CZ": (3.149812936782837, 0.9656873941421509, 0.04440271109342575),
|
| 287 |
+
},
|
| 288 |
+
"PRO": {
|
| 289 |
+
"N": (-0.836250364780426, -0.9899801015853882, 0.5561304688453674),
|
| 290 |
+
"CA": (0.32722190022468567, -0.6164458394050598, -0.25072571635246277),
|
| 291 |
+
"C": (1.6121541261672974, -1.1711241006851196, 0.31082412600517273),
|
| 292 |
+
"O": (1.6127740144729614, -2.2771971225738525, 0.9156193733215332),
|
| 293 |
+
"CB": (0.3248198926448822, 0.9028244018554688, -0.33368146419525146),
|
| 294 |
+
"CG": (-1.1425083875656128, 1.2730128765106201, -0.2590600252151489),
|
| 295 |
+
"CD": (-1.8495968580245972, 0.026575811207294464, 0.2681289613246918),
|
| 296 |
+
},
|
| 297 |
+
"SER": {
|
| 298 |
+
"N": (0.674650251865387, 1.5018702745437622, -0.5367295145988464),
|
| 299 |
+
"CA": (0.00013792862591799349, 0.4966467022895813, 0.28510504961013794),
|
| 300 |
+
"C": (0.9941009879112244, -0.5374617576599121, 0.73505038022995),
|
| 301 |
+
"O": (1.0545241832733154, -0.8683545589447021, 1.9495396614074707),
|
| 302 |
+
"CB": (-1.1279288530349731, -0.1659376323223114, -0.5160963535308838),
|
| 303 |
+
"OG": (-1.8135979175567627, -1.085249662399292, 0.28947514295578003),
|
| 304 |
+
},
|
| 305 |
+
"THR": {
|
| 306 |
+
"N": (-1.325830340385437, -1.3728225231170654, 0.6882233023643494),
|
| 307 |
+
"CA": (-0.5433306097984314, -0.16364754736423492, 0.41697052121162415),
|
| 308 |
+
"C": (-1.294381856918335, 0.7077372074127197, -0.5549946427345276),
|
| 309 |
+
"O": (-1.6939635276794434, 0.23654410243034363, -1.6540418863296509),
|
| 310 |
+
"CB": (0.853203296661377, -0.5363803505897522, -0.14109353721141815),
|
| 311 |
+
"OG1": (1.5220820903778076, -1.379003643989563, 0.7635167837142944),
|
| 312 |
+
"CG2": (1.7225933074951172, 0.7054727077484131, -0.3651331067085266),
|
| 313 |
+
},
|
| 314 |
+
"TRP": {
|
| 315 |
+
"N": (3.686030864715576, 0.7599999904632568, 0.496155709028244),
|
| 316 |
+
"CA": (2.384092092514038, 0.09079249948263168, 0.5325262546539307),
|
| 317 |
+
"C": (2.1113572120666504, -0.6121063232421875, -0.7733646035194397),
|
| 318 |
+
"O": (1.796526312828064, -1.8323148488998413, -0.7775964140892029),
|
| 319 |
+
"CB": (1.281521201133728, 1.1139036417007446, 0.8559791445732117),
|
| 320 |
+
"CG": (-0.04292375594377518, 0.44645074009895325, 1.0942792892456055),
|
| 321 |
+
"CD1": (-0.42329534888267517, -0.15470874309539795, 2.2227554321289062),
|
| 322 |
+
"CD2": (-1.1023900508880615, 0.2158389836549759, 0.11529432237148285),
|
| 323 |
+
"NE1": (-1.7030320167541504, -0.7665823101997375, 2.0595016479492188),
|
| 324 |
+
"CE2": (-2.045644998550415, -0.4881173074245453, 0.710669219493866),
|
| 325 |
+
"CE3": (-1.2173502445220947, 0.6102271676063538, -1.300106406211853),
|
| 326 |
+
"CZ2": (-3.256009340286255, -0.9164394736289978, -0.00984987337142229),
|
| 327 |
+
"CZ3": (-2.315925121307373, 0.2306906282901764, -1.9776310920715332),
|
| 328 |
+
"CH2": (-3.3817875385284424, -0.5677337646484375, -1.3032053709030151),
|
| 329 |
+
},
|
| 330 |
+
"TYR": {
|
| 331 |
+
"N": (-1.7900604009628296, -0.8409399390220642, 1.3180142641067505),
|
| 332 |
+
"CA": (-1.913882851600647, 0.23552845418453217, 0.330669641494751),
|
| 333 |
+
"C": (-3.347280740737915, 0.3588399887084961, -0.09830684959888458),
|
| 334 |
+
"O": (-3.967811346054077, -0.6449354290962219, -0.5423302054405212),
|
| 335 |
+
"CB": (-1.0093992948532104, 0.0004731413209810853, -0.8981552124023438),
|
| 336 |
+
"CG": (0.4520410895347595, 0.021162061020731926, -0.5305932760238647),
|
| 337 |
+
"CD1": (1.0992432832717896, 1.1877919435501099, -0.3579142987728119),
|
| 338 |
+
"CD2": (1.1803174018859863, -1.253401279449463, -0.31122180819511414),
|
| 339 |
+
"CE1": (2.5253450870513916, 1.1990256309509277, 0.029804613441228867),
|
| 340 |
+
"CE2": (2.471151113510132, -1.240687608718872, 0.043534230440855026),
|
| 341 |
+
"CZ": (3.180687665939331, 0.04672492295503616, 0.2214856892824173),
|
| 342 |
+
"OH": (4.523719787597656, 0.0671030730009079, 0.5877485871315002),
|
| 343 |
+
},
|
| 344 |
+
"VAL": {
|
| 345 |
+
"N": (0.5987519025802612, -1.569443702697754, -0.7379124760627747),
|
| 346 |
+
"CA": (0.6014357209205627, -0.10503966361284256, -0.6336286664009094),
|
| 347 |
+
"C": (1.8391697406768799, 0.4067850410938263, 0.06351757049560547),
|
| 348 |
+
"O": (2.3952062129974365, -0.2666190266609192, 0.9731166958808899),
|
| 349 |
+
"CB": (-0.694736897945404, 0.4259096384048462, 0.03581475466489792),
|
| 350 |
+
"CG1": (-1.9276031255722046, 0.09515828639268875, -0.8172357082366943),
|
| 351 |
+
"CG2": (-0.8938426971435547, -0.08640842139720917, 1.472349762916565),
|
| 352 |
+
},
|
| 353 |
+
"UNK": {
|
| 354 |
+
"N": (0.0, 0.0, 0.0),
|
| 355 |
+
"CA": (0.0, 0.0, 0.0),
|
| 356 |
+
"C": (0.0, 0.0, 0.0),
|
| 357 |
+
"O": (0.0, 0.0, 0.0),
|
| 358 |
+
},
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
# Protonated nitrogens at physiological pH (matches CHARGED_ATOMS in the
|
| 362 |
+
# opensource constants for the protein subset).
|
| 363 |
+
PROTEIN_CHARGED_ATOMS: dict[tuple[str, str], int] = {
|
| 364 |
+
("LYS", "NZ"): 1,
|
| 365 |
+
("ARG", "NH2"): 1,
|
| 366 |
+
("HIS", "ND1"): 1,
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
# Only the elements that appear in canonical protein heavy atoms.
|
| 370 |
+
_PROTEIN_ELEMENT_TO_ATOMIC_NUM: dict[str, int] = {"C": 6, "N": 7, "O": 8, "S": 16}
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _encode_atom_name(name: str) -> list[int]:
|
| 374 |
+
padded = name.ljust(4)[:4]
|
| 375 |
+
return [ord(c) - 32 if c != " " else 0 for c in padded]
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def prepare_protein_features(sequence: str) -> dict[str, Tensor]:
|
| 379 |
+
"""Featurize a single protein sequence for ESMFold2ExperimentalModel.forward.
|
| 380 |
+
|
| 381 |
+
Returns the same keys with the same dtypes/shapes as
|
| 382 |
+
``ESMFold2InputBuilder.prepare_input(StructurePredictionInput(...))``
|
| 383 |
+
restricted to a single-chain protein with no MSA, modifications,
|
| 384 |
+
distogram conditioning, or covalent bonds. All tensors have a
|
| 385 |
+
leading batch dim of 1; the caller is responsible for moving them
|
| 386 |
+
to the model device.
|
| 387 |
+
"""
|
| 388 |
+
if not sequence:
|
| 389 |
+
raise ValueError("sequence must be non-empty")
|
| 390 |
+
|
| 391 |
+
res_3letter = [PROTEIN_1TO3.get(c, "UNK") for c in sequence]
|
| 392 |
+
L = len(sequence)
|
| 393 |
+
|
| 394 |
+
token_atom_starts: list[int] = []
|
| 395 |
+
atom_records: list[tuple[int, str, str, int, tuple[float, float, float]]] = []
|
| 396 |
+
res_type_vals: list[int] = []
|
| 397 |
+
input_id_vals: list[int] = []
|
| 398 |
+
distogram_rep_atom_idx: list[int] = []
|
| 399 |
+
|
| 400 |
+
atom_cursor = 0
|
| 401 |
+
for t_idx, (letter, res_3) in enumerate(zip(sequence, res_3letter)):
|
| 402 |
+
atom_names = PROTEIN_HEAVY_ATOMS[res_3]
|
| 403 |
+
res_type = PROTEIN_RESIDUE_TO_RES_TYPE.get(res_3, PROTEIN_UNK_RES_TYPE)
|
| 404 |
+
input_id = ESM_PROTEIN_VOCAB.get(letter, ESM_PROTEIN_VOCAB["X"])
|
| 405 |
+
|
| 406 |
+
token_atom_starts.append(atom_cursor)
|
| 407 |
+
for name in atom_names:
|
| 408 |
+
charge = PROTEIN_CHARGED_ATOMS.get((res_3, name), 0)
|
| 409 |
+
element = name[0] # protein heavy atoms are all single-letter C/N/O/S
|
| 410 |
+
ref_pos = PROTEIN_REF_POS[res_3][name]
|
| 411 |
+
atom_records.append((t_idx, name, element, charge, ref_pos))
|
| 412 |
+
atom_cursor += 1
|
| 413 |
+
|
| 414 |
+
rep_name = "CB" if "CB" in atom_names else "CA"
|
| 415 |
+
distogram_rep_atom_idx.append(
|
| 416 |
+
token_atom_starts[t_idx] + atom_names.index(rep_name)
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
res_type_vals.append(res_type)
|
| 420 |
+
input_id_vals.append(input_id)
|
| 421 |
+
|
| 422 |
+
n_real_atoms = len(atom_records)
|
| 423 |
+
n_atoms = math.ceil(n_real_atoms / 32) * 32 if n_real_atoms > 0 else 32
|
| 424 |
+
|
| 425 |
+
ref_pos = torch.zeros(n_atoms, 3, dtype=torch.float32)
|
| 426 |
+
ref_element = torch.zeros(n_atoms, dtype=torch.int64)
|
| 427 |
+
ref_charge = torch.zeros(n_atoms, dtype=torch.int8)
|
| 428 |
+
ref_atom_name_chars = torch.zeros(n_atoms, 4, dtype=torch.int64)
|
| 429 |
+
ref_space_uid = torch.zeros(n_atoms, dtype=torch.int64)
|
| 430 |
+
atom_attention_mask = torch.zeros(n_atoms, dtype=torch.bool)
|
| 431 |
+
atom_to_token = torch.zeros(n_atoms, dtype=torch.int64)
|
| 432 |
+
|
| 433 |
+
for i, (t_idx, name, element, charge, pos) in enumerate(atom_records):
|
| 434 |
+
ref_pos[i] = torch.tensor(pos, dtype=torch.float32)
|
| 435 |
+
ref_element[i] = _PROTEIN_ELEMENT_TO_ATOMIC_NUM[element]
|
| 436 |
+
ref_charge[i] = charge
|
| 437 |
+
ref_atom_name_chars[i] = torch.tensor(
|
| 438 |
+
_encode_atom_name(name), dtype=torch.int64
|
| 439 |
+
)
|
| 440 |
+
ref_space_uid[i] = t_idx
|
| 441 |
+
atom_attention_mask[i] = True
|
| 442 |
+
atom_to_token[i] = t_idx
|
| 443 |
+
|
| 444 |
+
token_index = torch.arange(L, dtype=torch.int64)
|
| 445 |
+
residue_index = torch.arange(L, dtype=torch.int64)
|
| 446 |
+
asym_id = torch.zeros(L, dtype=torch.int64)
|
| 447 |
+
sym_id = torch.zeros(L, dtype=torch.int64)
|
| 448 |
+
entity_id = torch.ones(L, dtype=torch.int64)
|
| 449 |
+
mol_type = torch.full((L,), MOL_TYPE_PROTEIN, dtype=torch.int64)
|
| 450 |
+
res_type = torch.tensor(res_type_vals, dtype=torch.int64)
|
| 451 |
+
input_ids = torch.tensor(input_id_vals, dtype=torch.int64)
|
| 452 |
+
token_bonds = torch.zeros(L, L, 1, dtype=torch.float32)
|
| 453 |
+
token_attention_mask = torch.ones(L, dtype=torch.bool)
|
| 454 |
+
distogram_atom_idx = torch.tensor(distogram_rep_atom_idx, dtype=torch.int64)
|
| 455 |
+
|
| 456 |
+
# Single-sequence MSA: depth 1, row 0 is the sequence itself.
|
| 457 |
+
msa = res_type.unsqueeze(0)
|
| 458 |
+
msa_attention_mask = torch.ones(1, L, dtype=torch.bool)
|
| 459 |
+
has_deletion = torch.zeros(1, L, dtype=torch.bool)
|
| 460 |
+
deletion_value = torch.zeros(1, L, dtype=torch.float32)
|
| 461 |
+
deletion_mean = torch.zeros(L, dtype=torch.float32)
|
| 462 |
+
|
| 463 |
+
features = {
|
| 464 |
+
"token_index": token_index,
|
| 465 |
+
"residue_index": residue_index,
|
| 466 |
+
"asym_id": asym_id,
|
| 467 |
+
"sym_id": sym_id,
|
| 468 |
+
"entity_id": entity_id,
|
| 469 |
+
"mol_type": mol_type,
|
| 470 |
+
"res_type": res_type,
|
| 471 |
+
"input_ids": input_ids,
|
| 472 |
+
"token_bonds": token_bonds,
|
| 473 |
+
"token_attention_mask": token_attention_mask,
|
| 474 |
+
"ref_pos": ref_pos,
|
| 475 |
+
"ref_element": ref_element,
|
| 476 |
+
"ref_charge": ref_charge,
|
| 477 |
+
"ref_atom_name_chars": ref_atom_name_chars,
|
| 478 |
+
"ref_space_uid": ref_space_uid,
|
| 479 |
+
"atom_attention_mask": atom_attention_mask,
|
| 480 |
+
"atom_to_token": atom_to_token,
|
| 481 |
+
"distogram_atom_idx": distogram_atom_idx,
|
| 482 |
+
"msa": msa,
|
| 483 |
+
"msa_attention_mask": msa_attention_mask,
|
| 484 |
+
"has_deletion": has_deletion,
|
| 485 |
+
"deletion_value": deletion_value,
|
| 486 |
+
"deletion_mean": deletion_mean,
|
| 487 |
+
}
|
| 488 |
+
return {k: v.unsqueeze(0) for k, v in features.items()}
|