Upload folder using huggingface_hub
Browse files- .gitattributes +22 -10
- .gitignore +6 -0
- LICENSE +21 -0
- README.md +316 -0
- config.json +1 -0
- environment.yml +281 -0
- model/dms_enrichment.py +382 -0
- model/dms_utils.py +73 -0
- model/eval_ablang.py +65 -0
- model/generate_dms.py +60 -0
- model/multichain_util.py +216 -0
- model/parse_abysis.py +65 -0
- model/plot_ab-binding_benchmarks.py +755 -0
- model/plot_esm1v_benchmarks.py +317 -0
- model/plot_mpnn_benchmarks.py +232 -0
- model/score_log_likelihoods.py +162 -0
- model/util.py +316 -0
- scripts/esm1v_ab_benchmarking.sh +84 -0
- scripts/examples/7mmo_abc_fvar.pdb +0 -0
- scripts/if_ab_benchmarking.sh +44 -0
- scripts/mpnn_ab_benchmarking.sh +39 -0
- scripts/recommend.py +152 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,47 @@
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bin.* filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 11 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 13 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 17 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 20 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 21 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 22 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 24 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.db* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.ark* filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*.gguf* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.ggml filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.llamafile* filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.pt2 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 46 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data
|
| 2 |
+
output
|
| 3 |
+
*.pyc
|
| 4 |
+
*.png
|
| 5 |
+
*.log
|
| 6 |
+
.tar
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 varun-shanker
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- life-science
|
| 9 |
+
- protein
|
| 10 |
+
- antibody
|
| 11 |
+
- mutation
|
| 12 |
+
- inverse-folding
|
| 13 |
+
- structural-evolution
|
| 14 |
+
frameworks: PyTorch
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">Structural Evolution</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Introduction
|
| 24 |
+
|
| 25 |
+
Structural Evolution is an unsupervised protein and antibody mutation recommendation workflow based on a structure-informed protein language model. It takes a PDB/CIF structure of a protein or protein complex as input, scores candidate mutation sequences under structural conditions with ESM-IF1, and selects high-probability substitutions from deep mutational scanning candidates to assist protein and antibody sequence optimization.
|
| 26 |
+
|
| 27 |
+
Paper:
|
| 28 |
+
|
| 29 |
+
> **Unsupervised evolution of protein and antibody complexes with a structure-informed language model**
|
| 30 |
+
> https://doi.org/10.1126/science.adk8946
|
| 31 |
+
|
| 32 |
+
# Model Description
|
| 33 |
+
|
| 34 |
+
Structural Evolution uses the structure-conditioned language model ESM-IF1 to recommend mutations for a target chain in a given protein structure. The program first extracts the target chain's wild-type sequence from the input PDB/CIF and generates single-point deep mutational scanning candidates. It then calculates the log-likelihood of each candidate under a single-chain or multichain backbone condition and selects recommended mutations in descending score order.
|
| 35 |
+
|
| 36 |
+
# Use Cases
|
| 37 |
+
|
| 38 |
+
| Use case | Description |
|
| 39 |
+
| --- | --- |
|
| 40 |
+
| Single-point protein mutation recommendation | Select high-probability amino acid substitutions from structure information |
|
| 41 |
+
| Antibody sequence optimization | Recommend structure-conditioned mutations for antibody heavy and light chains separately |
|
| 42 |
+
| Protein complex optimization | Evaluate target-chain mutations in a multichain backbone context |
|
| 43 |
+
| Deep mutational scanning candidate screening | Score all single-point mutations and output high-scoring candidates |
|
| 44 |
+
|
| 45 |
+
# Usage
|
| 46 |
+
|
| 47 |
+
## 1. Using OneCode
|
| 48 |
+
|
| 49 |
+
Experience intelligent one-click AI4S programming in the OneCode online environment:
|
| 50 |
+
|
| 51 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 52 |
+
|
| 53 |
+
## 2. Manual Installation and Usage
|
| 54 |
+
|
| 55 |
+
**Hardware Requirements**
|
| 56 |
+
|
| 57 |
+
- Structural Evolution supports inference on CPUs and GPUs.
|
| 58 |
+
- A GPU/DCU is recommended for ESM-IF1 inference to accelerate candidate sequence scoring.
|
| 59 |
+
- Larger protein complexes or larger mutation candidate libraries require more GPU/DCU memory and host memory.
|
| 60 |
+
- If no GPU/DCU is available, you can explicitly use the CPU with `--nogpu`, but execution will be significantly slower.
|
| 61 |
+
|
| 62 |
+
### Set Up the Runtime Environment
|
| 63 |
+
|
| 64 |
+
#### DCU Environment
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Activate DTK and CONDA first
|
| 68 |
+
conda create -n onescience311 python=3.11 -y
|
| 69 |
+
conda activate onescience311
|
| 70 |
+
|
| 71 |
+
# Install with uv support
|
| 72 |
+
pip install onescience[bio] \
|
| 73 |
+
-i http://mirrors.onescience.ai:3141/pypi/simple/ \
|
| 74 |
+
--trusted-host mirrors.onescience.ai
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
#### Environment Notes
|
| 78 |
+
|
| 79 |
+
- If you encounter missing dependencies or version incompatibilities during execution, refer to the dependency versions specified in `environment.yml` and install or adjust the relevant dependencies as needed.
|
| 80 |
+
|
| 81 |
+
### Prepare the Weights
|
| 82 |
+
|
| 83 |
+
Complete Structural Evolution inference only requires the additional **ESM-IF1 model weights**. Standard mutation recommendation does not require downloading any additional training dataset from the paper.
|
| 84 |
+
|
| 85 |
+
#### 1) ESM-IF1 Model Weights
|
| 86 |
+
|
| 87 |
+
Download the ESM-IF1 weights:
|
| 88 |
+
|
| 89 |
+
```text
|
| 90 |
+
https://zenodo.org/records/12631662
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Command:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
wget -P ~/.cache/torch/hub/checkpoints \
|
| 97 |
+
https://zenodo.org/records/12631662/files/esm_if1_20220410.zip
|
| 98 |
+
|
| 99 |
+
unzip ~/.cache/torch/hub/checkpoints/esm_if1_20220410.zip \
|
| 100 |
+
-d ~/.cache/torch/hub/checkpoints/
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
After extraction, make sure that the following file exists:
|
| 104 |
+
|
| 105 |
+
```text
|
| 106 |
+
~/.cache/torch/hub/checkpoints/
|
| 107 |
+
└── esm_if1_20220410.pt
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
The file must exist.
|
| 111 |
+
|
| 112 |
+
`scripts/recommend.py` always loads the model from:
|
| 113 |
+
|
| 114 |
+
```text
|
| 115 |
+
~/.cache/torch/hub/checkpoints/esm_if1_20220410.pt
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
loads the model from this path. If you save the weights elsewhere, update the checkpoint path in the code.
|
| 119 |
+
|
| 120 |
+
## 3. Quick Start
|
| 121 |
+
|
| 122 |
+
### Download the Model Package
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
hf download OneScience-Group/structural-evolution \
|
| 126 |
+
--local-dir ./structural-evolution
|
| 127 |
+
|
| 128 |
+
cd structural-evolution
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
- Structural Evolution additionally depends on the **ESM-IF1 model weights**. Prepare `esm_if1_20220410.pt` as described in "Prepare the Weights" and make sure that the checkpoint path in the code matches its actual location.
|
| 132 |
+
|
| 133 |
+
### Quick Verification
|
| 134 |
+
|
| 135 |
+
View the inference options:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python scripts/recommend.py --help
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Run a quick verification with the official example structure:
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
python scripts/recommend.py \
|
| 145 |
+
scripts/examples/7mmo_abc_fvar.pdb \
|
| 146 |
+
--chain A \
|
| 147 |
+
--n 10
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
# Example Data
|
| 151 |
+
|
| 152 |
+
The official repository provides an example structure in:
|
| 153 |
+
|
| 154 |
+
```text
|
| 155 |
+
scripts/examples/
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
The directory contains:
|
| 159 |
+
|
| 160 |
+
```text
|
| 161 |
+
scripts/examples/
|
| 162 |
+
└─�� 7mmo_abc_fvar.pdb
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
This structure contains the LYCoV-1404 antibody variable region and SARS-CoV-2 RBD, and demonstrates mutation recommendation for an antibody heavy chain.
|
| 166 |
+
|
| 167 |
+
For your own tasks, prepare:
|
| 168 |
+
|
| 169 |
+
```text
|
| 170 |
+
PDB or CIF structure of a protein or protein complex
|
| 171 |
+
+
|
| 172 |
+
Target chain ID
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
For example:
|
| 176 |
+
|
| 177 |
+
```text
|
| 178 |
+
structure.pdb
|
| 179 |
+
chain A
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
# Inference Examples
|
| 183 |
+
|
| 184 |
+
## Basic Mutation Recommendation
|
| 185 |
+
|
| 186 |
+
The simplest way to run it is:
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
python scripts/recommend.py \
|
| 190 |
+
/path/to/structure.pdb \
|
| 191 |
+
--chain A
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
Default output:
|
| 195 |
+
|
| 196 |
+
```text
|
| 197 |
+
Top 10 mutations
|
| 198 |
+
maxrep = 1
|
| 199 |
+
multichain backbone = True
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
This recommends 10 mutations by default and allows each original residue position to appear at most once.
|
| 203 |
+
|
| 204 |
+
## Antibody Example
|
| 205 |
+
|
| 206 |
+
Official example:
|
| 207 |
+
|
| 208 |
+
```bash
|
| 209 |
+
python scripts/recommend.py \
|
| 210 |
+
scripts/examples/7mmo_abc_fvar.pdb \
|
| 211 |
+
--chain A \
|
| 212 |
+
--seqpath scripts/examples/7mmo_chainA_lib.fasta \
|
| 213 |
+
--outpath scripts/examples/7mmo_chainA_scores.csv \
|
| 214 |
+
--upperbound 109 \
|
| 215 |
+
--offset 1
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
Where:
|
| 219 |
+
|
| 220 |
+
| Parameter | Description |
|
| 221 |
+
| --- | --- |
|
| 222 |
+
| `--chain` | Target chain ID |
|
| 223 |
+
| `--seqpath` | Output path for the generated deep mutational scanning FASTA |
|
| 224 |
+
| `--outpath` | Output path for the CSV containing scores for all mutation candidates |
|
| 225 |
+
| `--n` | Number of mutations to recommend; default: 10 |
|
| 226 |
+
| `--maxrep` | Maximum number of times the same position may appear in the recommendations; default: 1 |
|
| 227 |
+
| `--upperbound` | During final recommendation, consider only positions below this residue number |
|
| 228 |
+
| `--offset` | Offset correction for PDB residue numbering |
|
| 229 |
+
| `--order` | Specify chain order in a multichain structure |
|
| 230 |
+
| `--multichain-backbone` | Use all chains as the structural context |
|
| 231 |
+
| `--singlechain-backbone` | Use only the target chain backbone |
|
| 232 |
+
| `--nogpu` | Force CPU execution |
|
| 233 |
+
|
| 234 |
+
In the antibody example above, `--chain A` specifies the heavy chain. `--upperbound 109` excludes mutations in the final framework region. Because the input structure lacks the first residue, `--offset 1` corrects the mutation numbering.
|
| 235 |
+
|
| 236 |
+
## Customize the Number of Recommendations
|
| 237 |
+
|
| 238 |
+
For example, output the top 20 candidates and allow the same position to appear at most twice:
|
| 239 |
+
|
| 240 |
+
```bash
|
| 241 |
+
python scripts/recommend.py \
|
| 242 |
+
/path/to/your_structure.pdb \
|
| 243 |
+
--chain A \
|
| 244 |
+
--n 20 \
|
| 245 |
+
--maxrep 2
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
## Single-Chain Backbone Condition
|
| 249 |
+
|
| 250 |
+
To use only the backbone information of the target chain:
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
python scripts/recommend.py \
|
| 254 |
+
/path/to/your_structure.pdb \
|
| 255 |
+
--chain A \
|
| 256 |
+
--singlechain-backbone
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
## CPU Inference
|
| 260 |
+
|
| 261 |
+
```bash
|
| 262 |
+
python scripts/recommend.py \
|
| 263 |
+
/path/to/your_structure.pdb \
|
| 264 |
+
--chain A \
|
| 265 |
+
--nogpu
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
# Output Description
|
| 269 |
+
|
| 270 |
+
Structural Evolution first generates a complete single-point deep mutational scanning sequence library for the target chain in the input structure, then uses ESM-IF1 to calculate the structure-conditioned log-likelihood of each candidate sequence.
|
| 271 |
+
|
| 272 |
+
If the following options are not specified manually:
|
| 273 |
+
|
| 274 |
+
```text
|
| 275 |
+
--seqpath
|
| 276 |
+
--outpath
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
the program automatically saves the results under `output/`.
|
| 280 |
+
|
| 281 |
+
The main outputs include:
|
| 282 |
+
|
| 283 |
+
```text
|
| 284 |
+
*.fasta
|
| 285 |
+
*.csv
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
The files are:
|
| 289 |
+
|
| 290 |
+
| Output | Description |
|
| 291 |
+
| --- | --- |
|
| 292 |
+
| DMS FASTA | Contains the wild-type sequence and all single-point mutation candidates |
|
| 293 |
+
| scores CSV | Stores candidate sequences and their corresponding `log_likelihood` scores |
|
| 294 |
+
| Terminal output | Top-N recommended mutations sorted by `log_likelihood` |
|
| 295 |
+
|
| 296 |
+
Candidate sequences in the CSV are sorted by:
|
| 297 |
+
|
| 298 |
+
```text
|
| 299 |
+
log_likelihood
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
in descending order, and recommended mutations are finally selected using parameters such as `n`, `maxrep`, and `upperbound`.
|
| 303 |
+
|
| 304 |
+
# Official OneScience Information
|
| 305 |
+
|
| 306 |
+
| Platform | Main OneScience repository | Skills repository |
|
| 307 |
+
| --- | --- | --- |
|
| 308 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 309 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 310 |
+
|
| 311 |
+
# Citation and License
|
| 312 |
+
|
| 313 |
+
- Original Structural Evolution paper: [Unsupervised evolution of protein and antibody complexes with a structure-informed language model](https://doi.org/10.1126/science.adk8946).
|
| 314 |
+
- Structural Evolution is released under the **MIT License**, which permits use, copying, modification, publication, distribution, sublicensing, and commercial use provided that the copyright notice and license text are retained. See `LICENSE` in the repository root.
|
| 315 |
+
- Structural Evolution inference depends on ESM-IF1 and related ESM code. The MIT License of the Structural Evolution repository does not automatically cover third-party model weights or dependency resources. For commercial use, redistribution, or other purposes, also review the applicable ESM/ESM-IF1 licenses and model-weight terms of use.
|
| 316 |
+
- If you use this repository in research, cite the original Structural Evolution paper and, as appropriate, the related work on ESM-IF1 and other components actually used.
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|
environment.yml
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: struct-evo
|
| 2 |
+
channels:
|
| 3 |
+
- pytorch
|
| 4 |
+
- pyg
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- _libgcc_mutex=0.1=main
|
| 9 |
+
- _openmp_mutex=5.1=1_gnu
|
| 10 |
+
- abseil-cpp=20211102.0=hd4dd3e8_0
|
| 11 |
+
- anyio=4.2.0=py39h06a4308_0
|
| 12 |
+
- argon2-cffi=21.3.0=pyhd3eb1b0_0
|
| 13 |
+
- argon2-cffi-bindings=21.2.0=py39h7f8727e_0
|
| 14 |
+
- arrow-cpp=14.0.2=h374c478_1
|
| 15 |
+
- asttokens=2.0.5=pyhd3eb1b0_0
|
| 16 |
+
- attrs=23.1.0=py39h06a4308_0
|
| 17 |
+
- aws-c-auth=0.6.19=h5eee18b_0
|
| 18 |
+
- aws-c-cal=0.5.20=hdbd6064_0
|
| 19 |
+
- aws-c-common=0.8.5=h5eee18b_0
|
| 20 |
+
- aws-c-compression=0.2.16=h5eee18b_0
|
| 21 |
+
- aws-c-event-stream=0.2.15=h6a678d5_0
|
| 22 |
+
- aws-c-http=0.6.25=h5eee18b_0
|
| 23 |
+
- aws-c-io=0.13.10=h5eee18b_0
|
| 24 |
+
- aws-c-mqtt=0.7.13=h5eee18b_0
|
| 25 |
+
- aws-c-s3=0.1.51=hdbd6064_0
|
| 26 |
+
- aws-c-sdkutils=0.1.6=h5eee18b_0
|
| 27 |
+
- aws-checksums=0.1.13=h5eee18b_0
|
| 28 |
+
- aws-crt-cpp=0.18.16=h6a678d5_0
|
| 29 |
+
- aws-sdk-cpp=1.10.55=h721c034_0
|
| 30 |
+
- backcall=0.2.0=pyhd3eb1b0_0
|
| 31 |
+
- beautifulsoup4=4.12.3=py39h06a4308_0
|
| 32 |
+
- blas=1.0=mkl
|
| 33 |
+
- bleach=4.1.0=pyhd3eb1b0_0
|
| 34 |
+
- bokeh=2.4.3=py39h06a4308_0
|
| 35 |
+
- boost-cpp=1.82.0=hdb19cb5_2
|
| 36 |
+
- bottleneck=1.3.7=py39ha9d4c09_0
|
| 37 |
+
- brotli=1.0.9=h5eee18b_8
|
| 38 |
+
- brotli-bin=1.0.9=h5eee18b_8
|
| 39 |
+
- brotli-python=1.0.9=py39h5a03fae_7
|
| 40 |
+
- bzip2=1.0.8=h5eee18b_6
|
| 41 |
+
- c-ares=1.19.1=h5eee18b_0
|
| 42 |
+
- ca-certificates=2024.3.11=h06a4308_0
|
| 43 |
+
- certifi=2024.6.2=py39h06a4308_0
|
| 44 |
+
- cffi=1.16.0=py39h5eee18b_1
|
| 45 |
+
- charset-normalizer=3.3.2=pyhd8ed1ab_0
|
| 46 |
+
- click=8.1.7=py39h06a4308_0
|
| 47 |
+
- cloudpickle=2.2.1=py39h06a4308_0
|
| 48 |
+
- colorama=0.4.6=pyhd8ed1ab_0
|
| 49 |
+
- colorcet=3.0.0=py39h06a4308_0
|
| 50 |
+
- comm=0.2.1=py39h06a4308_0
|
| 51 |
+
- contourpy=1.2.0=py39hdb19cb5_0
|
| 52 |
+
- cudatoolkit=11.3.1=h2bc3f7f_2
|
| 53 |
+
- cycler=0.11.0=pyhd3eb1b0_0
|
| 54 |
+
- cyrus-sasl=2.1.28=h52b45da_1
|
| 55 |
+
- cytoolz=0.12.2=py39h5eee18b_0
|
| 56 |
+
- dask=2023.11.0=py39h06a4308_0
|
| 57 |
+
- dask-core=2023.11.0=py39h06a4308_0
|
| 58 |
+
- datashader=0.14.1=py39h06a4308_1
|
| 59 |
+
- datashape=0.5.4=py39h06a4308_1
|
| 60 |
+
- dbus=1.13.18=hb2f20db_0
|
| 61 |
+
- debugpy=1.6.7=py39h6a678d5_0
|
| 62 |
+
- decorator=5.1.1=pyhd3eb1b0_0
|
| 63 |
+
- defusedxml=0.7.1=pyhd3eb1b0_0
|
| 64 |
+
- distributed=2023.11.0=py39h06a4308_0
|
| 65 |
+
- entrypoints=0.4=py39h06a4308_0
|
| 66 |
+
- et_xmlfile=1.1.0=py39h06a4308_0
|
| 67 |
+
- exceptiongroup=1.2.0=py39h06a4308_0
|
| 68 |
+
- executing=0.8.3=pyhd3eb1b0_0
|
| 69 |
+
- expat=2.6.2=h6a678d5_0
|
| 70 |
+
- fontconfig=2.14.1=h4c34cd2_2
|
| 71 |
+
- fonttools=4.51.0=py39h5eee18b_0
|
| 72 |
+
- freetype=2.12.1=h4a9f257_0
|
| 73 |
+
- fsspec=2024.3.1=py39h06a4308_0
|
| 74 |
+
- gflags=2.2.2=h6a678d5_1
|
| 75 |
+
- glib=2.78.4=h6a678d5_0
|
| 76 |
+
- glib-tools=2.78.4=h6a678d5_0
|
| 77 |
+
- glog=0.5.0=h6a678d5_1
|
| 78 |
+
- grpc-cpp=1.48.2=he1ff14a_1
|
| 79 |
+
- gst-plugins-base=1.14.1=h6a678d5_1
|
| 80 |
+
- gstreamer=1.14.1=h5eee18b_1
|
| 81 |
+
- heapdict=1.0.1=pyhd3eb1b0_0
|
| 82 |
+
- holoviews=1.15.0=py39h06a4308_1
|
| 83 |
+
- hvplot=0.8.0=py39h06a4308_0
|
| 84 |
+
- icu=73.1=h6a678d5_0
|
| 85 |
+
- idna=3.7=pyhd8ed1ab_0
|
| 86 |
+
- importlib-metadata=7.0.1=py39h06a4308_0
|
| 87 |
+
- importlib_resources=6.1.1=py39h06a4308_1
|
| 88 |
+
- intel-openmp=2023.1.0=hdb19cb5_46306
|
| 89 |
+
- ipykernel=6.28.0=py39h06a4308_0
|
| 90 |
+
- ipython=8.15.0=py39h06a4308_0
|
| 91 |
+
- ipython_genutils=0.2.0=pyhd3eb1b0_1
|
| 92 |
+
- jedi=0.18.1=py39h06a4308_1
|
| 93 |
+
- jinja2=2.11.3=pyhd8ed1ab_2
|
| 94 |
+
- joblib=1.4.2=pyhd8ed1ab_0
|
| 95 |
+
- jpeg=9e=h5eee18b_1
|
| 96 |
+
- jsonschema=4.19.2=py39h06a4308_0
|
| 97 |
+
- jsonschema-specifications=2023.7.1=py39h06a4308_0
|
| 98 |
+
- jupyter_client=7.4.9=py39h06a4308_0
|
| 99 |
+
- jupyter_core=5.7.2=py39h06a4308_0
|
| 100 |
+
- jupyter_events=0.10.0=py39h06a4308_0
|
| 101 |
+
- jupyter_server=2.10.0=py39h06a4308_0
|
| 102 |
+
- jupyter_server_terminals=0.4.4=py39h06a4308_1
|
| 103 |
+
- jupyterlab_pygments=0.2.2=py39h06a4308_0
|
| 104 |
+
- kiwisolver=1.4.4=py39h6a678d5_0
|
| 105 |
+
- krb5=1.20.1=h143b758_1
|
| 106 |
+
- lcms2=2.12=h3be6417_0
|
| 107 |
+
- ld_impl_linux-64=2.38=h1181459_1
|
| 108 |
+
- lerc=3.0=h295c915_0
|
| 109 |
+
- libboost=1.82.0=h109eef0_2
|
| 110 |
+
- libbrotlicommon=1.0.9=h5eee18b_8
|
| 111 |
+
- libbrotlidec=1.0.9=h5eee18b_8
|
| 112 |
+
- libbrotlienc=1.0.9=h5eee18b_8
|
| 113 |
+
- libclang=14.0.6=default_hc6dbbc7_1
|
| 114 |
+
- libclang13=14.0.6=default_he11475f_1
|
| 115 |
+
- libcups=2.4.2=h2d74bed_1
|
| 116 |
+
- libcurl=8.7.1=h251f7ec_0
|
| 117 |
+
- libdeflate=1.17=h5eee18b_1
|
| 118 |
+
- libedit=3.1.20230828=h5eee18b_0
|
| 119 |
+
- libev=4.33=h7f8727e_1
|
| 120 |
+
- libevent=2.1.12=hdbd6064_1
|
| 121 |
+
- libffi=3.4.4=h6a678d5_1
|
| 122 |
+
- libgcc-ng=11.2.0=h1234567_1
|
| 123 |
+
- libgfortran-ng=11.2.0=h00389a5_1
|
| 124 |
+
- libgfortran5=11.2.0=h1234567_1
|
| 125 |
+
- libglib=2.78.4=hdc74915_0
|
| 126 |
+
- libgomp=11.2.0=h1234567_1
|
| 127 |
+
- libiconv=1.16=h5eee18b_3
|
| 128 |
+
- libllvm14=14.0.6=hdb19cb5_3
|
| 129 |
+
- libnghttp2=1.57.0=h2d74bed_0
|
| 130 |
+
- libpng=1.6.39=h5eee18b_0
|
| 131 |
+
- libpq=12.17=hdbd6064_0
|
| 132 |
+
- libprotobuf=3.20.3=he621ea3_0
|
| 133 |
+
- libsodium=1.0.18=h7b6447c_0
|
| 134 |
+
- libssh2=1.11.0=h251f7ec_0
|
| 135 |
+
- libstdcxx-ng=11.2.0=h1234567_1
|
| 136 |
+
- libthrift=0.15.0=h1795dd8_2
|
| 137 |
+
- libtiff=4.5.1=h6a678d5_0
|
| 138 |
+
- libuuid=1.41.5=h5eee18b_0
|
| 139 |
+
- libuv=1.44.2=h5eee18b_0
|
| 140 |
+
- libwebp-base=1.3.2=h5eee18b_0
|
| 141 |
+
- libxcb=1.15=h7f8727e_0
|
| 142 |
+
- libxkbcommon=1.0.1=h5eee18b_1
|
| 143 |
+
- libxml2=2.10.4=hfdd30dd_2
|
| 144 |
+
- llvmlite=0.42.0=py39h6a678d5_0
|
| 145 |
+
- locket=1.0.0=py39h06a4308_0
|
| 146 |
+
- lz4=4.3.2=py39h5eee18b_0
|
| 147 |
+
- lz4-c=1.9.4=h6a678d5_1
|
| 148 |
+
- markdown=3.4.1=py39h06a4308_0
|
| 149 |
+
- markupsafe=1.1.1=py39h3811e60_3
|
| 150 |
+
- matplotlib=3.8.0=py39h06a4308_0
|
| 151 |
+
- matplotlib-base=3.8.0=py39h1128e8f_0
|
| 152 |
+
- matplotlib-inline=0.1.6=py39h06a4308_0
|
| 153 |
+
- mistune=0.8.4=py39h27cfd23_1000
|
| 154 |
+
- mkl=2023.1.0=h213fc3f_46344
|
| 155 |
+
- mkl-service=2.4.0=py39h5eee18b_1
|
| 156 |
+
- mkl_fft=1.3.8=py39h5eee18b_0
|
| 157 |
+
- mkl_random=1.2.4=py39hdb19cb5_0
|
| 158 |
+
- msgpack-python=1.0.3=py39hd09550d_0
|
| 159 |
+
- multipledispatch=0.6.0=py39h06a4308_0
|
| 160 |
+
- mysql=5.7.24=h721c034_2
|
| 161 |
+
- nbclassic=1.1.0=py39h06a4308_0
|
| 162 |
+
- nbclient=0.5.13=py39h06a4308_0
|
| 163 |
+
- nbconvert=6.4.4=py39h06a4308_0
|
| 164 |
+
- nbformat=5.9.2=py39h06a4308_0
|
| 165 |
+
- ncurses=6.4=h6a678d5_0
|
| 166 |
+
- nest-asyncio=1.6.0=py39h06a4308_0
|
| 167 |
+
- notebook=6.5.7=py39h06a4308_0
|
| 168 |
+
- notebook-shim=0.2.3=py39h06a4308_0
|
| 169 |
+
- numba=0.59.1=py39h6a678d5_0
|
| 170 |
+
- numexpr=2.8.7=py39h85018f9_0
|
| 171 |
+
- numpy=1.22.3=py39hf6e8229_2
|
| 172 |
+
- numpy-base=1.22.3=py39h060ed82_2
|
| 173 |
+
- openjpeg=2.4.0=h9ca470c_1
|
| 174 |
+
- openpyxl=3.0.10=py39h5eee18b_0
|
| 175 |
+
- openssl=3.0.14=h5eee18b_0
|
| 176 |
+
- orc=1.7.4=hb3bc3d3_1
|
| 177 |
+
- overrides=7.4.0=py39h06a4308_0
|
| 178 |
+
- packaging=23.2=py39h06a4308_0
|
| 179 |
+
- pandocfilters=1.5.0=pyhd3eb1b0_0
|
| 180 |
+
- panel=0.13.1=py39h06a4308_0
|
| 181 |
+
- param=1.12.0=pyhd3eb1b0_0
|
| 182 |
+
- parso=0.8.3=pyhd3eb1b0_0
|
| 183 |
+
- partd=1.4.1=py39h06a4308_0
|
| 184 |
+
- pcre2=10.42=hebb0a14_1
|
| 185 |
+
- pexpect=4.8.0=pyhd3eb1b0_3
|
| 186 |
+
- pickleshare=0.7.5=pyhd3eb1b0_1003
|
| 187 |
+
- pillow=10.3.0=py39h5eee18b_0
|
| 188 |
+
- pip=24.0=py39h06a4308_0
|
| 189 |
+
- platformdirs=3.10.0=py39h06a4308_0
|
| 190 |
+
- ply=3.11=py39h06a4308_0
|
| 191 |
+
- prometheus_client=0.14.1=py39h06a4308_0
|
| 192 |
+
- prompt-toolkit=3.0.43=py39h06a4308_0
|
| 193 |
+
- psutil=5.9.0=py39h5eee18b_0
|
| 194 |
+
- ptyprocess=0.7.0=pyhd3eb1b0_2
|
| 195 |
+
- pure_eval=0.2.2=pyhd3eb1b0_0
|
| 196 |
+
- pyarrow=14.0.2=py39h1eedbd7_0
|
| 197 |
+
- pybind11-abi=4=hd8ed1ab_3
|
| 198 |
+
- pycparser=2.21=pyhd3eb1b0_0
|
| 199 |
+
- pyct=0.5.0=py39h06a4308_0
|
| 200 |
+
- pyg=2.1.0=py39_torch_1.11.0_cu113
|
| 201 |
+
- pygments=2.15.1=py39h06a4308_1
|
| 202 |
+
- pyparsing=3.0.9=py39h06a4308_0
|
| 203 |
+
- pyqt=5.15.10=py39h6a678d5_0
|
| 204 |
+
- pyqt5-sip=12.13.0=py39h5eee18b_0
|
| 205 |
+
- pysocks=1.7.1=pyha2e5f31_6
|
| 206 |
+
- python=3.9.19=h955ad1f_1
|
| 207 |
+
- python-dateutil=2.9.0post0=py39h06a4308_2
|
| 208 |
+
- python-fastjsonschema=2.16.2=py39h06a4308_0
|
| 209 |
+
- python-json-logger=2.0.7=py39h06a4308_0
|
| 210 |
+
- python-lmdb=1.4.1=py39h6a678d5_0
|
| 211 |
+
- python_abi=3.9=2_cp39
|
| 212 |
+
- pytorch=1.11.0=py3.9_cuda11.3_cudnn8.2.0_0
|
| 213 |
+
- pytorch-cluster=1.6.0=py39_torch_1.11.0_cu113
|
| 214 |
+
- pytorch-mutex=1.0=cuda
|
| 215 |
+
- pytorch-scatter=2.0.9=py39_torch_1.11.0_cu113
|
| 216 |
+
- pytorch-sparse=0.6.15=py39_torch_1.11.0_cu113
|
| 217 |
+
- pytz=2024.1=py39h06a4308_0
|
| 218 |
+
- pyviz_comms=3.0.2=py39h06a4308_0
|
| 219 |
+
- pyyaml=6.0.1=py39h5eee18b_0
|
| 220 |
+
- pyzmq=24.0.1=py39h5eee18b_0
|
| 221 |
+
- qt-main=5.15.2=h53bd1ea_10
|
| 222 |
+
- re2=2022.04.01=h295c915_0
|
| 223 |
+
- readline=8.2=h5eee18b_0
|
| 224 |
+
- referencing=0.30.2=py39h06a4308_0
|
| 225 |
+
- requests=2.32.3=pyhd8ed1ab_0
|
| 226 |
+
- rfc3339-validator=0.1.4=py39h06a4308_0
|
| 227 |
+
- rfc3986-validator=0.1.1=py39h06a4308_0
|
| 228 |
+
- rpds-py=0.10.6=py39hb02cf49_0
|
| 229 |
+
- s2n=1.3.27=hdbd6064_0
|
| 230 |
+
- scikit-learn=1.4.2=py39h1128e8f_1
|
| 231 |
+
- scipy=1.11.4=py39h5f9d8c6_0
|
| 232 |
+
- send2trash=1.8.2=py39h06a4308_0
|
| 233 |
+
- setuptools=69.5.1=py39h06a4308_0
|
| 234 |
+
- sip=6.7.12=py39h6a678d5_0
|
| 235 |
+
- six=1.16.0=pyhd3eb1b0_1
|
| 236 |
+
- snappy=1.1.10=h6a678d5_1
|
| 237 |
+
- sniffio=1.3.0=py39h06a4308_0
|
| 238 |
+
- sortedcontainers=2.4.0=pyhd3eb1b0_0
|
| 239 |
+
- soupsieve=2.5=py39h06a4308_0
|
| 240 |
+
- sqlite=3.45.3=h5eee18b_0
|
| 241 |
+
- stack_data=0.2.0=pyhd3eb1b0_0
|
| 242 |
+
- tbb=2021.8.0=hdb19cb5_0
|
| 243 |
+
- tblib=1.7.0=pyhd3eb1b0_0
|
| 244 |
+
- terminado=0.17.1=py39h06a4308_0
|
| 245 |
+
- testpath=0.6.0=py39h06a4308_0
|
| 246 |
+
- threadpoolctl=3.5.0=pyhc1e730c_0
|
| 247 |
+
- tk=8.6.14=h39e8969_0
|
| 248 |
+
- tomli=2.0.1=py39h06a4308_0
|
| 249 |
+
- toolz=0.12.0=py39h06a4308_0
|
| 250 |
+
- tornado=6.4.1=py39h5eee18b_0
|
| 251 |
+
- tqdm=4.66.4=pyhd8ed1ab_0
|
| 252 |
+
- traitlets=5.14.3=py39h06a4308_0
|
| 253 |
+
- typing-extensions=4.11.0=py39h06a4308_0
|
| 254 |
+
- typing_extensions=4.11.0=py39h06a4308_0
|
| 255 |
+
- unicodedata2=15.1.0=py39h5eee18b_0
|
| 256 |
+
- urllib3=2.2.2=pyhd8ed1ab_0
|
| 257 |
+
- utf8proc=2.6.1=h5eee18b_1
|
| 258 |
+
- wcwidth=0.2.5=pyhd3eb1b0_0
|
| 259 |
+
- webencodings=0.5.1=py39h06a4308_1
|
| 260 |
+
- websocket-client=1.8.0=py39h06a4308_0
|
| 261 |
+
- wheel=0.43.0=py39h06a4308_0
|
| 262 |
+
- xarray=2023.6.0=py39h06a4308_0
|
| 263 |
+
- xz=5.4.6=h5eee18b_1
|
| 264 |
+
- yaml=0.2.5=h7b6447c_0
|
| 265 |
+
- zeromq=4.3.5=h6a678d5_0
|
| 266 |
+
- zict=3.0.0=py39h06a4308_0
|
| 267 |
+
- zipp=3.17.0=py39h06a4308_0
|
| 268 |
+
- zlib=1.2.13=h5eee18b_1
|
| 269 |
+
- zstd=1.5.5=hc292b87_2
|
| 270 |
+
- pip:
|
| 271 |
+
- ablang==0.3.1
|
| 272 |
+
- biopython==1.78
|
| 273 |
+
- biotite==0.34.1
|
| 274 |
+
- git+https://github.com/facebookresearch/esm.git
|
| 275 |
+
- iqplot==0.3.3
|
| 276 |
+
- msgpack==1.0.8
|
| 277 |
+
- natsort==8.4.0
|
| 278 |
+
- networkx==3.2.1
|
| 279 |
+
- pandas==2.0.3
|
| 280 |
+
- tzdata==2024.1
|
| 281 |
+
prefix: /data/home/vrs/miniconda3/envs/struct-evo
|
model/dms_enrichment.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from biotite.sequence.io.fasta import FastaFile, get_sequences
|
| 4 |
+
from Bio import pairwise2
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import shutil
|
| 7 |
+
import os
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import bokeh.io
|
| 11 |
+
import bokeh.plotting
|
| 12 |
+
import bokeh.palettes
|
| 13 |
+
from bokeh.transform import factor_cmap
|
| 14 |
+
|
| 15 |
+
from bokeh.io import export_svg
|
| 16 |
+
import iqplot
|
| 17 |
+
from colorcet import glasbey_category10
|
| 18 |
+
|
| 19 |
+
import subprocess
|
| 20 |
+
|
| 21 |
+
bla_dict = {
|
| 22 |
+
'protein':'bla',
|
| 23 |
+
'pdbfile':'bla_1m40_a.pdb',
|
| 24 |
+
'chain':'A',
|
| 25 |
+
'dmsfile': 'dms_bla.csv',
|
| 26 |
+
'fitness_col': 'DMS_amp_2500_(b)',
|
| 27 |
+
'threshold' : 0.01
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
calm1_dict = {
|
| 31 |
+
'protein':'CALM1',
|
| 32 |
+
'pdbfile':'calm1_5v03_r.pdb',
|
| 33 |
+
'chain':'R',
|
| 34 |
+
'dmsfile': 'dms_calm1.csv',
|
| 35 |
+
'fitness_col':'DMS',
|
| 36 |
+
'threshold': 1
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
haeiiim_dict = {
|
| 40 |
+
'protein':'haeIIIM',
|
| 41 |
+
'pdbfile':'haeiiim_3ubt_b.pdb',
|
| 42 |
+
'chain':'B',
|
| 43 |
+
'dmsfile': 'dms_haeiiim.csv',
|
| 44 |
+
'fitness_col':'DMS_G3',
|
| 45 |
+
'threshold' : 0.01
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
gal4_dict = {
|
| 49 |
+
'protein':'GAL4',
|
| 50 |
+
'pdbfile':'gal4_3coq_b.pdb',
|
| 51 |
+
'chain':'B',
|
| 52 |
+
'dmsfile': 'dms_gal4.csv',
|
| 53 |
+
'fitness_col':'DMS_nonsel_24',
|
| 54 |
+
'threshold': 1
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
hras_dict = {
|
| 58 |
+
'protein':'HRAS',
|
| 59 |
+
'pdbfile':'hras_2ce2_x.pdb',
|
| 60 |
+
'chain':'X',
|
| 61 |
+
'dmsfile': 'dms_hras.csv',
|
| 62 |
+
'fitness_col':'DMS_unregulated',
|
| 63 |
+
'threshold': 1
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
mapk1_dict = {
|
| 67 |
+
'protein':'MAPK1',
|
| 68 |
+
'pdbfile':'mapk1_4zzn_a.pdb',
|
| 69 |
+
'chain':'A',
|
| 70 |
+
'dmsfile': 'dms_mapk1.csv',
|
| 71 |
+
'fitness_col':'DMS_VRT',
|
| 72 |
+
'threshold': 1
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
tpk1_dict = {
|
| 76 |
+
'protein':'TPK1',
|
| 77 |
+
'pdbfile':'tpk1_3s4y_a.pdb',
|
| 78 |
+
'chain':'A',
|
| 79 |
+
'dmsfile': 'dms_tpk1.csv',
|
| 80 |
+
'fitness_col':'DMS',
|
| 81 |
+
'threshold': 1
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
tpmt_dict = {
|
| 85 |
+
'protein':'TPMT',
|
| 86 |
+
'pdbfile':'tpmt_2bzg_a.pdb',
|
| 87 |
+
'chain':'A',
|
| 88 |
+
'dmsfile': 'dms_tpmt.csv',
|
| 89 |
+
'fitness_col':'DMS',
|
| 90 |
+
'threshold': 1
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
ube2i_dict = {
|
| 94 |
+
'protein':'UBE2I',
|
| 95 |
+
'pdbfile':'ube2i_5f6e_a.pdb',
|
| 96 |
+
'chain':'A',
|
| 97 |
+
'dmsfile': 'dms_tpmt.csv',
|
| 98 |
+
'fitness_col':'DMS',
|
| 99 |
+
'threshold': 1
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
ubi4_dict = {
|
| 103 |
+
'protein':'UBI4',
|
| 104 |
+
'pdbfile':'ubi4_4q5e_b.pdb',
|
| 105 |
+
'chain':'B',
|
| 106 |
+
'dmsfile': 'dms_ubi4.csv',
|
| 107 |
+
'fitness_col':'DMS_limiting_(b)',
|
| 108 |
+
'threshold': 1
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
studies = [
|
| 112 |
+
bla_dict,
|
| 113 |
+
calm1_dict,
|
| 114 |
+
gal4_dict,
|
| 115 |
+
haeiiim_dict,
|
| 116 |
+
hras_dict,
|
| 117 |
+
tpmt_dict,
|
| 118 |
+
tpk1_dict,
|
| 119 |
+
mapk1_dict,
|
| 120 |
+
ube2i_dict,
|
| 121 |
+
ubi4_dict,
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
def score_if(prot_dict):
|
| 125 |
+
prefix = 'data/dms/'
|
| 126 |
+
fasta_outpath = f"dms_libs/{prot_dict['pdbfile'][:-4]}_dmsLib.fasta"
|
| 127 |
+
score_if_cmd = [
|
| 128 |
+
'python',
|
| 129 |
+
'model/score_log_likelihoods.py',
|
| 130 |
+
prefix + f"structures/{prot_dict['pdbfile']}",
|
| 131 |
+
'--seqpath', prefix + fasta_outpath,
|
| 132 |
+
'--outpath', f"output/dms/if_results/dms_{prot_dict['protein']}_if.csv"
|
| 133 |
+
,'--chain', prot_dict['chain']
|
| 134 |
+
]
|
| 135 |
+
subprocess.run(score_if_cmd, check=True)
|
| 136 |
+
|
| 137 |
+
def get_stats(prot_dict, n, top_per):
|
| 138 |
+
|
| 139 |
+
top_per = top_per/100
|
| 140 |
+
prefix = 'output/dms/'
|
| 141 |
+
esm1v_outpath = 'dms_esm1vScored/' + f"dms_{prot_dict['protein']}" + '_maskMarginals.csv'
|
| 142 |
+
if_results_df = pd.read_csv( prefix + f"if_results/dms_{prot_dict['protein']}_if.csv")
|
| 143 |
+
dms_df = pd.read_csv(prefix + esm1v_outpath)
|
| 144 |
+
|
| 145 |
+
unprofiled_df = dms_df[dms_df[prot_dict['fitness_col']].isna()]
|
| 146 |
+
unprofiled_variants = unprofiled_df['variant'].to_list()
|
| 147 |
+
|
| 148 |
+
dms_df = dms_df.dropna(subset = [prot_dict['fitness_col']])
|
| 149 |
+
pop_n = len(dms_df)
|
| 150 |
+
if_results_df = if_results_df[~if_results_df['seqid'].isin(unprofiled_variants)]
|
| 151 |
+
|
| 152 |
+
assert (len(if_results_df) == len(dms_df))
|
| 153 |
+
|
| 154 |
+
threshold = dms_df[prot_dict['fitness_col']].quantile(1-top_per)
|
| 155 |
+
highFit_subset_variants = dms_df[dms_df[prot_dict['fitness_col']] >= threshold]['variant']
|
| 156 |
+
n_highFit = len(highFit_subset_variants)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
dms_df['esm1v'] = dms_df.loc[:, dms_df.columns.str.startswith('esm1v')].mean(axis=1)
|
| 160 |
+
top_esm1v_df = dms_df.sort_values(by = 'esm1v', ascending = False)[:n]
|
| 161 |
+
top_esm1v_variants = top_esm1v_df['variant'].to_list()
|
| 162 |
+
|
| 163 |
+
top_if_df = if_results_df.sort_values(by = 'log_likelihood', ascending = False)[:n]
|
| 164 |
+
top_if_variants = top_if_df['seqid'].to_list()
|
| 165 |
+
|
| 166 |
+
esm1v_hits_df = top_esm1v_df[top_esm1v_df['variant'].isin(highFit_subset_variants)]
|
| 167 |
+
if_hits_df = top_if_df[top_if_df['seqid'].isin(highFit_subset_variants)]
|
| 168 |
+
|
| 169 |
+
n_esm1v_hits = len(esm1v_hits_df)
|
| 170 |
+
n_if_hits = len(if_hits_df)
|
| 171 |
+
|
| 172 |
+
if_hit_enrich = (n_if_hits / n) / top_per
|
| 173 |
+
esm1v_hit_enrich = (n_esm1v_hits / n) / top_per
|
| 174 |
+
|
| 175 |
+
if_hit_rate = (n_if_hits / n)
|
| 176 |
+
esm1v_hit_rate = (n_esm1v_hits / n)
|
| 177 |
+
|
| 178 |
+
return (prot_dict['protein'], pop_n, n_highFit, n_if_hits, n_esm1v_hits, if_hit_enrich, esm1v_hit_enrich, if_hit_rate, esm1v_hit_rate )
|
| 179 |
+
|
| 180 |
+
def plot_bars(melted_df):
|
| 181 |
+
p = bokeh.plotting.figure(
|
| 182 |
+
height=350,
|
| 183 |
+
width=1100,
|
| 184 |
+
y_axis_label="High Fitness \n Prediction Precision",
|
| 185 |
+
x_axis_label = "Functional Percentile Threshold \n for High Fitness Classification",
|
| 186 |
+
x_range=bokeh.models.FactorRange(*factors, group_padding = 1.2),
|
| 187 |
+
tools="save",
|
| 188 |
+
title=''
|
| 189 |
+
)
|
| 190 |
+
p.output_backend = "svg"
|
| 191 |
+
|
| 192 |
+
p.vbar(
|
| 193 |
+
source=melted_df,
|
| 194 |
+
x="cats",
|
| 195 |
+
top="Hit Rate",
|
| 196 |
+
width = 1,
|
| 197 |
+
line_color='black',
|
| 198 |
+
alpha = 'alpha',
|
| 199 |
+
legend_field='legend_label',
|
| 200 |
+
fill_color=bokeh.transform.factor_cmap(
|
| 201 |
+
'Method',
|
| 202 |
+
palette=['#999999', '#43a2ca'],
|
| 203 |
+
factors=list(melted_df['Method'].unique()),
|
| 204 |
+
start=1,
|
| 205 |
+
end=2
|
| 206 |
+
)
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
p.xgrid.grid_line_color = None
|
| 210 |
+
p.ygrid.grid_line_color = None
|
| 211 |
+
p.x_range.range_padding = 0.03
|
| 212 |
+
p.legend.location = "top_right"
|
| 213 |
+
p.legend.spacing = 20
|
| 214 |
+
p.legend.label_text_font_size = "11pt"
|
| 215 |
+
p.legend.label_text_color = "black"
|
| 216 |
+
p.legend.orientation = "horizontal"
|
| 217 |
+
p.xaxis.major_label_orientation = 1.2
|
| 218 |
+
p.xaxis.separator_line_alpha = 0
|
| 219 |
+
p.xaxis.group_text_font_size = '11pt'
|
| 220 |
+
p.xaxis.subgroup_text_font_size = '0pt'
|
| 221 |
+
p.xaxis.axis_label_text_font_size = '11pt'
|
| 222 |
+
p.xaxis.major_label_text_font_size = '10pt'
|
| 223 |
+
p.xaxis.axis_label_text_font_style = 'normal'
|
| 224 |
+
p.xaxis.major_label_text_color = 'black'
|
| 225 |
+
p.xaxis.axis_label_text_color = 'black'
|
| 226 |
+
|
| 227 |
+
p.yaxis.axis_label_text_font_size = '11pt'
|
| 228 |
+
p.yaxis.axis_label_text_font_style = 'normal'
|
| 229 |
+
p.yaxis.axis_label_text_color = 'black'
|
| 230 |
+
p.yaxis.major_label_text_font_size = '10pt'
|
| 231 |
+
p.xaxis.group_text_color = 'dimgrey'
|
| 232 |
+
|
| 233 |
+
return p
|
| 234 |
+
|
| 235 |
+
def plot_ecdf_hits(prot_dict, n, top_per_lst):
|
| 236 |
+
|
| 237 |
+
#use least stringent threshold to plot all hits
|
| 238 |
+
top_per = max(top_per_lst)/100
|
| 239 |
+
prefix = 'output/dms/'
|
| 240 |
+
|
| 241 |
+
esm1v_outpath = 'dms_esm1vScored/' + f"dms_{prot_dict['protein']}" + '_maskMarginals.csv'
|
| 242 |
+
if_results_df = pd.read_csv( prefix + f"if_results/dms_{prot_dict['protein']}_if.csv")
|
| 243 |
+
dms_df = pd.read_csv(prefix + esm1v_outpath)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
unprofiled_df = dms_df[dms_df[prot_dict['fitness_col']].isna()]
|
| 247 |
+
unprofiled_variants = unprofiled_df['variant'].to_list()
|
| 248 |
+
|
| 249 |
+
dms_df = dms_df.dropna(subset = [prot_dict['fitness_col']])
|
| 250 |
+
dms_df['percentile'] = dms_df[prot_dict['fitness_col']].rank(pct=True, method='average')
|
| 251 |
+
dms_df['zscore'] = (dms_df[prot_dict['fitness_col']] - dms_df[prot_dict['fitness_col']].mean())/dms_df[prot_dict['fitness_col']].std()
|
| 252 |
+
if_results_df = if_results_df[~if_results_df['seqid'].isin(unprofiled_variants)]
|
| 253 |
+
|
| 254 |
+
assert (len(if_results_df) == len(dms_df))
|
| 255 |
+
|
| 256 |
+
threshold = dms_df[prot_dict['fitness_col']].quantile(1-top_per)
|
| 257 |
+
highFit_subset_variants = dms_df[dms_df[prot_dict['fitness_col']] >= threshold]['variant']
|
| 258 |
+
n_highFit = len(highFit_subset_variants)
|
| 259 |
+
|
| 260 |
+
dms_df['esm1v'] = dms_df.loc[:, dms_df.columns.str.startswith('esm1v')].mean(axis=1)
|
| 261 |
+
top_esm1v_df = dms_df.sort_values(by = 'esm1v', ascending = False)[:n]
|
| 262 |
+
top_esm1v_variants = top_esm1v_df['variant'].to_list()
|
| 263 |
+
|
| 264 |
+
top_if_df = if_results_df.sort_values(by = 'log_likelihood', ascending = False)[:n]
|
| 265 |
+
top_if_variants = top_if_df['seqid'].to_list()
|
| 266 |
+
|
| 267 |
+
esm1v_hits_df = top_esm1v_df[top_esm1v_df['variant'].isin(highFit_subset_variants)]
|
| 268 |
+
if_hits_df = top_if_df[top_if_df['seqid'].isin(highFit_subset_variants)]
|
| 269 |
+
if_hits_df = pd.merge(if_hits_df, dms_df[['variant', 'zscore', 'percentile', prot_dict['fitness_col']]], left_on='seqid', right_on='variant', how='left')
|
| 270 |
+
|
| 271 |
+
p_1v = iqplot.ecdf(
|
| 272 |
+
data=dms_df,
|
| 273 |
+
q=prot_dict['fitness_col'],
|
| 274 |
+
style="staircase",
|
| 275 |
+
palette = 'lightgrey',
|
| 276 |
+
line_kwargs= {'line_width':6},
|
| 277 |
+
x_axis_label = prot_dict['protein']+' Fitness Measure',
|
| 278 |
+
#title = prot_dict['protein']
|
| 279 |
+
)
|
| 280 |
+
p_if = iqplot.ecdf(
|
| 281 |
+
data=dms_df,
|
| 282 |
+
q=prot_dict['fitness_col'],
|
| 283 |
+
style="staircase",
|
| 284 |
+
palette = 'lightgrey',
|
| 285 |
+
line_kwargs= {'line_width':6},
|
| 286 |
+
x_axis_label = None, #prot_dict['protein']+'Fitness Measure',
|
| 287 |
+
title = prot_dict['protein']
|
| 288 |
+
)
|
| 289 |
+
colors = ['#1f78b4', '#33a02c', '#ff7f00',]
|
| 290 |
+
for i, top_per in enumerate(top_per_lst):
|
| 291 |
+
top_per = top_per/100
|
| 292 |
+
hline = bokeh.models.Span(location=1-top_per, dimension='width', line_color=colors[i], line_dash='dashed', line_width=4, )
|
| 293 |
+
p_1v.add_layout(hline)
|
| 294 |
+
p_if.add_layout(hline)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
p_1v.circle(
|
| 298 |
+
source = esm1v_hits_df,
|
| 299 |
+
x = prot_dict['fitness_col'],
|
| 300 |
+
y = 'percentile',
|
| 301 |
+
color = 'dimgrey',
|
| 302 |
+
size = 11,
|
| 303 |
+
line_color = 'lightgrey',
|
| 304 |
+
alpha = 0.8,
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
p_if.circle(
|
| 308 |
+
source = if_hits_df,
|
| 309 |
+
x = prot_dict['fitness_col'],
|
| 310 |
+
y = 'percentile',
|
| 311 |
+
color = '#43a2ca',
|
| 312 |
+
size = 11,
|
| 313 |
+
line_color = 'white',
|
| 314 |
+
alpha = 0.8,
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
p_if.title.text_font_style = 'normal'
|
| 318 |
+
p_if.title.text_color = 'black'
|
| 319 |
+
p_if.title.align = 'center'
|
| 320 |
+
p_if.title.text_font_size = '16pt'
|
| 321 |
+
p_1v.xaxis.axis_label_text_font_size = '14pt'
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
for p in [p_if, p_1v]:
|
| 325 |
+
p.xaxis.axis_label_text_font_style = 'normal'
|
| 326 |
+
p.yaxis.axis_label_text_font_style = 'normal'
|
| 327 |
+
p.xaxis.axis_label_text_font_size = '14pt'
|
| 328 |
+
p.yaxis.axis_label_text_font_size = '14pt'
|
| 329 |
+
p.xaxis.axis_label_text_color = 'black'
|
| 330 |
+
p.yaxis.axis_label_text_color = 'black'
|
| 331 |
+
p.xaxis.major_label_text_font_size = '14pt'
|
| 332 |
+
p.yaxis.major_label_text_font_size = '14pt'
|
| 333 |
+
p.xaxis.major_label_text_color = 'black'
|
| 334 |
+
p.yaxis.major_label_text_color = 'black'
|
| 335 |
+
p.output_backend = "svg"
|
| 336 |
+
|
| 337 |
+
return [p_if, p_1v]
|
| 338 |
+
|
| 339 |
+
if __name__ == '__main__':
|
| 340 |
+
for s in studies:
|
| 341 |
+
score_if(s)
|
| 342 |
+
shutil.copytree("data/dms/dms_esm1vScored", "output/dms/dms_esm1vScored", dirs_exist_ok=True)
|
| 343 |
+
|
| 344 |
+
name, n_pop, n_popHits, n_if_hits, n_esm1v_hits, if_hit_enrich, esm1v_hit_enrich, if_hit_rate, esm1v_hit_rate, p_threshold = ([] for _ in range(10))
|
| 345 |
+
data_lists = [name, n_pop, n_popHits, n_if_hits, n_esm1v_hits, if_hit_enrich, esm1v_hit_enrich, if_hit_rate, esm1v_hit_rate, p_threshold]
|
| 346 |
+
for s in studies:
|
| 347 |
+
for p in [5, 10, 20 ]:
|
| 348 |
+
outputs = get_stats(s, 10, p)
|
| 349 |
+
for i in range(len(outputs)):
|
| 350 |
+
data_lists[i].append(outputs[i])
|
| 351 |
+
data_lists[len(data_lists)-1].append(p)
|
| 352 |
+
col_names = ['Protein', 'Total Library Size', 'Library Hits (Variants with Fitness >95th)', 'Inverse Folding Hits (in Top 10)', 'ESM1v Hits (in Top 10)', 'Inverse Folding Hit Enrichment', 'ESM1v Hit Enrichment','Inverse Folding Hit Rate', 'ESM1v Hit Rate','Percentile Threshold' ]
|
| 353 |
+
results_df = pd.DataFrame({col_names[i]: data_lists[i] for i in range(len(data_lists))})
|
| 354 |
+
# Melt the dataframe
|
| 355 |
+
melted_df = results_df.melt(id_vars=['Protein', 'Percentile Threshold'], value_vars=[ 'ESM1v Hit Rate', 'Inverse Folding Hit Rate',],
|
| 356 |
+
var_name='Method', value_name='Hit Rate')
|
| 357 |
+
melted_df['Method'] = melted_df['Method'].replace({
|
| 358 |
+
'Inverse Folding Hit Rate': 'Inverse Folding',
|
| 359 |
+
'ESM1v Hit Rate': 'Language Model'})
|
| 360 |
+
melted_df['alpha'] = (1/melted_df['Percentile Threshold'].to_numpy())*3 + 0.4
|
| 361 |
+
melted_df['Percentile Threshold'] = melted_df['Percentile Threshold'].astype(str)
|
| 362 |
+
melted_df['legend_label'] = ['Structure-Informed Language Model' if x=='Inverse Folding' else 'Language Model' for x in melted_df['Method']]
|
| 363 |
+
|
| 364 |
+
melted_df['cats'] = melted_df.apply(lambda x: (x["Protein"], x["Method"], x['Percentile Threshold'],), axis = 1)
|
| 365 |
+
factors = list(melted_df.cats)
|
| 366 |
+
|
| 367 |
+
p = plot_bars(melted_df)
|
| 368 |
+
comparePrecision_fname = 'output/dms/precision_comparison.html'
|
| 369 |
+
bokeh.plotting.output_file(comparePrecision_fname)
|
| 370 |
+
bokeh.io.show(p)
|
| 371 |
+
|
| 372 |
+
ecdf_plots = []
|
| 373 |
+
for s in studies:
|
| 374 |
+
ecdf_plots.extend(plot_ecdf_hits(s, 10, [20,10,5]))
|
| 375 |
+
mid = int(len(ecdf_plots)/2)
|
| 376 |
+
ecdf_fname = 'output/dms/fitness_ecdfs.html'
|
| 377 |
+
bokeh.plotting.output_file(ecdf_fname)
|
| 378 |
+
bokeh.io.show(bokeh.layouts.gridplot([ecdf_plots[:mid:2],
|
| 379 |
+
ecdf_plots[1:mid:2],
|
| 380 |
+
ecdf_plots[mid::2],
|
| 381 |
+
ecdf_plots[mid+1::2]])
|
| 382 |
+
)
|
model/dms_utils.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import Counter
|
| 2 |
+
import datetime
|
| 3 |
+
from dateutil.parser import parse as dparse
|
| 4 |
+
import errno
|
| 5 |
+
import numpy as np
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
import sys
|
| 9 |
+
import time
|
| 10 |
+
import warnings
|
| 11 |
+
from Bio import pairwise2
|
| 12 |
+
from Bio import BiopythonWarning
|
| 13 |
+
warnings.simplefilter('ignore', BiopythonWarning)
|
| 14 |
+
from Bio import Seq, SeqIO
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
np.random.seed(1)
|
| 18 |
+
random.seed(1)
|
| 19 |
+
|
| 20 |
+
AAs = [
|
| 21 |
+
'A', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L',
|
| 22 |
+
'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'Y',
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
def tprint(string):
|
| 26 |
+
string = str(string)
|
| 27 |
+
sys.stdout.write(str(datetime.datetime.now()) + ' | ')
|
| 28 |
+
sys.stdout.write(string + '\n')
|
| 29 |
+
sys.stdout.flush()
|
| 30 |
+
|
| 31 |
+
def mkdir_p(path):
|
| 32 |
+
try:
|
| 33 |
+
os.makedirs(path)
|
| 34 |
+
except OSError as exc: # Python >2.5
|
| 35 |
+
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
| 36 |
+
pass
|
| 37 |
+
else:
|
| 38 |
+
raise
|
| 39 |
+
|
| 40 |
+
def deep_mutational_scan(sequence, exclude_noop=True):
|
| 41 |
+
for pos, wt in enumerate(sequence):
|
| 42 |
+
for mt in AAs:
|
| 43 |
+
if exclude_noop and wt == mt:
|
| 44 |
+
continue
|
| 45 |
+
yield (pos, wt, mt)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def make_mutations(seq, mutations):
|
| 49 |
+
mut_seq = [ char for char in seq ]
|
| 50 |
+
for mutation in mutations:
|
| 51 |
+
wt, pos, mt = mutation[0], int(mutation[1:-1]) - 1, mutation[-1]
|
| 52 |
+
assert(seq[pos] == wt)
|
| 53 |
+
mut_seq[pos] = mt
|
| 54 |
+
mut_seq = ''.join(mut_seq).replace('-', '')
|
| 55 |
+
return mut_seq
|
| 56 |
+
|
| 57 |
+
def find_mutations(seq1, seq2):
|
| 58 |
+
alignment = pairwise2.align.globalms(
|
| 59 |
+
seq1, seq2, 5, -4, -6, -.1, one_alignment_only=True,
|
| 60 |
+
)[0]
|
| 61 |
+
|
| 62 |
+
mutation_set = []
|
| 63 |
+
pos1, pos2, pos_map = 0, 0, {}
|
| 64 |
+
for wt, mt in zip(alignment[0], alignment[1]):
|
| 65 |
+
if wt != '-':
|
| 66 |
+
pos1 += 1
|
| 67 |
+
if mt != '-':
|
| 68 |
+
pos2 += 1
|
| 69 |
+
if wt != mt and wt != '-' and mt != '-':
|
| 70 |
+
mut_str = f'{wt}{pos1}{mt}'
|
| 71 |
+
mutation_set.append(mut_str)
|
| 72 |
+
|
| 73 |
+
return mutation_set
|
model/eval_ablang.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ablang
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import scipy.special
|
| 5 |
+
|
| 6 |
+
heavy_ablang = ablang.pretrained("heavy")
|
| 7 |
+
heavy_ablang.freeze()
|
| 8 |
+
|
| 9 |
+
light_ablang = ablang.pretrained("light")
|
| 10 |
+
light_ablang.freeze()
|
| 11 |
+
|
| 12 |
+
ab_dict = dict(
|
| 13 |
+
cr6261= ('EVQLVESGAEVKKPGSSVKVSCKASGGPFRSYAISWVRQAPGQGPEWMGGIIPIFGTTKYAPKFQGRVTITADDFAGTVYMELSSLRSEDTAMYYCAKHMGYQVRETMDVWGKGTTVTVSS',
|
| 14 |
+
'QSVLTQPPSVSAAPGQKVTISCSGSSSNIGNDYVSWYQQLPGTAPKLLIYDNNKRPSGIPDRFSGSKSGTSATLGITGLQTGDEANYYCATWDRRPTAYVVFGGGTKLTVL'),
|
| 15 |
+
cr9114= ('QVQLVQSGAEVKKPGSSVKVSCKSSGGTSNNYAISWVRQAPGQGLDWMGGISPIFGSTAYAQKFQGRVTISADIFSNTAYMELNSLTSEDTAVYFCARHGNYYYYSGMDVWGQGTTVTVSS',
|
| 16 |
+
'QSALTQPPAVSGTPGQRVTISCSGSDSNIGRRSVNWYQQFPGTAPKLLIYSNDQRPSVVPDRFSGSKSGTSASLAISGLQSEDEAEYYCAAWDDSLKGAVFGGGTQLTVL'),
|
| 17 |
+
g6 = ('EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTV',
|
| 18 |
+
'DIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIK')
|
| 19 |
+
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
def eval_ablang(s, ab, chain):
|
| 23 |
+
if chain == 'hc':
|
| 24 |
+
fname = 'output/ab_mutagenesis_expts/'+ab+'/' + ab +'_hc_ablangScores.csv'
|
| 25 |
+
log_likelihoods = heavy_ablang(s, mode = 'likelihood')[0][1:-1]
|
| 26 |
+
alphabet = heavy_ablang.tokenizer.vocab_to_aa
|
| 27 |
+
elif chain == 'lc':
|
| 28 |
+
fname = 'output/ab_mutagenesis_expts/'+ab+'/' + ab +'_lc_ablangScores.csv'
|
| 29 |
+
log_likelihoods = light_ablang(s, mode = 'likelihood')[0][1:-1]
|
| 30 |
+
alphabet = light_ablang.tokenizer.vocab_to_aa
|
| 31 |
+
|
| 32 |
+
assert (log_likelihoods.shape)[0] == len(s)
|
| 33 |
+
|
| 34 |
+
filt_alphabet = {key: value for key, value in alphabet.items() if value.isalpha()}
|
| 35 |
+
log_likelihood_ratio = []
|
| 36 |
+
for i,res_log_likelihoods in enumerate(log_likelihoods):
|
| 37 |
+
wt_res = s[i]
|
| 38 |
+
wt_index = list(filt_alphabet.values()).index(wt_res)
|
| 39 |
+
wt_log_likelihood = res_log_likelihoods[wt_index]
|
| 40 |
+
log_likelihood_ratio.extend(res_log_likelihoods-wt_log_likelihood)
|
| 41 |
+
|
| 42 |
+
res_order = [alphabet[key] for key in range(1, 21)] #extract order of residues in likelihood
|
| 43 |
+
mt = res_order * len(s)
|
| 44 |
+
wt = [char for char in s for i in range(len(res_order))]
|
| 45 |
+
pos = [i+1 for i in range(len(s)) for j in range(len(res_order))]
|
| 46 |
+
data = {
|
| 47 |
+
'pos': pos,
|
| 48 |
+
'wt': wt,
|
| 49 |
+
'mt': mt,
|
| 50 |
+
'log_likelihood' : log_likelihoods.flatten(),
|
| 51 |
+
'log_likelihood_ratio' : log_likelihood_ratio,
|
| 52 |
+
}
|
| 53 |
+
df = pd.DataFrame(data)
|
| 54 |
+
df.to_csv(fname, index = False)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
|
| 59 |
+
for ab in ab_dict:
|
| 60 |
+
for s,chain in zip(ab_dict[ab], ('hc','lc')):
|
| 61 |
+
eval_ablang(s, ab, chain)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == '__main__':
|
| 65 |
+
main()
|
model/generate_dms.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from dms_utils import deep_mutational_scan
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
import esm
|
| 7 |
+
from util import load_structure, extract_coords_from_structure
|
| 8 |
+
import biotite.structure
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_native_seq(pdbfile, chain):
|
| 13 |
+
structure = load_structure(pdbfile, chain)
|
| 14 |
+
_ , native_seq = extract_coords_from_structure(structure)
|
| 15 |
+
return native_seq
|
| 16 |
+
|
| 17 |
+
def write_dms_lib(args):
|
| 18 |
+
'''Writes a deep mutational scanning library, including the native/wildtype (wt) of the
|
| 19 |
+
indicated target chain in the structure to an output Fasta file'''
|
| 20 |
+
|
| 21 |
+
sequence = get_native_seq(args.pdbfile, args.chain)
|
| 22 |
+
Path(args.outpath).parent.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
with open(args.dmspath, 'w') as f:
|
| 24 |
+
f.write('>wt\n')
|
| 25 |
+
f.write(sequence+'\n')
|
| 26 |
+
for pos, wt, mt in deep_mutational_scan(sequence):
|
| 27 |
+
assert(sequence[pos] == wt)
|
| 28 |
+
mut_seq = sequence[:pos] + mt + sequence[(pos + 1):]
|
| 29 |
+
|
| 30 |
+
f.write('>' + str(wt) + str(pos+1) + str(mt) + '\n')
|
| 31 |
+
f.write(mut_seq + '\n')
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
parser = argparse.ArgumentParser(
|
| 36 |
+
description='Create a DMS library based on target chain in the structure.'
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
'pdbfile', type=str,
|
| 40 |
+
help='input filepath, either .pdb or .cif',
|
| 41 |
+
)
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
'--dmspath', type=str,
|
| 44 |
+
help='output filepath for dms library',
|
| 45 |
+
)
|
| 46 |
+
parser.add_argument(
|
| 47 |
+
'--chain', type=str,
|
| 48 |
+
help='chain id for the chain of interest', default='A',
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
args = parser.parse_args()
|
| 52 |
+
|
| 53 |
+
if args.dmspath is None:
|
| 54 |
+
args.dmspath = f'predictions/{args.pdbfile[:-4]}-{args.chain}_dms.fasta'
|
| 55 |
+
|
| 56 |
+
write_dms_lib(args)
|
| 57 |
+
|
| 58 |
+
if __name__ == '__main__':
|
| 59 |
+
|
| 60 |
+
main()
|
model/multichain_util.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import biotite.structure
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
from typing import Sequence, Tuple, List
|
| 5 |
+
from util import (
|
| 6 |
+
load_structure,
|
| 7 |
+
extract_coords_from_structure,
|
| 8 |
+
load_coords,
|
| 9 |
+
get_sequence_loss,
|
| 10 |
+
get_encoder_output,
|
| 11 |
+
)
|
| 12 |
+
import util
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def extract_coords_from_complex(structure: biotite.structure.AtomArray):
|
| 16 |
+
"""
|
| 17 |
+
Args:
|
| 18 |
+
structure: biotite AtomArray
|
| 19 |
+
Returns:
|
| 20 |
+
Tuple (coords_list, seq_list)
|
| 21 |
+
- coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 22 |
+
coordinates representing the backbone of each chain
|
| 23 |
+
- seqs: Dictionary mapping chain ids to native sequences of each chain
|
| 24 |
+
"""
|
| 25 |
+
coords = {}
|
| 26 |
+
seqs = {}
|
| 27 |
+
all_chains = biotite.structure.get_chains(structure)
|
| 28 |
+
for chain_id in all_chains:
|
| 29 |
+
chain = structure[structure.chain_id == chain_id]
|
| 30 |
+
coords[chain_id], seqs[chain_id] = extract_coords_from_structure(chain)
|
| 31 |
+
return coords, seqs
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_complex_coords(fpath, chains):
|
| 35 |
+
"""
|
| 36 |
+
Args:
|
| 37 |
+
fpath: filepath to either pdb or cif file
|
| 38 |
+
chains: the chain ids (the order matters for autoregressive model)
|
| 39 |
+
Returns:
|
| 40 |
+
Tuple (coords_list, seq_list)
|
| 41 |
+
- coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 42 |
+
coordinates representing the backbone of each chain
|
| 43 |
+
- seqs: Dictionary mapping chain ids to native sequences of each chain
|
| 44 |
+
"""
|
| 45 |
+
structure = load_structure(fpath, chains)
|
| 46 |
+
return extract_coords_from_complex(structure)
|
| 47 |
+
|
| 48 |
+
#*
|
| 49 |
+
def _concatenate_coords(
|
| 50 |
+
coords,
|
| 51 |
+
target_chain_id,
|
| 52 |
+
padding_length=10,
|
| 53 |
+
order=None
|
| 54 |
+
):
|
| 55 |
+
"""
|
| 56 |
+
Args:
|
| 57 |
+
coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 58 |
+
coordinates representing the backbone of each chain
|
| 59 |
+
target_chain_id: The chain id to sample sequences for
|
| 60 |
+
padding_length: Length of padding between concatenated chains
|
| 61 |
+
Returns:
|
| 62 |
+
Tuple (coords, seq)
|
| 63 |
+
- coords_concatenated is an L x 3 x 3 array for N, CA, C coordinates, a
|
| 64 |
+
concatenation of the chains with padding in between
|
| 65 |
+
AND target chain placed first
|
| 66 |
+
- seq is the extracted sequence, with padding tokens inserted
|
| 67 |
+
between the concatenated chains
|
| 68 |
+
"""
|
| 69 |
+
pad_coords = np.full((padding_length, 3, 3), np.nan, dtype=np.float32)
|
| 70 |
+
if order is None:
|
| 71 |
+
order = (
|
| 72 |
+
[ target_chain_id ] +
|
| 73 |
+
[ chain_id for chain_id in coords if chain_id != target_chain_id ]
|
| 74 |
+
)
|
| 75 |
+
coords_list, coords_chains = [], []
|
| 76 |
+
for idx, chain_id in enumerate(order):
|
| 77 |
+
if idx > 0:
|
| 78 |
+
coords_list.append(pad_coords)
|
| 79 |
+
coords_chains.append([ 'pad' ] * padding_length)
|
| 80 |
+
coords_list.append(list(coords[chain_id]))
|
| 81 |
+
coords_chains.append([ chain_id ] * coords[chain_id].shape[0])
|
| 82 |
+
coords_concatenated = np.concatenate(coords_list, axis=0)
|
| 83 |
+
coords_chains = np.concatenate(coords_chains, axis=0).ravel()
|
| 84 |
+
return coords_concatenated, coords_chains
|
| 85 |
+
|
| 86 |
+
#*
|
| 87 |
+
def _concatenate_seqs(
|
| 88 |
+
native_seqs,
|
| 89 |
+
target_seq,
|
| 90 |
+
target_chain_id,
|
| 91 |
+
padding_length=10,
|
| 92 |
+
order=None,
|
| 93 |
+
):
|
| 94 |
+
"""
|
| 95 |
+
Args:
|
| 96 |
+
native_seqs: Dictionary mapping chain ids to corresponding AA sequence
|
| 97 |
+
target_seq: The chain id to sample sequences for
|
| 98 |
+
padding_length: Length of padding between concatenated chains
|
| 99 |
+
Returns:
|
| 100 |
+
native_seqs_concatenated: Array of length L, concatenation of the chain
|
| 101 |
+
sequences with padding in between
|
| 102 |
+
"""
|
| 103 |
+
if order is None:
|
| 104 |
+
order = (
|
| 105 |
+
[ target_chain_id ] +
|
| 106 |
+
[ chain_id for chain_id in native_seqs if chain_id != target_chain_id ]
|
| 107 |
+
)
|
| 108 |
+
native_seqs_list = []
|
| 109 |
+
for idx, chain_id in enumerate(order):
|
| 110 |
+
if idx > 0:
|
| 111 |
+
native_seqs_list.append(['<mask>'] * (padding_length - 1) + ['<cath>'])
|
| 112 |
+
if chain_id == target_chain_id:
|
| 113 |
+
native_seqs_list.append(list(target_seq))
|
| 114 |
+
else:
|
| 115 |
+
native_seqs_list.append(list(native_seqs[chain_id]))
|
| 116 |
+
native_seqs_concatenated = ''.join(np.concatenate(native_seqs_list, axis=0))
|
| 117 |
+
return native_seqs_concatenated
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
#*
|
| 121 |
+
def sample_sequence_in_complex(model, coords, target_chain_id, temperature=1.,
|
| 122 |
+
padding_length=10):
|
| 123 |
+
"""
|
| 124 |
+
Samples sequence for one chain in a complex.
|
| 125 |
+
Args:
|
| 126 |
+
model: An instance of the GVPTransformer model
|
| 127 |
+
coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 128 |
+
coordinates representing the backbone of each chain
|
| 129 |
+
target_chain_id: The chain id to sample sequences for
|
| 130 |
+
padding_length: padding length in between chains
|
| 131 |
+
Returns:
|
| 132 |
+
Sampled sequence for the target chain
|
| 133 |
+
"""
|
| 134 |
+
target_chain_len = coords[target_chain_id].shape[0]
|
| 135 |
+
all_coords, coords_chains = _concatenate_coords(coords, target_chain_id)
|
| 136 |
+
device = next(model.parameters()).device
|
| 137 |
+
|
| 138 |
+
# Supply padding tokens for other chains to avoid unused sampling for speed
|
| 139 |
+
padding_pattern = ['<pad>'] * all_coords.shape[0]
|
| 140 |
+
for i in range(target_chain_len):
|
| 141 |
+
padding_pattern[i] = '<mask>'
|
| 142 |
+
sampled = model.sample(all_coords, partial_seq=padding_pattern,
|
| 143 |
+
temperature=temperature, device=device)
|
| 144 |
+
sampled = sampled[:target_chain_len]
|
| 145 |
+
return sampled
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
#*
|
| 149 |
+
def score_sequence_in_complex(
|
| 150 |
+
model,
|
| 151 |
+
alphabet,
|
| 152 |
+
coords,
|
| 153 |
+
native_seqs,
|
| 154 |
+
target_chain_id,
|
| 155 |
+
target_seq,
|
| 156 |
+
padding_length=10,
|
| 157 |
+
order=None,
|
| 158 |
+
):
|
| 159 |
+
"""
|
| 160 |
+
Scores sequence for one chain in a complex.
|
| 161 |
+
Args:
|
| 162 |
+
model: An instance of the GVPTransformer model
|
| 163 |
+
alphabet: Alphabet for the model
|
| 164 |
+
coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 165 |
+
coordinates representing the backbone of each chain
|
| 166 |
+
native_seqs: Dictionary mapping chain ids to sequence
|
| 167 |
+
extracted from each chain
|
| 168 |
+
target_chain_id: The chain id to sample sequences for
|
| 169 |
+
target_seq: Target sequence for the target chain for scoring.
|
| 170 |
+
padding_length: padding length in between chains
|
| 171 |
+
Returns:
|
| 172 |
+
Tuple (ll_fullseq, ll_withcoord)
|
| 173 |
+
- ll_fullseq: Average log-likelihood over the full target chain
|
| 174 |
+
- ll_targetseq Average log-likelihood in target chain excluding those
|
| 175 |
+
residues without coordinates
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
assert(len(target_seq) == len(native_seqs[target_chain_id]))
|
| 179 |
+
|
| 180 |
+
all_coords, coords_chains = _concatenate_coords(
|
| 181 |
+
coords,
|
| 182 |
+
target_chain_id,
|
| 183 |
+
order=order,
|
| 184 |
+
)
|
| 185 |
+
all_seqs = _concatenate_seqs(
|
| 186 |
+
native_seqs,
|
| 187 |
+
target_seq,
|
| 188 |
+
target_chain_id,
|
| 189 |
+
order=order,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
loss, target_padding_mask = get_sequence_loss(model, alphabet, all_coords,
|
| 193 |
+
all_seqs)
|
| 194 |
+
assert(all_coords.shape[0] == coords_chains.shape[0] == loss.shape[0])
|
| 195 |
+
|
| 196 |
+
ll_fullseq = -np.mean(loss[coords_chains != 'pad'])
|
| 197 |
+
ll_targetseq = -np.mean(loss[coords_chains == target_chain_id])
|
| 198 |
+
|
| 199 |
+
return ll_fullseq, ll_targetseq
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def get_encoder_output_for_complex(model, alphabet, coords, target_chain_id):
|
| 203 |
+
"""
|
| 204 |
+
Args:
|
| 205 |
+
model: An instance of the GVPTransformer model
|
| 206 |
+
alphabet: Alphabet for the model
|
| 207 |
+
coords: Dictionary mapping chain ids to L x 3 x 3 array for N, CA, C
|
| 208 |
+
coordinates representing the backbone of each chain
|
| 209 |
+
target_chain_id: The chain id to sample sequences for
|
| 210 |
+
Returns:
|
| 211 |
+
Dictionary mapping chain id to encoder output for each chain
|
| 212 |
+
"""
|
| 213 |
+
all_coords = _concatenate_coords(coords, target_chain_id)
|
| 214 |
+
all_rep = get_encoder_output(model, alphabet, all_coords)
|
| 215 |
+
target_chain_len = coords[target_chain_id].shape[0]
|
| 216 |
+
return all_rep[:target_chain_len]
|
model/parse_abysis.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
AAs = set('ACDEFGHIKLMNPQRSTVWY')
|
| 6 |
+
|
| 7 |
+
seqs_abs = {
|
| 8 |
+
'cr6261_vh': 'EVQLVESGAEVKKPGSSVKVSCKASGGPFRSYAISWVRQAPGQGPEWMGGIIPIFGTTKYAPKFQGRVTITADDFAGTVYMELSSLRSEDTAMYYCAKHMGYQVRETMDVWGKGTTVTVSS',
|
| 9 |
+
'cr9114_vh': 'QVQLVQSGAEVKKPGSSVKVSCKSSGGTSNNYAISWVRQAPGQGLDWMGGISPIFGSTAYAQKFQGRVTISADIFSNTAYMELNSLTSEDTAVYFCARHGNYYYYSGMDVWGQGTTVTVSS',
|
| 10 |
+
'g6_vh': 'EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTV',
|
| 11 |
+
'g6_vl': 'DIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIK',
|
| 12 |
+
'beb_vh': 'QITLKESGPTLVKPTQTLTLTCTFSGFSLSISGVGVGWLRQPPGKALEWLALIYWDDDKRYSPSLKSRLTISKDTSKNQVVLKMTNIDPVDTATYYCAHHSISTIFDHWGQGTLVTVSS',
|
| 13 |
+
'beb_vl' : 'QSALTQPASVSGSPGQSITISCTATSSDVGDYNYVSWYQQHPGKAPKLMIFEVSDRPSGISNRFSGSKSGNTASLTISGLQAEDEADYYCSSYTTSSAVFGGGTKLTVL',
|
| 14 |
+
'sa58_vh': 'QVQLAQSGSELRKPGASVKVSCDTSGHSFTSNAIHWVRQAPGQGLEWMGWINTDTGTPTYAQGFTGRFVFSLDTSARTAYLQISSLKADDTAVFYCARERDYSDYFFDYWGQGTLVTVSS',
|
| 15 |
+
'sa58_vl': 'EVVMTQSPASLSVSPGERATLSCRARASLGISTDLAWYQQRPGQAPRLLIYGASTRATGIPARFSGSGSGTEFTLTISSLQSEDSAVYYCQQYSNWPLTFGGGTKVEIK',
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_abysis(seq, seq_name):
|
| 20 |
+
fname = f'data/abysis/abysis_counts_{seq_name}.json'
|
| 21 |
+
with open(fname) as f:
|
| 22 |
+
json_data = json.load(f)
|
| 23 |
+
|
| 24 |
+
data = []
|
| 25 |
+
for idx, freq_table in enumerate(json_data['frequencies']):
|
| 26 |
+
wt = seq[idx]
|
| 27 |
+
total = freq_table['total']
|
| 28 |
+
for entry in freq_table['counts']:
|
| 29 |
+
if entry['aa'] == seq[idx]:
|
| 30 |
+
wt_frac = entry['c'] / total
|
| 31 |
+
aa_to_freq, seen = {}, set()
|
| 32 |
+
for entry in freq_table['counts']:
|
| 33 |
+
mt = entry['aa']
|
| 34 |
+
seen.add(mt)
|
| 35 |
+
counts = entry['c']
|
| 36 |
+
frac = entry['c'] / total
|
| 37 |
+
ratio = frac / wt_frac
|
| 38 |
+
data.append([ idx + 1, wt, mt, counts, frac, ratio ])
|
| 39 |
+
for mt in AAs - seen:
|
| 40 |
+
data.append([ idx + 1, wt, mt, 0, 0., 0. ])
|
| 41 |
+
|
| 42 |
+
df = pd.DataFrame(data, columns=[
|
| 43 |
+
'pos',
|
| 44 |
+
'wt',
|
| 45 |
+
'mt',
|
| 46 |
+
'counts',
|
| 47 |
+
'fraction',
|
| 48 |
+
'likelihood_ratio',
|
| 49 |
+
])
|
| 50 |
+
|
| 51 |
+
if any(prefix in seq_name for prefix in ['cr6261', 'cr9114', 'g6']):
|
| 52 |
+
subdirectory = seq_name.split('_')[0]
|
| 53 |
+
else:
|
| 54 |
+
subdirectory = ''
|
| 55 |
+
|
| 56 |
+
output_dir = os.path.join('output', 'ab_mutagenesis_expts', subdirectory)
|
| 57 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
output_path = os.path.join(output_dir, f'abysis_counts_{seq_name}.txt')
|
| 60 |
+
df.to_csv(output_path, sep='\t')
|
| 61 |
+
|
| 62 |
+
if __name__ == '__main__':
|
| 63 |
+
for seq_name in seqs_abs:
|
| 64 |
+
seq = seqs_abs[seq_name]
|
| 65 |
+
parse_abysis(seq, seq_name)
|
model/plot_ab-binding_benchmarks.py
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import bokeh.io
|
| 4 |
+
import bokeh.plotting
|
| 5 |
+
import bokeh.palettes
|
| 6 |
+
from bokeh.transform import factor_cmap
|
| 7 |
+
import datashader
|
| 8 |
+
import holoviews as hv
|
| 9 |
+
import holoviews.operation.datashader
|
| 10 |
+
hv.extension("bokeh")
|
| 11 |
+
|
| 12 |
+
import warnings
|
| 13 |
+
# Suppress FutureWarning messages
|
| 14 |
+
warnings.simplefilter(action='ignore',)
|
| 15 |
+
|
| 16 |
+
# Your Pandas code here
|
| 17 |
+
|
| 18 |
+
if_conds = ['ag+ab', 'ab only', 'ab']
|
| 19 |
+
|
| 20 |
+
cr9114_dict = {
|
| 21 |
+
'ab_name' : 'CR9114',
|
| 22 |
+
'files' : {
|
| 23 |
+
'Ab-Ag': 'output/ab_mutagenesis_expts/cr9114/4fqi_ablh_scores.csv',
|
| 24 |
+
'Ab only': 'output/ab_mutagenesis_expts/cr9114/4fqi_lh_scores.csv',
|
| 25 |
+
'Ab VH only': 'output/ab_mutagenesis_expts/cr9114/4fqi_h_scores.csv',
|
| 26 |
+
'ESM-1v': 'output/ab_mutagenesis_expts/cr9114/cr9114_exp_data_maskMargLabeled.csv',
|
| 27 |
+
'AbLang': 'output/ab_mutagenesis_expts/cr9114/cr9114_hc_ablangScores.csv',
|
| 28 |
+
'abYsis': 'output/ab_mutagenesis_expts/cr9114/abysis_counts_cr9114_vh.txt',
|
| 29 |
+
},
|
| 30 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr9114/cr9114_exp_data.csv',
|
| 31 |
+
dtype = {'genotype': str},
|
| 32 |
+
).rename(columns={'h1_mean': 'H1', 'h3_mean' : 'H3'}),
|
| 33 |
+
'ag_columns': ['H1', 'H3'],
|
| 34 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 35 |
+
'palette': bokeh.palettes.Spectral6
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
cr6261_dict = {
|
| 39 |
+
'ab_name' : 'CR6261',
|
| 40 |
+
'files' : {
|
| 41 |
+
'Ab-Ag': 'output/ab_mutagenesis_expts/cr6261/3gbn_ablh_scores.csv',
|
| 42 |
+
'Ab only': 'output/ab_mutagenesis_expts/cr6261/3gbn_lh_scores.csv',
|
| 43 |
+
'Ab VH only': 'output/ab_mutagenesis_expts/cr6261/3gbn_h_scores.csv',
|
| 44 |
+
'ESM-1v': 'output/ab_mutagenesis_expts/cr6261/cr6261_exp_data_maskMargLabeled.csv',
|
| 45 |
+
'AbLang': 'output/ab_mutagenesis_expts/cr6261/cr6261_hc_ablangScores.csv',
|
| 46 |
+
'abYsis': 'output/ab_mutagenesis_expts/cr6261/abysis_counts_cr6261_vh.txt',
|
| 47 |
+
},
|
| 48 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr6261/cr6261_exp_data.csv',
|
| 49 |
+
dtype={'genotype': str},
|
| 50 |
+
).rename(columns={'h1_mean': 'H1', 'h9_mean' : 'H9'}),
|
| 51 |
+
'ag_columns': ['H1', 'H9'],
|
| 52 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 53 |
+
'palette': bokeh.palettes.Dark2_6,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
g6LC_dict = {
|
| 58 |
+
'ab_name' : 'g6',
|
| 59 |
+
'files' : {
|
| 60 |
+
'Ab-Ag': 'output/ab_mutagenesis_expts/g6/2fjg_vlh_lc_scores.csv',
|
| 61 |
+
'Ab only': 'output/ab_mutagenesis_expts/g6/2fjg_lh_lc_scores.csv',
|
| 62 |
+
'Ab VH/VL only': 'output/ab_mutagenesis_expts/g6/2fjg_l_lc_scores.csv',
|
| 63 |
+
'ESM-1v': 'output/ab_mutagenesis_expts/g6/g6Lc_exp_data_maskMargLabeled.csv',
|
| 64 |
+
'AbLang': 'output/ab_mutagenesis_expts/g6/g6_lc_ablangScores.csv',
|
| 65 |
+
'abYsis': 'output/ab_mutagenesis_expts/g6/abysis_counts_g6_vl.txt',
|
| 66 |
+
},
|
| 67 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv'),
|
| 68 |
+
'ag_columns': ['norm_binding'],
|
| 69 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 70 |
+
'palette': bokeh.palettes.Pastel1_6,
|
| 71 |
+
'chain': 'VL'
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
g6HC_dict = {
|
| 75 |
+
'ab_name' : 'g6',
|
| 76 |
+
'files' : {
|
| 77 |
+
'Ab-Ag': 'output/ab_mutagenesis_expts/g6/2fjg_vlh_hc_scores.csv',
|
| 78 |
+
'Ab only': 'output/ab_mutagenesis_expts/g6/2fjg_lh_hc_scores.csv',
|
| 79 |
+
'Ab VH/VL only': 'output/ab_mutagenesis_expts/g6/2fjg_h_hc_scores.csv',
|
| 80 |
+
'ESM-1v': 'output/ab_mutagenesis_expts/g6/g6Hc_exp_data_maskMargLabeled.csv',
|
| 81 |
+
'AbLang': 'output/ab_mutagenesis_expts/g6/g6_hc_ablangScores.csv',
|
| 82 |
+
'abYsis': 'output/ab_mutagenesis_expts/g6/abysis_counts_g6_vh.txt',
|
| 83 |
+
},
|
| 84 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv'),
|
| 85 |
+
'ag_columns': ['norm_binding'],
|
| 86 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 87 |
+
'palette': bokeh.palettes.Pastel1_4,
|
| 88 |
+
'chain': 'VH'
|
| 89 |
+
}
|
| 90 |
+
def apply_mask_and_average(scores, genotype):
|
| 91 |
+
#scores here should already be a in log space
|
| 92 |
+
|
| 93 |
+
if '1' in genotype:
|
| 94 |
+
masked_list = []
|
| 95 |
+
for s, mask_char in zip(scores, genotype):
|
| 96 |
+
if mask_char == '1':
|
| 97 |
+
masked_list.append(s)
|
| 98 |
+
multi_avg = np.mean(masked_list)
|
| 99 |
+
else:
|
| 100 |
+
#if genotype is all zeros = wt
|
| 101 |
+
multi_avg = 0
|
| 102 |
+
|
| 103 |
+
return multi_avg
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def transform_single_to_multi( dms_df, singleMuts_df, condition, sort_col, ascending ):
|
| 107 |
+
multi_scores = []
|
| 108 |
+
#use input parameter as method bc key is kwarg in pd.sort_values
|
| 109 |
+
#method should be either esm1v or abysis or AbLang
|
| 110 |
+
|
| 111 |
+
#for cr sorts the single mutation dataframe in residue order, ie binary '10000' before '00010'. ascending = False
|
| 112 |
+
#for g6 sorts the single mutation dataframe in position order, ie residue index. ascending = True
|
| 113 |
+
|
| 114 |
+
singleMuts_sorted = singleMuts_df.sort_values(sort_col, key=lambda x: x.astype(int), ascending=ascending)
|
| 115 |
+
singles_scores = singleMuts_sorted[condition].to_list()
|
| 116 |
+
|
| 117 |
+
for g in dms_df['genotype']:
|
| 118 |
+
multi_scores.append(apply_mask_and_average(singles_scores, g ))
|
| 119 |
+
|
| 120 |
+
return multi_scores
|
| 121 |
+
|
| 122 |
+
def get_abysis_single_scores_df(singleMut_ids, abysis_df):
|
| 123 |
+
single_scores = []
|
| 124 |
+
all_pos = []
|
| 125 |
+
|
| 126 |
+
for id in singleMut_ids:
|
| 127 |
+
wt, pos, mt = id[0], int(id[1:-1]), id[-1]
|
| 128 |
+
likelihood_ratio = abysis_df.loc[(abysis_df['pos'] == pos) & (abysis_df['wt'] == wt) & (abysis_df['mt'] == mt)]['likelihood_ratio'].to_list()[0]
|
| 129 |
+
log_like_ratio = np.log10(likelihood_ratio)
|
| 130 |
+
single_scores.append(log_like_ratio)
|
| 131 |
+
all_pos.append(pos)
|
| 132 |
+
|
| 133 |
+
singleMuts_df = pd.DataFrame({'pos': all_pos, 'abYsis': single_scores})
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
return singleMuts_df
|
| 137 |
+
|
| 138 |
+
def get_ablang_single_scores_df(singleMut_ids, ablang_df):
|
| 139 |
+
single_scores = []
|
| 140 |
+
all_pos = []
|
| 141 |
+
|
| 142 |
+
for id in singleMut_ids:
|
| 143 |
+
wt, pos, mt = id[0], int(id[1:-1]), id[-1]
|
| 144 |
+
log_likelihood_ratio = ablang_df.loc[(ablang_df['pos'] == pos) & (ablang_df['wt'] == wt) & (ablang_df['mt'] == mt)]['log_likelihood_ratio'].to_list()[0]
|
| 145 |
+
single_scores.append(log_likelihood_ratio)
|
| 146 |
+
all_pos.append(pos)
|
| 147 |
+
|
| 148 |
+
singleMuts_df = pd.DataFrame({'pos': all_pos, 'AbLang': single_scores})
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
return singleMuts_df
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
#get melted correlations matrix of structure input x target antigen, for a given antibody
|
| 155 |
+
def get_corr(ab_name, files, dms_df, ag_columns, dropLOQ = False ):
|
| 156 |
+
|
| 157 |
+
inv_fold_files = {key: value for key, value in files.items() if key not in ['ESM-1v', 'AbLang', 'abYsis']}
|
| 158 |
+
other_files = {key: value for key, value in files.items() if key in ['ESM-1v', 'AbLang', 'abYsis']}
|
| 159 |
+
|
| 160 |
+
#retreive correlations from inverse fold scores
|
| 161 |
+
for key, filepath in inv_fold_files.items():
|
| 162 |
+
df = pd.read_csv(filepath) # Read the CSV file into a DataFrame
|
| 163 |
+
column_label = key
|
| 164 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 165 |
+
dms_df[column_label] = log_likelihood_column
|
| 166 |
+
|
| 167 |
+
#retreive correlations from abysis and esm1v scores
|
| 168 |
+
for key, filepath in other_files.items():
|
| 169 |
+
if key == 'ESM-1v':
|
| 170 |
+
singleMuts_df = pd.read_csv(filepath)
|
| 171 |
+
# Get the columns for all esm1v models
|
| 172 |
+
esm_columns = [col for col in singleMuts_df.columns if col.startswith('esm')]
|
| 173 |
+
# Calculate the average for each esm column
|
| 174 |
+
esm_avg_values = singleMuts_df[esm_columns].mean(axis=1)
|
| 175 |
+
singleMuts_df[key] = esm_avg_values
|
| 176 |
+
if ab_name == 'g6':
|
| 177 |
+
#g6 data set is only single mutations
|
| 178 |
+
dms_df[key] = esm_avg_values
|
| 179 |
+
else:
|
| 180 |
+
#cr datasets are multiple mutations
|
| 181 |
+
dms_df[key] = transform_single_to_multi(dms_df, singleMuts_df, key, 'genotype', ascending = False)
|
| 182 |
+
elif key == 'abYsis':
|
| 183 |
+
abysis_df = pd.read_csv(filepath, sep = '\t', dtype = {'pos': int})
|
| 184 |
+
if ab_name == 'g6':
|
| 185 |
+
dms_df[key] = get_abysis_single_scores_df(dms_df['mutant'], abysis_df)[key]
|
| 186 |
+
else:
|
| 187 |
+
#cr data set are multiple mutations. use same scoring strategy as esm1v paper
|
| 188 |
+
singleMut_ids = pd.read_csv(files['ESM-1v'])['mutant'].to_list()
|
| 189 |
+
singleMuts_df = get_abysis_single_scores_df(singleMut_ids, abysis_df)
|
| 190 |
+
dms_df[key] = transform_single_to_multi(dms_df, singleMuts_df, key, 'pos', ascending = True)
|
| 191 |
+
elif key == 'AbLang':
|
| 192 |
+
ablang_df = pd.read_csv(filepath, dtype = {'pos': int})
|
| 193 |
+
if ab_name == 'g6':
|
| 194 |
+
dms_df[key] = get_ablang_single_scores_df(dms_df['mutant'], ablang_df)[key]
|
| 195 |
+
else:
|
| 196 |
+
#cr data set are multiple mutations. use same scoring strategy as esm1v paper
|
| 197 |
+
singleMut_ids = pd.read_csv(files['ESM-1v'])['mutant'].to_list()
|
| 198 |
+
singleMuts_df = get_ablang_single_scores_df(singleMut_ids, ablang_df)
|
| 199 |
+
dms_df[key] = transform_single_to_multi(dms_df, singleMuts_df, key, 'pos', ascending = True)
|
| 200 |
+
|
| 201 |
+
conditions = list(files.keys())
|
| 202 |
+
|
| 203 |
+
#
|
| 204 |
+
if not dropLOQ:
|
| 205 |
+
correlations = dms_df[ag_columns + conditions].corr(method='spearman')
|
| 206 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 207 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 208 |
+
|
| 209 |
+
# Melt the correlations DataFrame and rename the index column
|
| 210 |
+
melted_correlations = (
|
| 211 |
+
correlations
|
| 212 |
+
.reset_index()
|
| 213 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 214 |
+
.rename(columns={'index': 'Input'})
|
| 215 |
+
)
|
| 216 |
+
melted_correlations['Method'] = ['LM' if (('ESM' in i) or ('AbLang' in i))
|
| 217 |
+
else 'IF' if 'Ab' in i
|
| 218 |
+
else 'MSA'
|
| 219 |
+
for i in melted_correlations['Input']
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
return melted_correlations
|
| 223 |
+
#if we wish to drop the samples on the lower limit of quantitation, drop samples from each ag group individually and compute correlation with IF log_like
|
| 224 |
+
else:
|
| 225 |
+
all_ag_melted = pd.DataFrame({})
|
| 226 |
+
for ag in ag_columns:
|
| 227 |
+
filt_df = dms_df[dms_df[ag] > min(dms_df[ag])]
|
| 228 |
+
|
| 229 |
+
correlations = filt_df[[ag] + conditions].corr(method='spearman')
|
| 230 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 231 |
+
correlations = correlations.drop(ag, axis= 0)
|
| 232 |
+
|
| 233 |
+
# Melt the correlations DataFrame and rename the index column
|
| 234 |
+
melted_correlations = (
|
| 235 |
+
correlations
|
| 236 |
+
.reset_index()
|
| 237 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 238 |
+
).rename(columns={'index': 'Input'})
|
| 239 |
+
|
| 240 |
+
all_ag_melted = pd.concat([all_ag_melted, melted_correlations], ignore_index= True)
|
| 241 |
+
all_ag_melted['Method'] = ['LM' if (('ESM' in i) or ('AbLang' in i))
|
| 242 |
+
else 'IF' if 'Ab' in i
|
| 243 |
+
else 'MSA'
|
| 244 |
+
for i in all_ag_melted['Input']
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
return all_ag_melted
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
#get melted correlations matrix of structure input x target antigen, for a given antibody
|
| 251 |
+
def get_g6_corr(g6Hc_dict, g6LC_dict, dropLOQ = False ):
|
| 252 |
+
|
| 253 |
+
ab_name = 'g6'
|
| 254 |
+
ag_columns = g6LC_dict['ag_columns']
|
| 255 |
+
vh_and_vl_dms_df = pd.DataFrame({})
|
| 256 |
+
|
| 257 |
+
for d in [g6HC_dict, g6LC_dict]:
|
| 258 |
+
|
| 259 |
+
files = d['files']
|
| 260 |
+
dms_df = d['dms_df']
|
| 261 |
+
|
| 262 |
+
inv_fold_files = {key: value for key, value in files.items() if key not in ['ESM-1v', 'AbLang', 'abYsis']}
|
| 263 |
+
other_files = {key: value for key, value in files.items() if key in ['ESM-1v', 'AbLang', 'abYsis']}
|
| 264 |
+
|
| 265 |
+
#retreive correlations from inverse fold scores
|
| 266 |
+
for key, filepath in inv_fold_files.items():
|
| 267 |
+
df = pd.read_csv(filepath) # Read the CSV file into a DataFrame
|
| 268 |
+
column_label = key
|
| 269 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 270 |
+
dms_df[column_label] = log_likelihood_column
|
| 271 |
+
|
| 272 |
+
#retreive correlations from abysis, ablang, and esm1v scores
|
| 273 |
+
for key, filepath in other_files.items():
|
| 274 |
+
if key == 'ESM-1v':
|
| 275 |
+
singleMuts_df = pd.read_csv(filepath)
|
| 276 |
+
# Get the columns for all esm1v models
|
| 277 |
+
esm_columns = [col for col in singleMuts_df.columns if col.startswith('esm')]
|
| 278 |
+
# Calculate the average for each esm column
|
| 279 |
+
esm_avg_values = singleMuts_df[esm_columns].mean(axis=1)
|
| 280 |
+
singleMuts_df[key] = esm_avg_values
|
| 281 |
+
if ab_name == 'g6':
|
| 282 |
+
#g6 data set is only single mutations
|
| 283 |
+
dms_df[key] = esm_avg_values
|
| 284 |
+
elif key == 'abYsis':
|
| 285 |
+
abysis_df = pd.read_csv(filepath, sep = '\t', dtype = {'pos': int})
|
| 286 |
+
if ab_name == 'g6':
|
| 287 |
+
dms_df[key] = get_abysis_single_scores_df(dms_df['mutant'], abysis_df)[key]
|
| 288 |
+
elif key == 'AbLang':
|
| 289 |
+
ablang_df = pd.read_csv(filepath, dtype = {'pos': int})
|
| 290 |
+
if ab_name == 'g6':
|
| 291 |
+
dms_df[key] = get_ablang_single_scores_df(dms_df['mutant'], ablang_df)[key]
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
vh_and_vl_dms_df = pd.concat([vh_and_vl_dms_df, dms_df], ignore_index= True)
|
| 295 |
+
|
| 296 |
+
conditions = list(files.keys())
|
| 297 |
+
|
| 298 |
+
#
|
| 299 |
+
if not dropLOQ:
|
| 300 |
+
correlations = vh_and_vl_dms_df[ag_columns + conditions].corr(method='spearman')
|
| 301 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 302 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 303 |
+
|
| 304 |
+
# Melt the correlations DataFrame and rename the index column
|
| 305 |
+
melted_correlations = (
|
| 306 |
+
correlations
|
| 307 |
+
.reset_index()
|
| 308 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 309 |
+
.rename(columns={'index': 'Input'})
|
| 310 |
+
)
|
| 311 |
+
melted_correlations['Method'] = ['LM' if (('ESM' in i) or ('AbLang' in i))
|
| 312 |
+
else 'IF' if 'Ab' in i
|
| 313 |
+
else 'MSA'
|
| 314 |
+
for i in melted_correlations['Input']
|
| 315 |
+
]
|
| 316 |
+
melted_correlations['Target'] = ['VEGF-A'] * len(melted_correlations)
|
| 317 |
+
|
| 318 |
+
return melted_correlations
|
| 319 |
+
#if we wish to drop the samples on the lower limit of quantitation, drop samples from each ag group individually and compute correlation with IF log_like
|
| 320 |
+
else:
|
| 321 |
+
all_ag_melted = pd.DataFrame({})
|
| 322 |
+
for ag in ag_columns:
|
| 323 |
+
filt_df = vh_and_vl_dms_df[vh_and_vl_dms_df[ag] > min(vh_and_vl_dms_df[ag])]
|
| 324 |
+
|
| 325 |
+
correlations = filt_df[[ag] + conditions].corr(method='spearman')
|
| 326 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 327 |
+
correlations = correlations.drop(ag, axis= 0)
|
| 328 |
+
|
| 329 |
+
# Melt the correlations DataFrame and rename the index column
|
| 330 |
+
melted_correlations = (
|
| 331 |
+
correlations
|
| 332 |
+
.reset_index()
|
| 333 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 334 |
+
).rename(columns={'index': 'Input'})
|
| 335 |
+
|
| 336 |
+
all_ag_melted = pd.concat([all_ag_melted, melted_correlations], ignore_index= True)
|
| 337 |
+
all_ag_melted['Method'] = ['LM' if (('ESM' in i) or ('AbLang' in i))
|
| 338 |
+
else 'IF' if 'Ab' in i
|
| 339 |
+
else 'MSA'
|
| 340 |
+
for i in all_ag_melted['Input']
|
| 341 |
+
]
|
| 342 |
+
melted_correlations['Target'] = ['VEGF-A'] * len(melted_correlations)
|
| 343 |
+
|
| 344 |
+
return all_ag_melted
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
#plot correlation bars for cr antibodies: cr6261 and cr9114
|
| 348 |
+
def plot_hbar(title, melted_correlations, palette = bokeh.palettes.Spectral6, compare = 'model'):
|
| 349 |
+
|
| 350 |
+
if compare == 'model':
|
| 351 |
+
melted_correlations.replace('Ab-Ag','InverseFolding', inplace = True)
|
| 352 |
+
color_by = 'Input'
|
| 353 |
+
elif compare == 'IF':
|
| 354 |
+
color_by = 'Method'
|
| 355 |
+
else:
|
| 356 |
+
color_by = 'Target'
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
melted_correlations['cats'] = melted_correlations.apply(lambda x: (x["Target"], x["Input"]), axis = 1)
|
| 360 |
+
factors = list(melted_correlations.cats)[::-1]
|
| 361 |
+
|
| 362 |
+
if 'CR6261' in title:
|
| 363 |
+
x_min = -0.15
|
| 364 |
+
else:
|
| 365 |
+
x_min = 0
|
| 366 |
+
|
| 367 |
+
p = bokeh.plotting.figure(
|
| 368 |
+
height=340,
|
| 369 |
+
width=440,
|
| 370 |
+
x_axis_label="Spearman Correlation",
|
| 371 |
+
x_range=[x_min, 1],
|
| 372 |
+
y_range=bokeh.models.FactorRange(*factors),
|
| 373 |
+
tools="save",
|
| 374 |
+
title = title
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
p.hbar(
|
| 379 |
+
source=melted_correlations,
|
| 380 |
+
y="cats",
|
| 381 |
+
right="Correlation",
|
| 382 |
+
height=0.6,
|
| 383 |
+
line_color = 'black',
|
| 384 |
+
legend_field = color_by,
|
| 385 |
+
# use the palette to colormap based on the the x[1:2] values
|
| 386 |
+
fill_color=factor_cmap(
|
| 387 |
+
color_by,
|
| 388 |
+
palette= palette,
|
| 389 |
+
factors = list(melted_correlations[color_by].unique()),
|
| 390 |
+
start=1,
|
| 391 |
+
end=3)
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
#labels_df = melted_correlations[melted_correlations['Input'].isin(['Ab-Ag','ESM-1v', 'abYsis', 'InverseFold'])]
|
| 395 |
+
labels_df = melted_correlations
|
| 396 |
+
labels_df['corr_str'] = labels_df['Correlation'].apply(lambda x: round(x, 2)).astype(str)
|
| 397 |
+
labels_source = bokeh.models.ColumnDataSource(labels_df)
|
| 398 |
+
|
| 399 |
+
labels = bokeh.models.LabelSet(x='Correlation', y='cats', text='corr_str',text_font_size = "10px",
|
| 400 |
+
x_offset=12, y_offset=-5, source=labels_source, render_mode='canvas')
|
| 401 |
+
|
| 402 |
+
p.ygrid.grid_line_color = None
|
| 403 |
+
p.y_range.range_padding = 0.1
|
| 404 |
+
p.add_layout(labels)
|
| 405 |
+
|
| 406 |
+
if compare == 'IF':
|
| 407 |
+
p.legend.orientation = "vertical"
|
| 408 |
+
p.legend.location = "bottom_right"
|
| 409 |
+
p.legend.label_text_font_size = "8pt"
|
| 410 |
+
else:
|
| 411 |
+
p.legend.visible = False
|
| 412 |
+
|
| 413 |
+
p.output_backend = "svg"
|
| 414 |
+
return p
|
| 415 |
+
|
| 416 |
+
def plot_g6_scatter(g6Hc_df, g6Lc_df, ag_columns, dropLOQ):
|
| 417 |
+
ag = ag_columns[0]
|
| 418 |
+
if dropLOQ:
|
| 419 |
+
g6Lc_df = g6Lc_df[g6Lc_df[ag] > np.min(g6Lc_df[ag])]
|
| 420 |
+
g6Hc_df = g6Hc_df[g6Hc_df[ag] > np.min(g6Hc_df[ag])]
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
hv.extension("bokeh")
|
| 424 |
+
|
| 425 |
+
plots = []
|
| 426 |
+
for (plot_df,title) in [(g6Hc_df, 'g6.31 VH'),(g6Lc_df, 'g6.31 VL')]:
|
| 427 |
+
|
| 428 |
+
# Generate HoloViews Points Element
|
| 429 |
+
points = hv.Points(
|
| 430 |
+
data=plot_df,
|
| 431 |
+
kdims=[ag, 'Ab-Ag'],
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# Datashade with spreading of points
|
| 435 |
+
p = hv.operation.datashader.dynspread(
|
| 436 |
+
hv.operation.datashader.rasterize(
|
| 437 |
+
points
|
| 438 |
+
).opts(
|
| 439 |
+
cmap='Magma',
|
| 440 |
+
cnorm='linear',
|
| 441 |
+
)
|
| 442 |
+
).opts(
|
| 443 |
+
frame_width=350,
|
| 444 |
+
frame_height=300,
|
| 445 |
+
padding=0.05,
|
| 446 |
+
show_grid=False,
|
| 447 |
+
colorbar = True
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
p = hv.render(p)
|
| 451 |
+
p.output_backend = "svg"
|
| 452 |
+
|
| 453 |
+
plots.append(p)
|
| 454 |
+
|
| 455 |
+
return plots
|
| 456 |
+
|
| 457 |
+
#plot scatter plot colored by n_muts for CR antibodies
|
| 458 |
+
def single_cr_scatterMut(plot_df, ag_columns, x, y, title, ):
|
| 459 |
+
x_axis_label = x + '-logKd' if x in ag_columns else 'log likelihood'
|
| 460 |
+
y_axis_label = y + '-logKd' if y in ag_columns else 'log likelihood'
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
#set up figure
|
| 464 |
+
p = bokeh.plotting.figure(
|
| 465 |
+
width=500,
|
| 466 |
+
height=400,
|
| 467 |
+
x_axis_label= x_axis_label,
|
| 468 |
+
y_axis_label= y_axis_label,
|
| 469 |
+
title = title
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
#Set up color mapper to color by number of mutations from germline
|
| 473 |
+
mapper = bokeh.transform.linear_cmap(field_name='som_mut', palette=bokeh.palettes.inferno(16) ,low=min(plot_df.som_mut) ,high=max(plot_df.som_mut))
|
| 474 |
+
p.circle(
|
| 475 |
+
source=plot_df,
|
| 476 |
+
x= x,
|
| 477 |
+
y= y,
|
| 478 |
+
alpha = 0.2,
|
| 479 |
+
line_color=mapper,
|
| 480 |
+
color=mapper,
|
| 481 |
+
size = 4
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
#
|
| 485 |
+
p.diamond(
|
| 486 |
+
#germline seqid is all 0's
|
| 487 |
+
source = plot_df[plot_df['genotype'].str.contains('1') == False],
|
| 488 |
+
x= x,
|
| 489 |
+
y= y,
|
| 490 |
+
color = 'dodgerblue',
|
| 491 |
+
size = 12,
|
| 492 |
+
legend_label = 'germline'
|
| 493 |
+
)
|
| 494 |
+
p.star(
|
| 495 |
+
#mature seqid is all 0's
|
| 496 |
+
source = plot_df[plot_df['genotype'].str.contains('0') == False],
|
| 497 |
+
x= x,
|
| 498 |
+
y= y,
|
| 499 |
+
color = 'green',
|
| 500 |
+
size = 12,
|
| 501 |
+
legend_label = 'mature'
|
| 502 |
+
)
|
| 503 |
+
p.legend.location = "top_left"
|
| 504 |
+
p.output_backend = "svg"
|
| 505 |
+
|
| 506 |
+
return p, mapper
|
| 507 |
+
|
| 508 |
+
#plot single scatter plot for CR antibodies - with dynamic spreading and datashading
|
| 509 |
+
def single_cr_scatter(plot_df, ag_columns, x, y, title, ):
|
| 510 |
+
x_axis_label = x + '-logKd' if x in ag_columns else 'log likelihood'
|
| 511 |
+
y_axis_label = y + '-logKd' if y in ag_columns else 'log likelihood'
|
| 512 |
+
|
| 513 |
+
hv.extension("bokeh")
|
| 514 |
+
|
| 515 |
+
# Generate HoloViews Points Element
|
| 516 |
+
points = hv.Points(
|
| 517 |
+
data=plot_df,
|
| 518 |
+
kdims=[x, y],
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
# Datashade with spreading of points
|
| 522 |
+
p = hv.operation.datashader.dynspread(
|
| 523 |
+
hv.operation.datashader.rasterize(
|
| 524 |
+
points
|
| 525 |
+
).opts(
|
| 526 |
+
cmap='Magma',
|
| 527 |
+
cnorm='linear',
|
| 528 |
+
)
|
| 529 |
+
).opts(
|
| 530 |
+
frame_width=350,
|
| 531 |
+
frame_height=300,
|
| 532 |
+
padding=0.05,
|
| 533 |
+
show_grid=False,
|
| 534 |
+
colorbar = True
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
p = hv.render(p)
|
| 538 |
+
|
| 539 |
+
p.grid.visible = False
|
| 540 |
+
#
|
| 541 |
+
p.diamond(
|
| 542 |
+
#germline seqid is all 0's
|
| 543 |
+
source = plot_df[plot_df['genotype'].str.contains('1') == False],
|
| 544 |
+
x= x,
|
| 545 |
+
y= y,
|
| 546 |
+
color = 'dodgerblue',
|
| 547 |
+
size = 12,
|
| 548 |
+
#legend_label = 'germline'
|
| 549 |
+
)
|
| 550 |
+
p.star(
|
| 551 |
+
#mature seqid is all 0's
|
| 552 |
+
source = plot_df[plot_df['genotype'].str.contains('0') == False],
|
| 553 |
+
x= x,
|
| 554 |
+
y= y,
|
| 555 |
+
color = 'green',
|
| 556 |
+
size = 12,
|
| 557 |
+
#legend_label = 'mature'
|
| 558 |
+
)
|
| 559 |
+
#p.legend.location = "top_left"
|
| 560 |
+
p.output_backend = "svg"
|
| 561 |
+
|
| 562 |
+
return p
|
| 563 |
+
|
| 564 |
+
def plot_CR_scatter_colorMuts(title, files, dms_df, ag_columns, dropLOQ = False):
|
| 565 |
+
|
| 566 |
+
agAb_files = {key: value for key, value in files.items() if key in ['Ab-Ag',]}
|
| 567 |
+
|
| 568 |
+
for key, filepath in agAb_files.items():
|
| 569 |
+
df = pd.read_csv(filepath) # Read the CSV file into a DataFrame
|
| 570 |
+
column_label = key
|
| 571 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 572 |
+
dms_df[column_label] = log_likelihood_column
|
| 573 |
+
|
| 574 |
+
plots = []
|
| 575 |
+
#predictions vs experiment
|
| 576 |
+
for ag in ag_columns:
|
| 577 |
+
|
| 578 |
+
if dropLOQ:
|
| 579 |
+
plot_df = dms_df[dms_df[ag] > min(dms_df[ag])]
|
| 580 |
+
#add back wt which is on LOQ for visualization
|
| 581 |
+
plot_df = pd.concat([plot_df, dms_df[dms_df['som_mut'] == 0]], ignore_index = True, axis = 0)
|
| 582 |
+
else:
|
| 583 |
+
plot_df = dms_df
|
| 584 |
+
|
| 585 |
+
p, _ = single_cr_scatterMut(plot_df, ag_columns, x = ag, y = 'Ab-Ag', title = f'{title} against {ag}')
|
| 586 |
+
plots.append(p)
|
| 587 |
+
|
| 588 |
+
#experimental vs experimental
|
| 589 |
+
plot_df = dms_df
|
| 590 |
+
if dropLOQ:
|
| 591 |
+
for ag in ag_columns:
|
| 592 |
+
plot_df = plot_df[plot_df[ag] > min(plot_df[ag])]
|
| 593 |
+
#add back wt which is on LOQ for visualization
|
| 594 |
+
plot_df = pd.concat([plot_df, dms_df[dms_df['som_mut'] == 0]], ignore_index = True, axis = 0)
|
| 595 |
+
|
| 596 |
+
p_exp, mapper = single_cr_scatterMut(plot_df, ag_columns, x= ag_columns[0], y = ag_columns[1], title = 'Experimental Cross-Reactive Binding Landscape')
|
| 597 |
+
p_exp.legend.location = "top_left"
|
| 598 |
+
color_bar = bokeh.models.ColorBar(color_mapper=mapper['transform'], width=8, title = 'Amino Acid Mutations')
|
| 599 |
+
#p_exp.add_layout(color_bar, 'right')
|
| 600 |
+
|
| 601 |
+
plots.append(p_exp)
|
| 602 |
+
p.output_backend = "svg"
|
| 603 |
+
|
| 604 |
+
return plots
|
| 605 |
+
|
| 606 |
+
def plot_CR_scatterplots(title, files, dms_df, ag_columns, dropLOQ = False):
|
| 607 |
+
|
| 608 |
+
agAb_files = {key: value for key, value in files.items() if key in ['Ab-Ag',]}
|
| 609 |
+
|
| 610 |
+
for key, filepath in agAb_files.items():
|
| 611 |
+
df = pd.read_csv(filepath) # Read the CSV file into a DataFrame
|
| 612 |
+
column_label = key
|
| 613 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 614 |
+
dms_df[column_label] = log_likelihood_column
|
| 615 |
+
|
| 616 |
+
plots = []
|
| 617 |
+
#predictions vs experiment
|
| 618 |
+
for ag in ag_columns:
|
| 619 |
+
|
| 620 |
+
if dropLOQ:
|
| 621 |
+
plot_df = dms_df[dms_df[ag] > min(dms_df[ag])]
|
| 622 |
+
#add back wt which is on LOQ for visualization
|
| 623 |
+
plot_df = pd.concat([plot_df, dms_df[dms_df['som_mut'] == 0]], ignore_index = True, axis = 0)
|
| 624 |
+
else:
|
| 625 |
+
plot_df = dms_df
|
| 626 |
+
|
| 627 |
+
p = single_cr_scatter(plot_df, ag_columns, x = ag, y = 'Ab-Ag', title = f'{title} against {ag}')
|
| 628 |
+
plots.append(p)
|
| 629 |
+
|
| 630 |
+
#experimental vs experimental
|
| 631 |
+
plot_df = dms_df
|
| 632 |
+
if dropLOQ:
|
| 633 |
+
for ag in ag_columns:
|
| 634 |
+
plot_df = plot_df[plot_df[ag] > min(plot_df[ag])]
|
| 635 |
+
#add back wt which is on LOQ for visualization
|
| 636 |
+
plot_df = pd.concat([plot_df, dms_df[dms_df['som_mut'] == 0]], ignore_index = True, axis = 0)
|
| 637 |
+
|
| 638 |
+
p_exp = single_cr_scatter(plot_df, ag_columns, x= ag_columns[0], y = ag_columns[1], title = 'Experimental Cross-Reactive Binding Landscape')
|
| 639 |
+
|
| 640 |
+
plots.append(p_exp)
|
| 641 |
+
p_exp.output_backend = "svg"
|
| 642 |
+
|
| 643 |
+
return plots
|
| 644 |
+
|
| 645 |
+
#plot correlation bars for cr antibodies: cr6261 and cr9114
|
| 646 |
+
def plot_LOQ_comparison(ab_name, unfilt_corrs, filt_corrs, palette = bokeh.palettes.Spectral6,):
|
| 647 |
+
unfilt_corrs['LOQ'] = ['Included'] * len(unfilt_corrs)
|
| 648 |
+
filt_corrs['LOQ'] = ['Excluded'] * len(filt_corrs)
|
| 649 |
+
combined_melt = pd.concat([unfilt_corrs,filt_corrs], ignore_index= True)
|
| 650 |
+
|
| 651 |
+
combined_melt['cats'] = combined_melt.apply(lambda x: (x["Target"], x["Input"], x["LOQ"]), axis = 1)
|
| 652 |
+
factors = list(combined_melt.cats)[::-1]
|
| 653 |
+
|
| 654 |
+
p = bokeh.plotting.figure(
|
| 655 |
+
height=850,
|
| 656 |
+
width=400,
|
| 657 |
+
x_axis_label="Spearman Correlation",
|
| 658 |
+
x_range=[-.25, 1],
|
| 659 |
+
y_range=bokeh.models.FactorRange(*factors),
|
| 660 |
+
tools="save",
|
| 661 |
+
title = f'{ab_name}, Impact of Ignoring Lower LOQ '
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
p.hbar(
|
| 665 |
+
source=combined_melt,
|
| 666 |
+
y="cats",
|
| 667 |
+
right="Correlation",
|
| 668 |
+
height=0.7,
|
| 669 |
+
line_color = 'black',
|
| 670 |
+
legend_field = 'LOQ',
|
| 671 |
+
# use the palette to colormap based on the the x[1:2] values
|
| 672 |
+
fill_color=factor_cmap(
|
| 673 |
+
'LOQ',
|
| 674 |
+
palette= palette,
|
| 675 |
+
factors = list(combined_melt['LOQ'].unique()),
|
| 676 |
+
start=1,
|
| 677 |
+
end=2)
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
p.ygrid.grid_line_color = None
|
| 681 |
+
p.y_range.range_padding = 0.1
|
| 682 |
+
p.legend.location = "bottom_right"
|
| 683 |
+
p.legend.orientation = "vertical"
|
| 684 |
+
p.yaxis.axis_label = "Model Input"
|
| 685 |
+
p.output_backend = "svg"
|
| 686 |
+
|
| 687 |
+
return p
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
if __name__ == '__main__':
|
| 692 |
+
|
| 693 |
+
datasets = [cr9114_dict, cr6261_dict, (g6HC_dict, g6LC_dict) ]
|
| 694 |
+
results = []
|
| 695 |
+
|
| 696 |
+
#compute with and without points on lower limit of quantitation ignored
|
| 697 |
+
for dropLOQ in [False, True]:
|
| 698 |
+
|
| 699 |
+
g6_corrs = []
|
| 700 |
+
all_corr_plots = []
|
| 701 |
+
|
| 702 |
+
for d in datasets:
|
| 703 |
+
if type(d) is tuple:
|
| 704 |
+
g6Hc, g6Lc = d
|
| 705 |
+
title = g6Hc['ab_name'] + ', ' + g6Hc['expt_type']
|
| 706 |
+
g6_combined = get_g6_corr(g6Hc, g6Lc, dropLOQ= dropLOQ)
|
| 707 |
+
|
| 708 |
+
all_corr_plots.append(plot_hbar(title, g6_combined, g6Lc['palette'], 'IF'))
|
| 709 |
+
|
| 710 |
+
#also plot scatter plots
|
| 711 |
+
scatter_fname = f"output/ab_mutagenesis_expts/g6_scatterplot_colorMuts{'_dropLOQ' if dropLOQ else ''}.html"
|
| 712 |
+
bokeh.plotting.output_file(scatter_fname)
|
| 713 |
+
g6_scatterMuts_plots = plot_g6_scatter(g6Hc['dms_df'], g6Lc['dms_df'], g6Hc['ag_columns'], dropLOQ= dropLOQ)
|
| 714 |
+
bokeh.io.show(bokeh.layouts.gridplot(g6_scatterMuts_plots, ncols = 2))
|
| 715 |
+
|
| 716 |
+
if (dropLOQ == False):
|
| 717 |
+
results.append(g6_combined)
|
| 718 |
+
|
| 719 |
+
else:
|
| 720 |
+
corr_df = get_corr(d['ab_name'], d['files'], d['dms_df'], d['ag_columns'], dropLOQ= dropLOQ)
|
| 721 |
+
title = d['ab_name'] + ', ' + d['expt_type']
|
| 722 |
+
|
| 723 |
+
all_corr_plots.append(plot_hbar(title, corr_df, d['palette'], 'IF'))
|
| 724 |
+
|
| 725 |
+
#also plot scatter plots for CR9114, CR6261
|
| 726 |
+
scatter_fname = f"output/ab_mutagenesis_expts/{d['ab_name']}_scatter_colorMuts{'_dropLOQ' if dropLOQ else ''}.html"
|
| 727 |
+
bokeh.plotting.output_file(scatter_fname)
|
| 728 |
+
cr_scatterMuts_plots = plot_CR_scatter_colorMuts(title, d['files'], d['dms_df'], d['ag_columns'], dropLOQ= dropLOQ)
|
| 729 |
+
bokeh.io.show(bokeh.layouts.gridplot(cr_scatterMuts_plots, ncols = 3))
|
| 730 |
+
|
| 731 |
+
#also scatter plots with datashading/dynamic spreading for CR9114, CR6261
|
| 732 |
+
scatter_fname = f"output/ab_mutagenesis_expts/{d['ab_name']}_scatter{'_dropLOQ' if dropLOQ else ''}.html"
|
| 733 |
+
bokeh.plotting.output_file(scatter_fname)
|
| 734 |
+
cr_scattter_plots = plot_CR_scatterplots(title, d['files'], d['dms_df'], d['ag_columns'], dropLOQ= dropLOQ)
|
| 735 |
+
bokeh.io.show(bokeh.layouts.gridplot(cr_scattter_plots, ncols = 3))
|
| 736 |
+
|
| 737 |
+
if (dropLOQ == False):
|
| 738 |
+
results.append(corr_df)
|
| 739 |
+
|
| 740 |
+
all_corr_fname = f"output/ab_mutagenesis_expts/benchmarks{'_dropLOQ' if dropLOQ else ''}.html"
|
| 741 |
+
bokeh.plotting.output_file(all_corr_fname)
|
| 742 |
+
bokeh.io.show(bokeh.layouts.gridplot(all_corr_plots, ncols = len(all_corr_plots)))
|
| 743 |
+
|
| 744 |
+
compareLOQ_fname = 'output/ab_mutagenesis_expts/LOQ_comparison.html'
|
| 745 |
+
bokeh.plotting.output_file(compareLOQ_fname)
|
| 746 |
+
compareLOQ_plots = []
|
| 747 |
+
for d in datasets:
|
| 748 |
+
if (type(d) != tuple) and (d['ab_name'].startswith('CR')):
|
| 749 |
+
corr_withLOQ = get_corr(d['ab_name'], d['files'], d['dms_df'], d['ag_columns'], )
|
| 750 |
+
corr_withoutLOQ = get_corr(d['ab_name'], d['files'], d['dms_df'], d['ag_columns'], dropLOQ= dropLOQ)
|
| 751 |
+
|
| 752 |
+
compareLOQ_plots.append(plot_LOQ_comparison(d['ab_name'], corr_withLOQ, corr_withoutLOQ , d['palette']))
|
| 753 |
+
bokeh.io.show(bokeh.layouts.gridplot(compareLOQ_plots, ncols = len(compareLOQ_plots)))
|
| 754 |
+
|
| 755 |
+
pd.concat(results, ignore_index= True).to_csv( 'output/ab_mutagenesis_expts/results.csv' , index = False)
|
model/plot_esm1v_benchmarks.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import bokeh.io
|
| 4 |
+
import bokeh.plotting
|
| 5 |
+
import bokeh.palettes
|
| 6 |
+
from bokeh.transform import factor_cmap
|
| 7 |
+
import datashader
|
| 8 |
+
import holoviews as hv
|
| 9 |
+
import holoviews.operation.datashader
|
| 10 |
+
hv.extension("bokeh")
|
| 11 |
+
|
| 12 |
+
import warnings
|
| 13 |
+
# Suppress FutureWarning messages
|
| 14 |
+
warnings.simplefilter(action='ignore',)
|
| 15 |
+
|
| 16 |
+
cr9114_dict = {
|
| 17 |
+
'ab_name' : 'CR9114',
|
| 18 |
+
'files' : {
|
| 19 |
+
'ESM-1v Ab-Ag': 'output/ab_mutagenesis_expts/cr9114/cr9114_esm1vAbAg_exp_data_maskMargLabeled.csv',
|
| 20 |
+
'ESM-1v Ab only': 'output/ab_mutagenesis_expts/cr9114/cr9114_esm1vbothchains_exp_data_maskMargLabeled.csv',
|
| 21 |
+
'ESM-1v Ab VH only': 'output/ab_mutagenesis_expts/cr9114/cr9114_exp_data_maskMargLabeled.csv',
|
| 22 |
+
},
|
| 23 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr9114/cr9114_exp_data.csv',
|
| 24 |
+
dtype = {'genotype': str},
|
| 25 |
+
).rename(columns={'h1_mean': 'H1', 'h3_mean' : 'H3'}),
|
| 26 |
+
'ag_columns': ['H1', 'H3'],
|
| 27 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 28 |
+
'palette': bokeh.palettes.Spectral6
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
cr6261_dict = {
|
| 32 |
+
'ab_name' : 'CR6261',
|
| 33 |
+
'files' : {
|
| 34 |
+
'ESM-1v Ab-Ag': 'output/ab_mutagenesis_expts/cr6261/cr6261_esm1vAbAg_exp_data_maskMargLabeled.csv',
|
| 35 |
+
'ESM-1v Ab only': 'output/ab_mutagenesis_expts/cr6261/cr6261_esm1vbothchains_exp_data_maskMargLabeled.csv',
|
| 36 |
+
'ESM-1v Ab VH only': 'output/ab_mutagenesis_expts/cr6261/cr6261_exp_data_maskMargLabeled.csv',
|
| 37 |
+
},
|
| 38 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr6261/cr6261_exp_data.csv',
|
| 39 |
+
dtype={'genotype': str},
|
| 40 |
+
).rename(columns={'h1_mean': 'H1', 'h9_mean' : 'H9'}),
|
| 41 |
+
'ag_columns': ['H1', 'H9'],
|
| 42 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 43 |
+
'palette': bokeh.palettes.Dark2_6,
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
g6LC_dict = {
|
| 47 |
+
'ab_name' : 'g6',
|
| 48 |
+
'files' : {
|
| 49 |
+
'LM Ab-Ag': 'output/ab_mutagenesis_expts/g6/g6Lc_esm1vAbAg_exp_data_maskMargLabeled.csv',
|
| 50 |
+
'LM Ab only': 'output/ab_mutagenesis_expts/g6/g6Lc_esm1vbothchains_exp_data_maskMargLabeled.csv',
|
| 51 |
+
'LM Ab VH/VL only': 'output/ab_mutagenesis_expts/g6/g6Lc_exp_data_maskMargLabeled.csv',
|
| 52 |
+
},
|
| 53 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv'),
|
| 54 |
+
'ag_columns': ['norm_binding'],
|
| 55 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 56 |
+
'palette': bokeh.palettes.Pastel1_6,
|
| 57 |
+
'chain': 'VL'
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
g6HC_dict = {
|
| 61 |
+
'ab_name' : 'g6',
|
| 62 |
+
'files' : {
|
| 63 |
+
'LM Ab-Ag': 'output/ab_mutagenesis_expts/g6/g6Hc_esm1vAbAg_exp_data_maskMargLabeled.csv',
|
| 64 |
+
'LM Ab only': 'output/ab_mutagenesis_expts/g6/g6Hc_esm1vbothchains_exp_data_maskMargLabeled.csv',
|
| 65 |
+
'LM Ab VH/VL only': 'output/ab_mutagenesis_expts/g6/g6Hc_exp_data_maskMargLabeled.csv',
|
| 66 |
+
},
|
| 67 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv'),
|
| 68 |
+
'ag_columns': ['norm_binding'],
|
| 69 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 70 |
+
'palette': bokeh.palettes.Pastel1_4,
|
| 71 |
+
'chain': 'VH'
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
def apply_mask_and_average(scores, genotype):
|
| 75 |
+
#scores here should already be a in log space
|
| 76 |
+
|
| 77 |
+
if '1' in genotype:
|
| 78 |
+
masked_list = []
|
| 79 |
+
for s, mask_char in zip(scores, genotype):
|
| 80 |
+
if mask_char == '1':
|
| 81 |
+
masked_list.append(s)
|
| 82 |
+
multi_avg = np.mean(masked_list)
|
| 83 |
+
else:
|
| 84 |
+
#if genotype is all zeros = wt
|
| 85 |
+
multi_avg = 0
|
| 86 |
+
|
| 87 |
+
return multi_avg
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def transform_single_to_multi( dms_df, singleMuts_df, condition, sort_col, ascending ):
|
| 91 |
+
multi_scores = []
|
| 92 |
+
#use input parameter as method bc key is kwarg in pd.sort_values
|
| 93 |
+
#method should be either esm1v or abysis or AbLang
|
| 94 |
+
|
| 95 |
+
#for cr sorts the single mutation dataframe in residue order, ie binary '10000' before '00010'. ascending = False
|
| 96 |
+
#for g6 sorts the single mutation dataframe in position order, ie residue index. ascending = True
|
| 97 |
+
|
| 98 |
+
singleMuts_sorted = singleMuts_df.sort_values(sort_col, key=lambda x: x.astype(int), ascending=ascending)
|
| 99 |
+
singles_scores = singleMuts_sorted[condition].to_list()
|
| 100 |
+
|
| 101 |
+
for g in dms_df['genotype']:
|
| 102 |
+
multi_scores.append(apply_mask_and_average(singles_scores, g ))
|
| 103 |
+
|
| 104 |
+
return multi_scores
|
| 105 |
+
|
| 106 |
+
#get melted correlations matrix of structure input x target antigen, for a given antibody
|
| 107 |
+
def get_corr(ab_name, files, dms_df, ag_columns, dropLOQ = False ):
|
| 108 |
+
|
| 109 |
+
#retreive correlations from abysis and esm1v scores
|
| 110 |
+
for key, filepath in files.items():
|
| 111 |
+
singleMuts_df = pd.read_csv(filepath)
|
| 112 |
+
# Get the columns for all esm1v models
|
| 113 |
+
esm_columns = [col for col in singleMuts_df.columns if col.startswith('esm')]
|
| 114 |
+
# Calculate the average for each esm column
|
| 115 |
+
esm_avg_values = singleMuts_df[esm_columns].mean(axis=1)
|
| 116 |
+
singleMuts_df[key] = esm_avg_values
|
| 117 |
+
if ab_name == 'g6':
|
| 118 |
+
#g6 data set is only single mutations
|
| 119 |
+
dms_df[key] = esm_avg_values
|
| 120 |
+
else:
|
| 121 |
+
#cr datasets are multiple mutations
|
| 122 |
+
dms_df[key] = transform_single_to_multi(dms_df, singleMuts_df, key, 'genotype', ascending = False)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
conditions = list(files.keys())
|
| 126 |
+
|
| 127 |
+
if not dropLOQ:
|
| 128 |
+
correlations = dms_df[ag_columns + conditions].corr(method='spearman')
|
| 129 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 130 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 131 |
+
|
| 132 |
+
# Melt the correlations DataFrame and rename the index column
|
| 133 |
+
melted_correlations = (
|
| 134 |
+
correlations
|
| 135 |
+
.reset_index()
|
| 136 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 137 |
+
.rename(columns={'index': 'Input'})
|
| 138 |
+
)
|
| 139 |
+
melted_correlations['Method'] = ['LM' for i in melted_correlations['Input']]
|
| 140 |
+
|
| 141 |
+
return melted_correlations
|
| 142 |
+
#if we wish to drop the samples on the lower limit of quantitation, drop samples from each ag group individually and compute correlation with IF log_like
|
| 143 |
+
else:
|
| 144 |
+
all_ag_melted = pd.DataFrame({})
|
| 145 |
+
for ag in ag_columns:
|
| 146 |
+
filt_df = dms_df[dms_df[ag] > min(dms_df[ag])]
|
| 147 |
+
|
| 148 |
+
correlations = filt_df[[ag] + conditions].corr(method='spearman')
|
| 149 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 150 |
+
correlations = correlations.drop(ag, axis= 0)
|
| 151 |
+
|
| 152 |
+
# Melt the correlations DataFrame and rename the index column
|
| 153 |
+
melted_correlations = (
|
| 154 |
+
correlations
|
| 155 |
+
.reset_index()
|
| 156 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 157 |
+
).rename(columns={'index': 'Input'})
|
| 158 |
+
|
| 159 |
+
all_ag_melted = pd.concat([all_ag_melted, melted_correlations], ignore_index= True)
|
| 160 |
+
all_ag_melted['Method'] = ['LM' for i in all_ag_melted['Input']]
|
| 161 |
+
|
| 162 |
+
return all_ag_melted
|
| 163 |
+
|
| 164 |
+
#get melted correlations matrix of structure input x target antigen, for a given antibody
|
| 165 |
+
def get_g6_corr(g6Hc_dict, g6LC_dict, dropLOQ = False ):
|
| 166 |
+
|
| 167 |
+
ab_name = 'g6'
|
| 168 |
+
ag_columns = g6LC_dict['ag_columns']
|
| 169 |
+
vh_and_vl_dms_df = pd.DataFrame({})
|
| 170 |
+
|
| 171 |
+
for d in [g6HC_dict, g6LC_dict]:
|
| 172 |
+
|
| 173 |
+
files = d['files']
|
| 174 |
+
dms_df = d['dms_df']
|
| 175 |
+
|
| 176 |
+
#retreive correlations from abysis, ablang, and esm1v scores
|
| 177 |
+
for key, filepath in files.items():
|
| 178 |
+
singleMuts_df = pd.read_csv(filepath)
|
| 179 |
+
# Get the columns for all esm1v models
|
| 180 |
+
esm_columns = [col for col in singleMuts_df.columns if col.startswith('esm')]
|
| 181 |
+
# Calculate the average for each esm column
|
| 182 |
+
esm_avg_values = singleMuts_df[esm_columns].mean(axis=1)
|
| 183 |
+
singleMuts_df[key] = esm_avg_values
|
| 184 |
+
if ab_name == 'g6':
|
| 185 |
+
#g6 data set is only single mutations
|
| 186 |
+
dms_df[key] = esm_avg_values
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
vh_and_vl_dms_df = pd.concat([vh_and_vl_dms_df, dms_df], ignore_index= True)
|
| 190 |
+
|
| 191 |
+
conditions = list(files.keys())
|
| 192 |
+
|
| 193 |
+
#
|
| 194 |
+
if not dropLOQ:
|
| 195 |
+
correlations = vh_and_vl_dms_df[ag_columns + conditions].corr(method='spearman')
|
| 196 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 197 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 198 |
+
|
| 199 |
+
# Melt the correlations DataFrame and rename the index column
|
| 200 |
+
melted_correlations = (
|
| 201 |
+
correlations
|
| 202 |
+
.reset_index()
|
| 203 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 204 |
+
.rename(columns={'index': 'Input'})
|
| 205 |
+
)
|
| 206 |
+
melted_correlations['Method'] = ['LM' for i in melted_correlations['Input']]
|
| 207 |
+
melted_correlations['Target'] = ['VEGF-A'] * len(melted_correlations)
|
| 208 |
+
|
| 209 |
+
return melted_correlations
|
| 210 |
+
#if we wish to drop the samples on the lower limit of quantitation, drop samples from each ag group individually and compute correlation with IF log_like
|
| 211 |
+
else:
|
| 212 |
+
all_ag_melted = pd.DataFrame({})
|
| 213 |
+
for ag in ag_columns:
|
| 214 |
+
filt_df = vh_and_vl_dms_df[vh_and_vl_dms_df[ag] > min(vh_and_vl_dms_df[ag])]
|
| 215 |
+
|
| 216 |
+
correlations = filt_df[[ag] + conditions].corr(method='spearman')
|
| 217 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 218 |
+
correlations = correlations.drop(ag, axis= 0)
|
| 219 |
+
|
| 220 |
+
# Melt the correlations DataFrame and rename the index column
|
| 221 |
+
melted_correlations = (
|
| 222 |
+
correlations
|
| 223 |
+
.reset_index()
|
| 224 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 225 |
+
).rename(columns={'index': 'Input'})
|
| 226 |
+
|
| 227 |
+
all_ag_melted = pd.concat([all_ag_melted, melted_correlations], ignore_index= True)
|
| 228 |
+
all_ag_melted['Method'] = ['LM' for i in melted_correlations['Input']]
|
| 229 |
+
melted_correlations['Target'] = ['VEGF-A'] * len(melted_correlations)
|
| 230 |
+
|
| 231 |
+
return all_ag_melted
|
| 232 |
+
|
| 233 |
+
#plot correlation bars for cr antibodies: cr6261 and cr9114
|
| 234 |
+
def plot_hbar(title, melted_correlations, palette = bokeh.palettes.Spectral6, compare = 'model'):
|
| 235 |
+
|
| 236 |
+
if compare == 'model':
|
| 237 |
+
melted_correlations.replace('Ab-Ag','InverseFolding', inplace = True)
|
| 238 |
+
color_by = 'Input'
|
| 239 |
+
elif compare == 'IF':
|
| 240 |
+
color_by = 'Method'
|
| 241 |
+
else:
|
| 242 |
+
color_by = 'Target'
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
melted_correlations['cats'] = melted_correlations.apply(lambda x: (x["Target"], x["Input"]), axis = 1)
|
| 246 |
+
factors = list(melted_correlations.cats)[::-1]
|
| 247 |
+
|
| 248 |
+
p = bokeh.plotting.figure(
|
| 249 |
+
height=340,
|
| 250 |
+
width=440,
|
| 251 |
+
x_axis_label="Spearman Correlation",
|
| 252 |
+
x_range=[0, 1],
|
| 253 |
+
y_range=bokeh.models.FactorRange(*factors),
|
| 254 |
+
tools="save",
|
| 255 |
+
title = title
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
p.hbar(
|
| 260 |
+
source=melted_correlations,
|
| 261 |
+
y="cats",
|
| 262 |
+
right="Correlation",
|
| 263 |
+
height=0.6,
|
| 264 |
+
line_color = 'black',
|
| 265 |
+
legend_field = color_by,
|
| 266 |
+
# use the palette to colormap based on the the x[1:2] values
|
| 267 |
+
# fill_color=factor_cmap(
|
| 268 |
+
# color_by,
|
| 269 |
+
# palette= palette,
|
| 270 |
+
# factors = list(melted_correlations[color_by].unique()),
|
| 271 |
+
# start=1,
|
| 272 |
+
# end=3)
|
| 273 |
+
fill_color=bokeh.palettes.Dark2_6[1]
|
| 274 |
+
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
#labels_df = melted_correlations[melted_correlations['Input'].isin(['Ab-Ag','ESM-1v', 'abYsis', 'InverseFold'])]
|
| 278 |
+
labels_df = melted_correlations
|
| 279 |
+
labels_df['corr_str'] = labels_df['Correlation'].apply(lambda x: round(x, 2)).astype(str)
|
| 280 |
+
labels_source = bokeh.models.ColumnDataSource(labels_df)
|
| 281 |
+
|
| 282 |
+
labels = bokeh.models.LabelSet(x='Correlation', y='cats', text='corr_str',text_font_size = "10px",
|
| 283 |
+
x_offset=12, y_offset=-5, source=labels_source, render_mode='canvas')
|
| 284 |
+
|
| 285 |
+
p.ygrid.grid_line_color = None
|
| 286 |
+
p.y_range.range_padding = 0.1
|
| 287 |
+
p.add_layout(labels)
|
| 288 |
+
p.legend.visible = False
|
| 289 |
+
|
| 290 |
+
p.output_backend = "svg"
|
| 291 |
+
return p
|
| 292 |
+
|
| 293 |
+
if __name__ == '__main__':
|
| 294 |
+
datasets = [cr9114_dict, cr6261_dict, (g6HC_dict, g6LC_dict) ]
|
| 295 |
+
|
| 296 |
+
#compute with and without points on lower limit of quantitation ignored
|
| 297 |
+
for dropLOQ in [False]:
|
| 298 |
+
|
| 299 |
+
all_corr_plots = []
|
| 300 |
+
|
| 301 |
+
for d in datasets:
|
| 302 |
+
if type(d) is tuple:
|
| 303 |
+
g6Hc, g6Lc = d
|
| 304 |
+
title = g6Hc['ab_name'] + ', ' + g6Hc['expt_type']
|
| 305 |
+
g6_combined = get_g6_corr(g6Hc, g6Lc, dropLOQ= dropLOQ)
|
| 306 |
+
|
| 307 |
+
all_corr_plots.append(plot_hbar(title, g6_combined, g6Lc['palette'], 'IF'))
|
| 308 |
+
|
| 309 |
+
else:
|
| 310 |
+
corr_df = get_corr(d['ab_name'], d['files'], d['dms_df'], d['ag_columns'], dropLOQ= dropLOQ)
|
| 311 |
+
title = d['ab_name'] + ', ' + d['expt_type']
|
| 312 |
+
all_corr_plots.append(plot_hbar(title, corr_df, d['palette'], 'Target'))
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
all_corr_fname = f"output/ab_mutagenesis_expts/esm1v_benchmarks{'_dropLOQ' if dropLOQ else ''}.html"
|
| 316 |
+
bokeh.plotting.output_file(all_corr_fname)
|
| 317 |
+
bokeh.io.show(bokeh.layouts.gridplot(all_corr_plots, ncols = len(all_corr_plots)))
|
model/plot_mpnn_benchmarks.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import bokeh.io
|
| 4 |
+
import bokeh.plotting
|
| 5 |
+
import bokeh.palettes
|
| 6 |
+
from bokeh.transform import factor_cmap
|
| 7 |
+
import datashader
|
| 8 |
+
import holoviews as hv
|
| 9 |
+
import holoviews.operation.datashader
|
| 10 |
+
import os
|
| 11 |
+
from natsort import natsorted
|
| 12 |
+
|
| 13 |
+
hv.extension("bokeh")
|
| 14 |
+
|
| 15 |
+
import warnings
|
| 16 |
+
# Suppress FutureWarning messages
|
| 17 |
+
warnings.simplefilter(action='ignore',)
|
| 18 |
+
|
| 19 |
+
cr9114_dict = {
|
| 20 |
+
'ab_name' : 'CR9114',
|
| 21 |
+
'files' : {
|
| 22 |
+
'ESM-IF1': 'output/ab_mutagenesis_expts/cr9114/4fqi_ablh_scores.csv',
|
| 23 |
+
'ProteinMPNN': 'output/ab_mutagenesis_expts/cr9114/mpnn/score_only/',
|
| 24 |
+
|
| 25 |
+
},
|
| 26 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr9114/cr9114_exp_data.csv',
|
| 27 |
+
dtype = {'genotype': str},
|
| 28 |
+
).rename(columns={'h1_mean': 'H1', 'h3_mean' : 'H3'}),
|
| 29 |
+
'ag_columns': ['H1', 'H3'],
|
| 30 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 31 |
+
'palette': bokeh.palettes.Spectral6
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
cr6261_dict = {
|
| 35 |
+
'ab_name' : 'CR6261',
|
| 36 |
+
'files' : {
|
| 37 |
+
'ESM-IF1': 'output/ab_mutagenesis_expts/cr6261/3gbn_ablh_scores.csv',
|
| 38 |
+
'ProteinMPNN': 'output/ab_mutagenesis_expts/cr6261/mpnn/score_only/'
|
| 39 |
+
},
|
| 40 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/cr6261/cr6261_exp_data.csv',
|
| 41 |
+
dtype={'genotype': str},
|
| 42 |
+
).rename(columns={'h1_mean': 'H1', 'h9_mean' : 'H9'}),
|
| 43 |
+
'ag_columns': ['H1', 'H9'],
|
| 44 |
+
'expt_type': 'Combinatorial Mutagenesis for Affinity',
|
| 45 |
+
'palette': bokeh.palettes.Dark2_6,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
g6LC_dict = {
|
| 50 |
+
'ab_name' : 'g6',
|
| 51 |
+
'files' : {
|
| 52 |
+
'ESM-IF1':'output/ab_mutagenesis_expts/g6/2fjg_vlh_lc_scores.csv',
|
| 53 |
+
'ProteinMPNN':'output/ab_mutagenesis_expts/g6/proteinMpnnLC/score_only/',
|
| 54 |
+
|
| 55 |
+
},
|
| 56 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv'),
|
| 57 |
+
'ag_columns': ['norm_binding'],
|
| 58 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 59 |
+
'palette': bokeh.palettes.Pastel1_6,
|
| 60 |
+
'chain': 'VL'
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
g6HC_dict = {
|
| 64 |
+
'ab_name' : 'g6',
|
| 65 |
+
'files' : {
|
| 66 |
+
'ESM-IF1': 'output/ab_mutagenesis_expts/g6/2fjg_vlh_hc_scores.csv',
|
| 67 |
+
'ProteinMPNN':'output/ab_mutagenesis_expts/g6/proteinMpnnHC/score_only/',
|
| 68 |
+
},
|
| 69 |
+
'dms_df' : pd.read_csv('data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv'),
|
| 70 |
+
'ag_columns': ['norm_binding'],
|
| 71 |
+
'expt_type': 'Deep Mutational Scan for Binding',
|
| 72 |
+
'palette': bokeh.palettes.Pastel1_4,
|
| 73 |
+
'chain': 'VH'
|
| 74 |
+
}
|
| 75 |
+
def combine_mpnn_scores(dir):
|
| 76 |
+
global_scores = []
|
| 77 |
+
npz_files = [f for f in os.listdir(dir) if f.endswith('.npz')]
|
| 78 |
+
npz_files = natsorted(npz_files)[:-1]
|
| 79 |
+
|
| 80 |
+
for fname in npz_files:
|
| 81 |
+
file_path = os.path.join(dir, fname)
|
| 82 |
+
data = np.load(file_path)
|
| 83 |
+
global_scores.append(-1* data['global_score'][0])
|
| 84 |
+
data.close()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
return global_scores
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
#get melted correlations matrix of structure input x target antigen, for a given antibody
|
| 91 |
+
def get_corr(ab_name, files, dms_df, ag_columns,):
|
| 92 |
+
|
| 93 |
+
#retreive correlations from inverse fold scores
|
| 94 |
+
for key, filepath in files.items():
|
| 95 |
+
column_label = key
|
| 96 |
+
if key == 'ProteinMPNN':
|
| 97 |
+
scores = combine_mpnn_scores(filepath)
|
| 98 |
+
dms_df[column_label] = scores
|
| 99 |
+
elif key == 'ESM-IF1':
|
| 100 |
+
df = pd.read_csv(files['ESM-IF1']) # Read the CSV file into a DataFrame
|
| 101 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 102 |
+
dms_df[column_label] = log_likelihood_column
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
conditions = list(files.keys())
|
| 106 |
+
|
| 107 |
+
correlations = dms_df[ag_columns + conditions].corr(method='spearman')
|
| 108 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 109 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 110 |
+
|
| 111 |
+
# Melt the correlations DataFrame and rename the index column
|
| 112 |
+
melted_correlations = (
|
| 113 |
+
correlations
|
| 114 |
+
.reset_index()
|
| 115 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 116 |
+
.rename(columns={'index': 'Input'})
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
print(melted_correlations)
|
| 120 |
+
return melted_correlations
|
| 121 |
+
|
| 122 |
+
return all_ag_melted
|
| 123 |
+
|
| 124 |
+
#get melted correlations matrix of structure input x target antigen, for g6 antibody
|
| 125 |
+
def get_g6_corr(g6Hc_dict, g6LC_dict, dropLOQ = False ):
|
| 126 |
+
|
| 127 |
+
ab_name = 'g6'
|
| 128 |
+
ag_columns = g6LC_dict['ag_columns']
|
| 129 |
+
vh_and_vl_dms_df = pd.DataFrame({})
|
| 130 |
+
|
| 131 |
+
for d in [g6HC_dict, g6LC_dict]:
|
| 132 |
+
|
| 133 |
+
files = d['files']
|
| 134 |
+
dms_df = d['dms_df']
|
| 135 |
+
|
| 136 |
+
#retreive correlations from abysis, ablang, and esm1v scores
|
| 137 |
+
for key, filepath in files.items():
|
| 138 |
+
column_label = key
|
| 139 |
+
if key == 'ProteinMPNN':
|
| 140 |
+
scores = combine_mpnn_scores(filepath)
|
| 141 |
+
dms_df[column_label] = scores
|
| 142 |
+
elif key == 'ESM-IF1':
|
| 143 |
+
df = pd.read_csv(files['ESM-IF1']) # Read the CSV file into a DataFrame
|
| 144 |
+
log_likelihood_column = df['log_likelihood'] # Extract the 'log_likelihood' column
|
| 145 |
+
dms_df[column_label] = log_likelihood_column
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
vh_and_vl_dms_df = pd.concat([vh_and_vl_dms_df, dms_df], ignore_index= True)
|
| 149 |
+
|
| 150 |
+
conditions = list(files.keys())
|
| 151 |
+
|
| 152 |
+
correlations = vh_and_vl_dms_df[ag_columns + conditions].corr(method='spearman')
|
| 153 |
+
correlations = correlations.drop(conditions, axis= 1)
|
| 154 |
+
correlations = correlations.drop(ag_columns, axis= 0)
|
| 155 |
+
|
| 156 |
+
# Melt the correlations DataFrame and rename the index column
|
| 157 |
+
melted_correlations = (
|
| 158 |
+
correlations
|
| 159 |
+
.reset_index()
|
| 160 |
+
.melt(id_vars='index', var_name='Target', value_name='Correlation')
|
| 161 |
+
.rename(columns={'index': 'Input'})
|
| 162 |
+
)
|
| 163 |
+
melted_correlations['Target'] = ['VEGF-A'] * len(melted_correlations)
|
| 164 |
+
|
| 165 |
+
print(melted_correlations)
|
| 166 |
+
return melted_correlations
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
#plot correlation bars for cr antibodies: cr6261 and cr9114
|
| 170 |
+
def plot_hbar(title, melted_correlations, palette = bokeh.palettes.Spectral6, ):
|
| 171 |
+
|
| 172 |
+
melted_correlations['cats'] = melted_correlations.apply(lambda x: (x["Target"], x["Input"]), axis = 1)
|
| 173 |
+
factors = list(melted_correlations.cats)[::-1]
|
| 174 |
+
|
| 175 |
+
p = bokeh.plotting.figure(
|
| 176 |
+
height=340,
|
| 177 |
+
width=440,
|
| 178 |
+
x_axis_label="Spearman Correlation",
|
| 179 |
+
x_range=[0, 1],
|
| 180 |
+
y_range=bokeh.models.FactorRange(*factors),
|
| 181 |
+
tools="save",
|
| 182 |
+
title = title
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
p.hbar(
|
| 187 |
+
source=melted_correlations,
|
| 188 |
+
y="cats",
|
| 189 |
+
right="Correlation",
|
| 190 |
+
height=0.6,
|
| 191 |
+
line_color = 'black',
|
| 192 |
+
fill_color=bokeh.palettes.Dark2_6[0]
|
| 193 |
+
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
labels_df = melted_correlations
|
| 197 |
+
labels_df['corr_str'] = labels_df['Correlation'].apply(lambda x: round(x, 2)).astype(str)
|
| 198 |
+
labels_source = bokeh.models.ColumnDataSource(labels_df)
|
| 199 |
+
|
| 200 |
+
labels = bokeh.models.LabelSet(x='Correlation', y='cats', text='corr_str',text_font_size = "10px",
|
| 201 |
+
x_offset=12, y_offset=-5, source=labels_source, render_mode='canvas')
|
| 202 |
+
|
| 203 |
+
p.ygrid.grid_line_color = None
|
| 204 |
+
p.y_range.range_padding = 0.1
|
| 205 |
+
p.add_layout(labels)
|
| 206 |
+
p.legend.visible = False
|
| 207 |
+
|
| 208 |
+
p.output_backend = "svg"
|
| 209 |
+
return p
|
| 210 |
+
|
| 211 |
+
if __name__ == '__main__':
|
| 212 |
+
datasets = [cr9114_dict, cr6261_dict, (g6HC_dict, g6LC_dict) ]
|
| 213 |
+
|
| 214 |
+
all_corr_plots = []
|
| 215 |
+
|
| 216 |
+
for d in datasets:
|
| 217 |
+
if type(d) is tuple:
|
| 218 |
+
g6Hc, g6Lc = d
|
| 219 |
+
title = g6Hc['ab_name'] + ', ' + g6Hc['expt_type']
|
| 220 |
+
g6_combined = get_g6_corr(g6Hc, g6Lc)
|
| 221 |
+
|
| 222 |
+
all_corr_plots.append(plot_hbar(title, g6_combined, g6Lc['palette']))
|
| 223 |
+
|
| 224 |
+
else:
|
| 225 |
+
corr_df = get_corr(d['ab_name'], d['files'], d['dms_df'], d['ag_columns'])
|
| 226 |
+
title = d['ab_name'] + ', ' + d['expt_type']
|
| 227 |
+
all_corr_plots.append(plot_hbar(title, corr_df, d['palette']))
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
all_corr_fname = f"output/ab_mutagenesis_expts/mpnn_benchmarks.html"
|
| 231 |
+
bokeh.plotting.output_file(all_corr_fname)
|
| 232 |
+
bokeh.io.show(bokeh.layouts.gridplot(all_corr_plots, ncols = len(all_corr_plots)))
|
model/score_log_likelihoods.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from biotite.sequence.io.fasta import FastaFile, get_sequences
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
import warnings
|
| 9 |
+
import os
|
| 10 |
+
from dms_utils import deep_mutational_scan
|
| 11 |
+
import esm
|
| 12 |
+
import pandas as pd
|
| 13 |
+
from multichain_util import extract_coords_from_complex, _concatenate_coords, _concatenate_seqs, score_sequence_in_complex
|
| 14 |
+
from util import get_sequence_loss, load_structure, load_coords, score_sequence, extract_coords_from_structure
|
| 15 |
+
|
| 16 |
+
def get_native_seq(pdbfile, chain):
|
| 17 |
+
structure = load_structure(pdbfile, chain)
|
| 18 |
+
_ , native_seq = extract_coords_from_structure(structure)
|
| 19 |
+
return native_seq
|
| 20 |
+
|
| 21 |
+
def score_singlechain_backbone(model, alphabet, args):
|
| 22 |
+
if torch.cuda.is_available() and not args.nogpu:
|
| 23 |
+
model = model.cuda()
|
| 24 |
+
print("Transferred model to GPU")
|
| 25 |
+
|
| 26 |
+
coords, native_seq = load_coords(args.pdbfile, args.chain)
|
| 27 |
+
print('Native sequence loaded from structure file:')
|
| 28 |
+
print(native_seq)
|
| 29 |
+
print('\n')
|
| 30 |
+
ll, _ = score_sequence(
|
| 31 |
+
model, alphabet, coords, native_seq)
|
| 32 |
+
print('Native sequence')
|
| 33 |
+
print(f'Log likelihood: {ll:.2f}')
|
| 34 |
+
print(f'Perplexity: {np.exp(-ll):.2f}')
|
| 35 |
+
print('\nScoring variant sequences from sequence file..\n')
|
| 36 |
+
infile = FastaFile()
|
| 37 |
+
infile.read(args.seqpath)
|
| 38 |
+
seqs = get_sequences(infile)
|
| 39 |
+
Path(args.outpath).parent.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
with open(args.outpath, 'w') as fout:
|
| 41 |
+
fout.write('seqid,log_likelihood\n')
|
| 42 |
+
for header, seq in tqdm(seqs.items()):
|
| 43 |
+
ll, _ = score_sequence(
|
| 44 |
+
model, alphabet, coords, str(seq))
|
| 45 |
+
fout.write(header + ',' + str(ll) + '\n')
|
| 46 |
+
print(f'Results saved to {args.outpath}')
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def score_multichain_backbone(model, alphabet, args):
|
| 50 |
+
if torch.cuda.is_available() and not args.nogpu:
|
| 51 |
+
model = model.cuda()
|
| 52 |
+
print("Transferred model to GPU")
|
| 53 |
+
|
| 54 |
+
structure = load_structure(args.pdbfile)
|
| 55 |
+
coords, native_seqs = extract_coords_from_complex(structure)
|
| 56 |
+
target_chain_id = args.chain
|
| 57 |
+
native_seq = native_seqs[target_chain_id]
|
| 58 |
+
order = args.order
|
| 59 |
+
|
| 60 |
+
print('Native sequence loaded from structure file:')
|
| 61 |
+
print(native_seq)
|
| 62 |
+
print('\n')
|
| 63 |
+
|
| 64 |
+
ll_complex, ll_targetchain = score_sequence_in_complex(
|
| 65 |
+
model,
|
| 66 |
+
alphabet,
|
| 67 |
+
coords,
|
| 68 |
+
native_seqs,
|
| 69 |
+
target_chain_id,
|
| 70 |
+
native_seq,
|
| 71 |
+
order=order,
|
| 72 |
+
)
|
| 73 |
+
print('Native sequence')
|
| 74 |
+
print(f'Log likelihood of complex: {ll_complex:.2f}')
|
| 75 |
+
print(f'Log likelihood of target chain: {ll_targetchain:.2f}')
|
| 76 |
+
print(f'Perplexity: {np.exp(ll_complex):.2f}')
|
| 77 |
+
|
| 78 |
+
print('\nScoring variant sequences from sequence file..\n')
|
| 79 |
+
infile = FastaFile()
|
| 80 |
+
infile.read(args.seqpath)
|
| 81 |
+
seqs = get_sequences(infile)
|
| 82 |
+
Path(args.outpath).parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
with open(args.outpath, 'w') as fout:
|
| 84 |
+
fout.write('seqid,log_likelihood, log_likelihood_target\n')
|
| 85 |
+
for header, seq in tqdm(seqs.items()):
|
| 86 |
+
ll_complex, ll_targetchain = score_sequence_in_complex(
|
| 87 |
+
model,
|
| 88 |
+
alphabet,
|
| 89 |
+
coords,
|
| 90 |
+
native_seqs,
|
| 91 |
+
target_chain_id,
|
| 92 |
+
str(seq),
|
| 93 |
+
order=order,
|
| 94 |
+
)
|
| 95 |
+
fout.write(header + ',' + str(ll_complex) + ',' + str(ll_targetchain) + '\n')
|
| 96 |
+
print(f'Results saved to {args.outpath}')
|
| 97 |
+
|
| 98 |
+
def get_model_checkpoint_path(filename):
|
| 99 |
+
# Expanding the user's home directory
|
| 100 |
+
return os.path.expanduser(f"~/.cache/torch/hub/checkpoints/{filename}")
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
parser = argparse.ArgumentParser(
|
| 104 |
+
description='Score sequences based on a given structure.'
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
'pdbfile', type=str,
|
| 108 |
+
help='input filepath, either .pdb or .cif',
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
'--seqpath', type=str,
|
| 112 |
+
help='input filepath for variant sequences in a .fasta file',
|
| 113 |
+
)
|
| 114 |
+
parser.add_argument(
|
| 115 |
+
'--outpath', type=str,
|
| 116 |
+
help='output filepath for scores of variant sequences',
|
| 117 |
+
)
|
| 118 |
+
parser.add_argument(
|
| 119 |
+
'--chain', type=str,
|
| 120 |
+
help='chain id for the chain of interest', default='A',
|
| 121 |
+
)
|
| 122 |
+
parser.set_defaults(multichain_backbone=True)
|
| 123 |
+
parser.add_argument(
|
| 124 |
+
'--multichain-backbone', action='store_true',
|
| 125 |
+
help='use the backbones of all chains in the input for conditioning'
|
| 126 |
+
)
|
| 127 |
+
parser.add_argument(
|
| 128 |
+
'--order', type=str, default=None,
|
| 129 |
+
help='for multichain, specify order'
|
| 130 |
+
)
|
| 131 |
+
parser.add_argument(
|
| 132 |
+
'--singlechain-backbone', dest='multichain_backbone',
|
| 133 |
+
action='store_false',
|
| 134 |
+
help='use the backbone of only target chain in the input for conditioning'
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--nogpu", action="store_true",
|
| 139 |
+
help="Do not use GPU even if available"
|
| 140 |
+
)
|
| 141 |
+
args = parser.parse_args()
|
| 142 |
+
|
| 143 |
+
if args.outpath is None:
|
| 144 |
+
args.outpath = f'output/{args.pdbfile[:-4]}-chain{args.chain}_scores.csv'
|
| 145 |
+
|
| 146 |
+
model_checkpoint_path = get_model_checkpoint_path('esm_if1_20220410.pt')
|
| 147 |
+
with warnings.catch_warnings():
|
| 148 |
+
warnings.simplefilter('ignore', UserWarning)
|
| 149 |
+
model, alphabet = esm.pretrained.load_model_and_alphabet( \
|
| 150 |
+
model_checkpoint_path \
|
| 151 |
+
)
|
| 152 |
+
model = model.eval()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if args.multichain_backbone:
|
| 156 |
+
score_multichain_backbone(model, alphabet, args)
|
| 157 |
+
else:
|
| 158 |
+
score_singlechain_backbone(model, alphabet, args)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == '__main__':
|
| 162 |
+
main()
|
model/util.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import math
|
| 3 |
+
import biotite.structure
|
| 4 |
+
from biotite.structure.io import pdbx, pdb
|
| 5 |
+
from biotite.structure.residues import get_residues
|
| 6 |
+
from biotite.structure import filter_backbone
|
| 7 |
+
from biotite.structure import get_chains
|
| 8 |
+
from biotite.sequence import ProteinSequence
|
| 9 |
+
import numpy as np
|
| 10 |
+
from scipy.spatial import transform
|
| 11 |
+
from scipy.stats import special_ortho_group
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
import torch.utils.data as data
|
| 16 |
+
from typing import Sequence, Tuple, List
|
| 17 |
+
from esm.data import BatchConverter
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_structure(fpath, chain=None):
|
| 21 |
+
"""
|
| 22 |
+
Args:
|
| 23 |
+
fpath: filepath to either pdb or cif file
|
| 24 |
+
chain: the chain id or list of chain ids to load
|
| 25 |
+
Returns:
|
| 26 |
+
biotite.structure.AtomArray
|
| 27 |
+
"""
|
| 28 |
+
if fpath.endswith('cif'):
|
| 29 |
+
with open(fpath) as fin:
|
| 30 |
+
pdbxf = pdbx.PDBxFile.read(fin)
|
| 31 |
+
structure = pdbx.get_structure(pdbxf, model=1)
|
| 32 |
+
elif fpath.endswith('pdb'):
|
| 33 |
+
with open(fpath) as fin:
|
| 34 |
+
pdbf = pdb.PDBFile.read(fin)
|
| 35 |
+
structure = pdb.get_structure(pdbf, model=1)
|
| 36 |
+
bbmask = filter_backbone(structure)
|
| 37 |
+
structure = structure[bbmask]
|
| 38 |
+
all_chains = get_chains(structure)
|
| 39 |
+
if len(all_chains) == 0:
|
| 40 |
+
raise ValueError('No chains found in the input file.')
|
| 41 |
+
if chain is None:
|
| 42 |
+
chain_ids = all_chains
|
| 43 |
+
elif isinstance(chain, list):
|
| 44 |
+
chain_ids = chain
|
| 45 |
+
else:
|
| 46 |
+
chain_ids = [chain]
|
| 47 |
+
for chain in chain_ids:
|
| 48 |
+
if chain not in all_chains:
|
| 49 |
+
raise ValueError(f'Chain {chain} not found in input file')
|
| 50 |
+
chain_filter = [a.chain_id in chain_ids for a in structure]
|
| 51 |
+
structure = structure[chain_filter]
|
| 52 |
+
return structure
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def extract_coords_from_structure(structure: biotite.structure.AtomArray):
|
| 56 |
+
"""
|
| 57 |
+
Args:
|
| 58 |
+
structure: An instance of biotite AtomArray
|
| 59 |
+
Returns:
|
| 60 |
+
Tuple (coords, seq)
|
| 61 |
+
- coords is an L x 3 x 3 array for N, CA, C coordinates
|
| 62 |
+
- seq is the extracted sequence
|
| 63 |
+
"""
|
| 64 |
+
coords = get_atom_coords_residuewise(["N", "CA", "C"], structure)
|
| 65 |
+
residue_identities = get_residues(structure)[1]
|
| 66 |
+
seq = ''.join([ProteinSequence.convert_letter_3to1(r) for r in residue_identities])
|
| 67 |
+
return coords, seq
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def load_coords(fpath, chain):
|
| 71 |
+
"""
|
| 72 |
+
Args:
|
| 73 |
+
fpath: filepath to either pdb or cif file
|
| 74 |
+
chain: the chain id
|
| 75 |
+
Returns:
|
| 76 |
+
Tuple (coords, seq)
|
| 77 |
+
- coords is an L x 3 x 3 array for N, CA, C coordinates
|
| 78 |
+
- seq is the extracted sequence
|
| 79 |
+
"""
|
| 80 |
+
structure = load_structure(fpath, chain)
|
| 81 |
+
return extract_coords_from_structure(structure)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def get_atom_coords_residuewise(atoms: List[str], struct: biotite.structure.AtomArray):
|
| 85 |
+
"""
|
| 86 |
+
Example for atoms argument: ["N", "CA", "C"]
|
| 87 |
+
"""
|
| 88 |
+
def filterfn(s, axis=None):
|
| 89 |
+
filters = np.stack([s.atom_name == name for name in atoms], axis=1)
|
| 90 |
+
sum = filters.sum(0)
|
| 91 |
+
if not np.all(sum <= np.ones(filters.shape[1])):
|
| 92 |
+
raise RuntimeError("structure has multiple atoms with same name")
|
| 93 |
+
index = filters.argmax(0)
|
| 94 |
+
coords = s[index].coord
|
| 95 |
+
coords[sum == 0] = float("nan")
|
| 96 |
+
return coords
|
| 97 |
+
|
| 98 |
+
return biotite.structure.apply_residue_wise(struct, struct, filterfn)
|
| 99 |
+
|
| 100 |
+
#* now consistent with updated gpu
|
| 101 |
+
def get_sequence_loss(model, alphabet, coords, seq):
|
| 102 |
+
device = next(model.parameters()).device
|
| 103 |
+
batch_converter = CoordBatchConverter(alphabet)
|
| 104 |
+
batch = [(coords, None, seq)]
|
| 105 |
+
coords, confidence, strs, tokens, padding_mask = batch_converter(
|
| 106 |
+
batch, device=device)
|
| 107 |
+
|
| 108 |
+
prev_output_tokens = tokens[:, :-1].to(device)
|
| 109 |
+
target = tokens[:, 1:]
|
| 110 |
+
target_padding_mask = (target == alphabet.padding_idx)
|
| 111 |
+
logits, _ = model.forward(coords, padding_mask, confidence, prev_output_tokens)
|
| 112 |
+
loss = F.cross_entropy(logits, target, reduction='none')
|
| 113 |
+
loss = loss[0].cpu().detach().numpy()
|
| 114 |
+
target_padding_mask = target_padding_mask[0].cpu().numpy()
|
| 115 |
+
return loss, target_padding_mask
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def score_sequence(model, alphabet, coords, seq):
|
| 119 |
+
loss, target_padding_mask = get_sequence_loss(model, alphabet, coords, seq)
|
| 120 |
+
ll_fullseq = -np.sum(loss * ~target_padding_mask) / np.sum(~target_padding_mask)
|
| 121 |
+
# Also calculate average when excluding masked portions
|
| 122 |
+
coord_mask = np.all(np.isfinite(coords), axis=(-1, -2))
|
| 123 |
+
ll_withcoord = -np.sum(loss * coord_mask) / np.sum(coord_mask)
|
| 124 |
+
return ll_fullseq, ll_withcoord
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def get_encoder_output(model, alphabet, coords):
|
| 128 |
+
device = next(model.parameters()).device
|
| 129 |
+
batch_converter = CoordBatchConverter(alphabet)
|
| 130 |
+
batch = [(coords, None, None)]
|
| 131 |
+
coords, confidence, strs, tokens, padding_mask = batch_converter(
|
| 132 |
+
batch, device=device)
|
| 133 |
+
encoder_out = model.encoder.forward(coords, padding_mask, confidence,
|
| 134 |
+
return_all_hiddens=False)
|
| 135 |
+
# remove beginning and end (bos and eos tokens)
|
| 136 |
+
return encoder_out['encoder_out'][0][1:-1, 0]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def rotate(v, R):
|
| 140 |
+
"""
|
| 141 |
+
Rotates a vector by a rotation matrix.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
v: 3D vector, tensor of shape (length x batch_size x channels x 3)
|
| 145 |
+
R: rotation matrix, tensor of shape (length x batch_size x 3 x 3)
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Rotated version of v by rotation matrix R.
|
| 149 |
+
"""
|
| 150 |
+
R = R.unsqueeze(-3)
|
| 151 |
+
v = v.unsqueeze(-1)
|
| 152 |
+
return torch.sum(v * R, dim=-2)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_rotation_frames(coords):
|
| 156 |
+
"""
|
| 157 |
+
Returns a local rotation frame defined by N, CA, C positions.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
coords: coordinates, tensor of shape (batch_size x length x 3 x 3)
|
| 161 |
+
where the third dimension is in order of N, CA, C
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Local relative rotation frames in shape (batch_size x length x 3 x 3)
|
| 165 |
+
"""
|
| 166 |
+
v1 = coords[:, :, 2] - coords[:, :, 1]
|
| 167 |
+
v2 = coords[:, :, 0] - coords[:, :, 1]
|
| 168 |
+
e1 = normalize(v1, dim=-1)
|
| 169 |
+
u2 = v2 - e1 * torch.sum(e1 * v2, dim=-1, keepdim=True)
|
| 170 |
+
e2 = normalize(u2, dim=-1)
|
| 171 |
+
e3 = torch.cross(e1, e2, dim=-1)
|
| 172 |
+
R = torch.stack([e1, e2, e3], dim=-2)
|
| 173 |
+
return R
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def nan_to_num(ts, val=0.0):
|
| 177 |
+
"""
|
| 178 |
+
Replaces nans in tensor with a fixed value.
|
| 179 |
+
"""
|
| 180 |
+
val = torch.tensor(val, dtype=ts.dtype, device=ts.device)
|
| 181 |
+
return torch.where(~torch.isfinite(ts), val, ts)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def rbf(values, v_min, v_max, n_bins=16):
|
| 185 |
+
"""
|
| 186 |
+
Returns RBF encodings in a new dimension at the end.
|
| 187 |
+
"""
|
| 188 |
+
rbf_centers = torch.linspace(v_min, v_max, n_bins, device=values.device)
|
| 189 |
+
rbf_centers = rbf_centers.view([1] * len(values.shape) + [-1])
|
| 190 |
+
rbf_std = (v_max - v_min) / n_bins
|
| 191 |
+
v_expand = torch.unsqueeze(values, -1)
|
| 192 |
+
z = (values.unsqueeze(-1) - rbf_centers) / rbf_std
|
| 193 |
+
return torch.exp(-z ** 2)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def norm(tensor, dim, eps=1e-8, keepdim=False):
|
| 197 |
+
"""
|
| 198 |
+
Returns L2 norm along a dimension.
|
| 199 |
+
"""
|
| 200 |
+
return torch.sqrt(
|
| 201 |
+
torch.sum(torch.square(tensor), dim=dim, keepdim=keepdim) + eps)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def normalize(tensor, dim=-1):
|
| 205 |
+
"""
|
| 206 |
+
Normalizes a tensor along a dimension after removing nans.
|
| 207 |
+
"""
|
| 208 |
+
return nan_to_num(
|
| 209 |
+
torch.div(tensor, norm(tensor, dim=dim, keepdim=True))
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
#*
|
| 213 |
+
class CoordBatchConverter(BatchConverter):
|
| 214 |
+
def __call__(self, raw_batch: Sequence[Tuple[Sequence, str]], device=None):
|
| 215 |
+
"""
|
| 216 |
+
Args:
|
| 217 |
+
raw_batch: List of tuples (coords, confidence, seq)
|
| 218 |
+
In each tuple,
|
| 219 |
+
coords: list of floats, shape L x 3 x 3
|
| 220 |
+
confidence: list of floats, shape L; or scalar float; or None
|
| 221 |
+
seq: string of length L
|
| 222 |
+
Returns:
|
| 223 |
+
coords: Tensor of shape batch_size x L x 3 x 3
|
| 224 |
+
confidence: Tensor of shape batch_size x L
|
| 225 |
+
strs: list of strings
|
| 226 |
+
tokens: LongTensor of shape batch_size x L
|
| 227 |
+
padding_mask: ByteTensor of shape batch_size x L
|
| 228 |
+
"""
|
| 229 |
+
self.alphabet.cls_idx = self.alphabet.get_idx("<cath>")
|
| 230 |
+
batch = []
|
| 231 |
+
for coords, confidence, seq in raw_batch:
|
| 232 |
+
if confidence is None:
|
| 233 |
+
confidence = 1.
|
| 234 |
+
if isinstance(confidence, float) or isinstance(confidence, int):
|
| 235 |
+
confidence = [float(confidence)] * len(coords)
|
| 236 |
+
if seq is None:
|
| 237 |
+
seq = 'X' * len(coords)
|
| 238 |
+
batch.append(((coords, confidence), seq))
|
| 239 |
+
|
| 240 |
+
coords_and_confidence, strs, tokens = super().__call__(batch)
|
| 241 |
+
|
| 242 |
+
# pad beginning and end of each protein due to legacy reasons
|
| 243 |
+
coords = [
|
| 244 |
+
F.pad(torch.tensor(cd), (0, 0, 0, 0, 1, 1), value=np.nan) #<---pad set to nan from np.inf
|
| 245 |
+
for cd, _ in coords_and_confidence
|
| 246 |
+
]
|
| 247 |
+
confidence = [
|
| 248 |
+
F.pad(torch.tensor(cf), (1, 1), value=-1.)
|
| 249 |
+
for _, cf in coords_and_confidence
|
| 250 |
+
]
|
| 251 |
+
coords = self.collate_dense_tensors(coords, pad_v=np.nan)
|
| 252 |
+
confidence = self.collate_dense_tensors(confidence, pad_v=-1.)
|
| 253 |
+
if device is not None:
|
| 254 |
+
coords = coords.to(device)
|
| 255 |
+
confidence = confidence.to(device)
|
| 256 |
+
tokens = tokens.to(device)
|
| 257 |
+
padding_mask = torch.isnan(coords[:,:,0,0])
|
| 258 |
+
coord_mask = torch.isfinite(coords.sum(-2).sum(-1))
|
| 259 |
+
confidence = confidence * coord_mask + (-1.) * padding_mask
|
| 260 |
+
return coords, confidence, strs, tokens, padding_mask
|
| 261 |
+
|
| 262 |
+
def from_lists(self, coords_list, confidence_list=None, seq_list=None, device=None):
|
| 263 |
+
"""
|
| 264 |
+
Args:
|
| 265 |
+
coords_list: list of length batch_size, each item is a list of
|
| 266 |
+
floats in shape L x 3 x 3 to describe a backbone
|
| 267 |
+
confidence_list: one of
|
| 268 |
+
- None, default to highest confidence
|
| 269 |
+
- list of length batch_size, each item is a scalar
|
| 270 |
+
- list of length batch_size, each item is a list of floats of
|
| 271 |
+
length L to describe the confidence scores for the backbone
|
| 272 |
+
with values between 0. and 1.
|
| 273 |
+
seq_list: either None or a list of strings
|
| 274 |
+
Returns:
|
| 275 |
+
coords: Tensor of shape batch_size x L x 3 x 3
|
| 276 |
+
confidence: Tensor of shape batch_size x L
|
| 277 |
+
strs: list of strings
|
| 278 |
+
tokens: LongTensor of shape batch_size x L
|
| 279 |
+
padding_mask: ByteTensor of shape batch_size x L
|
| 280 |
+
"""
|
| 281 |
+
batch_size = len(coords_list)
|
| 282 |
+
if confidence_list is None:
|
| 283 |
+
confidence_list = [None] * batch_size
|
| 284 |
+
if seq_list is None:
|
| 285 |
+
seq_list = [None] * batch_size
|
| 286 |
+
raw_batch = zip(coords_list, confidence_list, seq_list)
|
| 287 |
+
return self.__call__(raw_batch, device)
|
| 288 |
+
|
| 289 |
+
@staticmethod
|
| 290 |
+
def collate_dense_tensors(samples, pad_v):
|
| 291 |
+
"""
|
| 292 |
+
Takes a list of tensors with the following dimensions:
|
| 293 |
+
[(d_11, ..., d_1K),
|
| 294 |
+
(d_21, ..., d_2K),
|
| 295 |
+
...,
|
| 296 |
+
(d_N1, ..., d_NK)]
|
| 297 |
+
and stack + pads them into a single tensor of:
|
| 298 |
+
(N, max_i=1,N { d_i1 }, ..., max_i=1,N {diK})
|
| 299 |
+
"""
|
| 300 |
+
if len(samples) == 0:
|
| 301 |
+
return torch.Tensor()
|
| 302 |
+
if len(set(x.dim() for x in samples)) != 1:
|
| 303 |
+
raise RuntimeError(
|
| 304 |
+
f"Samples has varying dimensions: {[x.dim() for x in samples]}"
|
| 305 |
+
)
|
| 306 |
+
(device,) = tuple(set(x.device for x in samples)) # assumes all on same device
|
| 307 |
+
max_shape = [max(lst) for lst in zip(*[x.shape for x in samples])]
|
| 308 |
+
result = torch.empty(
|
| 309 |
+
len(samples), *max_shape, dtype=samples[0].dtype, device=device
|
| 310 |
+
)
|
| 311 |
+
result.fill_(pad_v)
|
| 312 |
+
for i in range(len(samples)):
|
| 313 |
+
result_i = result[i]
|
| 314 |
+
t = samples[i]
|
| 315 |
+
result_i[tuple(slice(0, k) for k in t.shape)] = t
|
| 316 |
+
return result
|
scripts/esm1v_ab_benchmarking.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
run_prediction() {
|
| 4 |
+
python ../esm/examples/variant-prediction/predict.py \
|
| 5 |
+
--model-location esm1v_t33_650M_UR90S_1 esm1v_t33_650M_UR90S_2 esm1v_t33_650M_UR90S_3 esm1v_t33_650M_UR90S_4 esm1v_t33_650M_UR90S_5 \
|
| 6 |
+
--sequence "$1" \
|
| 7 |
+
--dms-input "$2" \
|
| 8 |
+
--mutation-col mutant \
|
| 9 |
+
--dms-output "$3" \
|
| 10 |
+
--scoring-strategy 'masked-marginals' \
|
| 11 |
+
--offset-idx "$4"
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
# CR6261
|
| 15 |
+
run_prediction "EVQLVESGAEVKKPGSSVKVSCKASGGPFRSYAISWVRQAPGQGPEWMGGIIPIFGTTKYAPKFQGRVTITADDFAGTVYMELSSLRSEDTAMYYCAKHMGYQVRETMDVWGKGTTVTVSS" \
|
| 16 |
+
"data/ab_mutagenesis_expts/cr6261/cr6261_singleMuts_exp_data.csv" \
|
| 17 |
+
"output/ab_mutagenesis_expts/cr6261/cr6261_exp_data_maskMargLabeled.csv" \
|
| 18 |
+
1
|
| 19 |
+
|
| 20 |
+
# CR9114
|
| 21 |
+
run_prediction "QVQLVQSGAEVKKPGSSVKVSCKSSGGTSNNYAISWVRQAPGQGLDWMGGISPIFGSTAYAQKFQGRVTISADIFSNTAYMELNSLTSEDTAVYFCARHGNYYYYSGMDVWGQGTTVTVSS" \
|
| 22 |
+
"data/ab_mutagenesis_expts/cr9114/cr9114_singleMuts_exp_data.csv" \
|
| 23 |
+
"output/ab_mutagenesis_expts/cr9114/cr9114_exp_data_maskMargLabeled.csv" \
|
| 24 |
+
1
|
| 25 |
+
|
| 26 |
+
# G6 HC
|
| 27 |
+
run_prediction "EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTV" \
|
| 28 |
+
"data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv" \
|
| 29 |
+
"output/ab_mutagenesis_expts/g6/g6Hc_exp_data_maskMargLabeled.csv" \
|
| 30 |
+
1
|
| 31 |
+
|
| 32 |
+
# G6 LC
|
| 33 |
+
run_prediction "DIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIK" \
|
| 34 |
+
"data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv" \
|
| 35 |
+
"output/ab_mutagenesis_expts/g6/g6Lc_exp_data_maskMargLabeled.csv" \
|
| 36 |
+
1
|
| 37 |
+
|
| 38 |
+
# G6 HC (both chains)
|
| 39 |
+
run_prediction "EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTVDIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIK" \
|
| 40 |
+
"data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv" \
|
| 41 |
+
"output/ab_mutagenesis_expts/g6/g6Hc_esm1vbothchains_exp_data_maskMargLabeled.csv" \
|
| 42 |
+
1
|
| 43 |
+
|
| 44 |
+
# G6 LC (both chains)
|
| 45 |
+
run_prediction "EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTVDIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIK" \
|
| 46 |
+
"data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv" \
|
| 47 |
+
"output/ab_mutagenesis_expts/g6/g6Lc_esm1vbothchains_exp_data_maskMargLabeled.csv" \
|
| 48 |
+
-117
|
| 49 |
+
|
| 50 |
+
# CR9114 (both chains)
|
| 51 |
+
run_prediction "QVQLVQSGAEVKKPGSSVKVSCKSSGGTSNNYAISWVRQAPGQGLDWMGGISPIFGSTAYAQKFQGRVTISADIFSNTAYMELNSLTSEDTAVYFCARHGNYYYYSGMDVWGQGTTVTVSSQSALTQPPAVSGTPGQRVTISCSGSDSNIGRRSVNWYQQFPGTAPKLLIYSNDQRPSVVPDRFSGSKSGTSASLAISGLQSEDEAEYYCAAWDDSLKGAVFGGGTQLTVL" \
|
| 52 |
+
"data/ab_mutagenesis_expts/cr9114/cr9114_singleMuts_exp_data.csv" \
|
| 53 |
+
"output/ab_mutagenesis_expts/cr9114/cr9114_esm1vbothchains_exp_data_maskMargLabeled.csv" \
|
| 54 |
+
1
|
| 55 |
+
|
| 56 |
+
# CR6261 (both chains)
|
| 57 |
+
run_prediction "EVQLVESGAEVKKPGSSVKVSCKASGGPFRSYAISWVRQAPGQGPEWMGGIIPIFGTTKYAPKFQGRVTITADDFAGTVYMELSSLRSEDTAMYYCAKHMGYQVRETMDVWGKGTTVTVSSQSVLTQPPSVSAAPGQKVTISCSGSSSNIGNDYVSWYQQLPGTAPKLLIYDNNKRPSGIPDRFSGSKSGTSATLGITGLQTGDEANYYCATWDRRPTAYVVFGGGTKLTVL" \
|
| 58 |
+
"data/ab_mutagenesis_expts/cr6261/cr6261_singleMuts_exp_data.csv" \
|
| 59 |
+
"output/ab_mutagenesis_expts/cr6261/cr6261_esm1vbothchains_exp_data_maskMargLabeled.csv" \
|
| 60 |
+
1
|
| 61 |
+
|
| 62 |
+
# CR9114 (antibody + antigen)
|
| 63 |
+
run_prediction "QVQLVQSGAEVKKPGSSVKVSCKSSGGTSNNYAISWVRQAPGQGLDWMGGISPIFGSTAYAQKFQGRVTISADIFSNTAYMELNSLTSEDTAVYFCARHGNYYYYSGMDVWGQGTTVTVSSQSALTQPPAVSGTPGQRVTISCSGSDSNIGRRSVNWYQQFPGTAPKLLIYSNDQRPSVVPDRFSGSKSGTSASLAISGLQSEDEAEYYCAAWDDSLKGAVFGGGTQLTVLADPGDQICIGYHANNSTEQVDTIMEKNVTVTHAQDILEKKHNGKLCDLDGVKPLILRDCSVAGWLLGNPMCDEFINVPEWSYIVEKANPVNDLCYPGDFNDYEELKHLLSRINHFEKIQIIPKSSWSSHEASLGVSSACPYQGKSSFFRNVVWLIKKNSTYPTIKRSYNNTNQEDLLVLWGIHHPNDAAEQTKLYQNPTTYISVGTSTLNQRLVPRIATRSKVNGQSGRMEFFWTILKPNDAINFESNGNFIAPEYAYKIVKKGDSTIMKSELEYGNCNTKCQTPMGAINSSMPFHNIHPLTIGECPKYVKSNRLVLATGLRNSPQRERRRKKRGLFGAIAGFIEGGWQGMVDGWYGYHHSNEQGSGYAADKESTQKAIDGVTNKVNSIIDKMNTQFEAVGREFNNLERRIENLNKKMEDGFLDVWTYNAELLVLMENERTLDFHDSNVKNLYDKVRLQLRDNAKELGNGCFEFYHKCDNECMESVRNGTYDYPQYSEEARLKREEISSGR" \
|
| 64 |
+
"data/ab_mutagenesis_expts/cr9114/cr9114_singleMuts_exp_data.csv" \
|
| 65 |
+
"output/ab_mutagenesis_expts/cr9114/cr9114_esm1vAbAg_exp_data_maskMargLabeled.csv" \
|
| 66 |
+
1
|
| 67 |
+
|
| 68 |
+
# CR6261 (antibody + antigen)
|
| 69 |
+
run_prediction "EVQLVESGAEVKKPGSSVKVSCKASGGPFRSYAISWVRQAPGQGPEWMGGIIPIFGTTKYAPKFQGRVTITADDFAGTVYMELSSLRSEDTAMYYCAKHMGYQVRETMDVWGKGTTVTVSSQSVLTQPPSVSAAPGQKVTISCSGSSSNIGNDYVSWYQQLPGTAPKLLIYDNNKRPSGIPDRFSGSKSGTSATLGITGLQTGDEANYYCATWDRRPTAYVVFGGGTKLTVLADPGDTICIGYHANNSTDTVDTVLEKNVTVTHSVNLLEDSHNGKLCKLKGIAPLQLGKCNIAGWLLGNPECDLLLTASSWSYIVETSNSENGTCYPGDFIDYEELREQLSSVSSFEKFEIFPKTSSWPNHETTKGVTAACSYAGASSFYRNLLWLTKKGSSYPKLSKSYVNNKGKEVLVLWGVHHPPTGTDQQSLYQNADAYVSVGSSKYNRRFTPEIAARPKVRDQAGRMNYYWTLLEPGDTITFEATGNLIAPWYAFALNRGSGSGIITSDAPVHDCNTKCQTPHGAINSSLPFQNIHPVTIGECPKYVRSTKLRMATGLRNIPSIQSRGLFGAIAGFIEGGWTGMIDGWYGYHHQNEQGSGYAADQKSTQNAIDGITNKVNSVIEKMNTQFTAVGKEFNNLERRIENLNKKVDDGFLDIWTYNAELLVLLENERTLDFHDSNVRNLYEKVKSQLKNNAKEIGNGCFEFYHKCDDACMESVRNGTYDYPKYSEESKLNREEIDGVSGR" \
|
| 70 |
+
"data/ab_mutagenesis_expts/cr6261/cr6261_singleMuts_exp_data.csv" \
|
| 71 |
+
"output/ab_mutagenesis_expts/cr6261/cr6261_esm1vAbAg_exp_data_maskMargLabeled.csv" \
|
| 72 |
+
1
|
| 73 |
+
|
| 74 |
+
# G6 HC (antibody + antigen)
|
| 75 |
+
run_prediction "EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTVDIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIKGQNHHEVVKFMDVYQRSYCHPIETLVDIFQEYPDEIEYIFKPSCVPLMRCGGCCNDEGLECVPTEESNITMQIMRIKPHQGQHIGEMSFLQHNKCECRPKKD" \
|
| 76 |
+
"data/ab_mutagenesis_expts/g6/g6_hc_exp_data.csv" \
|
| 77 |
+
"output/ab_mutagenesis_expts/g6/g6Hc_esm1vAbAg_exp_data_maskMargLabeled.csv" \
|
| 78 |
+
1
|
| 79 |
+
|
| 80 |
+
# G6 LC (antibody + antigen)
|
| 81 |
+
run_prediction "EVQLVESGGGLVQPGGSLRLSCAASGFTISDYWIHWVRQAPGKGLEWVAGITPAGGYTYYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCARFVFFLPYAMDYWGQGTLVTVDIQMTQSPSSLSASVGDRVTITCRASQDVSTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYTTPPTFGQGTKVEIKGQNHHEVVKFMDVYQRSYCHPIETLVDIFQEYPDEIEYIFKPSCVPLMRCGGCCNDEGLECVPTEESNITMQIMRIKPHQGQHIGEMSFLQHNKCECRPKKD" \
|
| 82 |
+
"data/ab_mutagenesis_expts/g6/g6_lc_exp_data.csv" \
|
| 83 |
+
"output/ab_mutagenesis_expts/g6/g6Lc_esm1vAbAg_exp_data_maskMargLabeled.csv" \
|
| 84 |
+
-117
|
scripts/examples/7mmo_abc_fvar.pdb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/if_ab_benchmarking.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
abs=("cr6261" "cr9114" "g6" "g6")
|
| 2 |
+
ab_fastas=("cr6261_3gbn_hc_lib.fasta" "cr9114_4fqi_hc_lib.fasta" "g6_2fjg_hc_lib.fasta" "g6_2fjg_lc_lib.fasta")
|
| 3 |
+
data_path="data/ab_mutagenesis_expts/"
|
| 4 |
+
out_prefix="output/ab_mutagenesis_expts/"
|
| 5 |
+
|
| 6 |
+
for ((i=0; i<${#abs[@]}; i++)); do
|
| 7 |
+
ab="${abs[i]}"
|
| 8 |
+
ab_fasta="${ab_fastas[i]}"
|
| 9 |
+
ab_dir_path="${data_path}${ab}/"
|
| 10 |
+
struc_list=("${ab_dir_path}"*.pdb)
|
| 11 |
+
ab_out_dir="${out_prefix}${ab}/"
|
| 12 |
+
|
| 13 |
+
# Set the default chain value
|
| 14 |
+
chain="H"
|
| 15 |
+
# Special handling for 'g6' antibody
|
| 16 |
+
if [[ "$ab" == "g6" ]]; then
|
| 17 |
+
[[ "$ab_fasta" == *"lc"* ]] && chain="L"
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
# gather pdbs and filter the hc/lc only structure from being scored by library for the other chain
|
| 21 |
+
if [[ "$ab" == "g6" && "$chain" == "L" ]]; then
|
| 22 |
+
struc_list=($(echo "${struc_list[@]}" | tr ' ' '\n' | grep -v '_h_' | tr '\n' ' '))
|
| 23 |
+
elif [[ "$ab" == "g6" && "$chain" == "H" ]]; then
|
| 24 |
+
struc_list=($(echo "${struc_list[@]}" | tr ' ' '\n' | grep -v '_l_' | tr '\n' ' '))
|
| 25 |
+
fi
|
| 26 |
+
|
| 27 |
+
mkdir -p "$ab_out_dir"
|
| 28 |
+
|
| 29 |
+
for struc in "${struc_list[@]}"; do
|
| 30 |
+
out_file="${ab_out_dir}${struc##*/}"
|
| 31 |
+
if [[ "$ab" == "g6" ]]; then
|
| 32 |
+
chain_modeled="$([ "$chain" == "H" ] && echo "hc" || echo "lc")"
|
| 33 |
+
out_file="${out_file%_fvar.pdb}_${chain_modeled}_scores.csv"
|
| 34 |
+
else
|
| 35 |
+
out_file="${out_file%_fvar.pdb}_scores.csv"
|
| 36 |
+
fi
|
| 37 |
+
|
| 38 |
+
if [[ ! -f "$out_file" ]]; then
|
| 39 |
+
python model/score_log_likelihoods.py "$struc" --chain "$chain" --seqpath "${ab_dir_path}${ab_fasta}" --outpath "$out_file"
|
| 40 |
+
else
|
| 41 |
+
echo "$out_file already exists. Skipping..."
|
| 42 |
+
fi
|
| 43 |
+
done
|
| 44 |
+
done
|
scripts/mpnn_ab_benchmarking.sh
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
run_protein_mpnn() {
|
| 4 |
+
python ../ProteinMPNN/protein_mpnn_run.py \
|
| 5 |
+
--path_to_fasta "$1" \
|
| 6 |
+
--score_only 1 \
|
| 7 |
+
--save_score 1 \
|
| 8 |
+
--pdb_path_chains "$2" \
|
| 9 |
+
--out_folder "$3" \
|
| 10 |
+
--pdb_path "$4"
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
# G6 Light Chain
|
| 14 |
+
run_protein_mpnn \
|
| 15 |
+
"data/ab_mutagenesis_expts/g6/g6_2fjg_lc_lib.fasta" \
|
| 16 |
+
"L" \
|
| 17 |
+
"output/ab_mutagenesis_expts/g6/proteinMpnnLC" \
|
| 18 |
+
"data/ab_mutagenesis_expts/g6/2fjg_vlh_fvar.pdb"
|
| 19 |
+
|
| 20 |
+
# G6 Heavy Chain
|
| 21 |
+
run_protein_mpnn \
|
| 22 |
+
"data/ab_mutagenesis_expts/g6/g6_2fjg_hc_lib.fasta" \
|
| 23 |
+
"H" \
|
| 24 |
+
"output/ab_mutagenesis_expts/g6/proteinMpnnHC" \
|
| 25 |
+
"data/ab_mutagenesis_expts/g6/2fjg_vlh_fvar.pdb"
|
| 26 |
+
|
| 27 |
+
# CR6261 Heavy Chain
|
| 28 |
+
run_protein_mpnn \
|
| 29 |
+
"data/ab_mutagenesis_expts/cr6261/cr6261_3gbn_hc_lib.fasta" \
|
| 30 |
+
"H" \
|
| 31 |
+
"output/ab_mutagenesis_expts/cr6261/mpnn" \
|
| 32 |
+
"data/ab_mutagenesis_expts/cr6261/3gbn_ablh_fvar.pdb"
|
| 33 |
+
|
| 34 |
+
# CR9114 Heavy Chain
|
| 35 |
+
run_protein_mpnn \
|
| 36 |
+
"data/ab_mutagenesis_expts/cr9114/cr9114_4fqi_hc_lib.fasta" \
|
| 37 |
+
"H" \
|
| 38 |
+
"output/ab_mutagenesis_expts/cr9114/mpnn" \
|
| 39 |
+
"data/ab_mutagenesis_expts/cr9114/4fqi_ablh_fvar.pdb"
|
scripts/recommend.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
ROOT_DIR = Path(__file__).resolve().parents[1]
|
| 7 |
+
MODEL_DIR = ROOT_DIR / "model"
|
| 8 |
+
if str(MODEL_DIR) not in sys.path:
|
| 9 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def load_esm_model(checkpoint_path):
|
| 13 |
+
import esm
|
| 14 |
+
import warnings
|
| 15 |
+
|
| 16 |
+
with warnings.catch_warnings():
|
| 17 |
+
warnings.simplefilter('ignore', UserWarning)
|
| 18 |
+
return esm.pretrained.load_model_and_alphabet(checkpoint_path)
|
| 19 |
+
|
| 20 |
+
def get_native_seq(pdbfile, chain):
|
| 21 |
+
import util
|
| 22 |
+
|
| 23 |
+
structure = util.load_structure(pdbfile, chain)
|
| 24 |
+
_ , native_seq = util.extract_coords_from_structure(structure)
|
| 25 |
+
return native_seq
|
| 26 |
+
|
| 27 |
+
def write_dms_lib(args):
|
| 28 |
+
'''Writes a deep mutational scanning library, including the native/wildtype (wt) of the
|
| 29 |
+
indicated target chain in the structure to an output Fasta file'''
|
| 30 |
+
from dms_utils import deep_mutational_scan
|
| 31 |
+
|
| 32 |
+
sequence = get_native_seq(args.pdbfile, args.chain)
|
| 33 |
+
Path(args.seqpath).parent.mkdir(parents=True, exist_ok=True)
|
| 34 |
+
with open(args.seqpath, 'w') as f:
|
| 35 |
+
f.write('>wt\n')
|
| 36 |
+
f.write(sequence+'\n')
|
| 37 |
+
for pos, wt, mt in deep_mutational_scan(sequence):
|
| 38 |
+
assert(sequence[pos] == wt)
|
| 39 |
+
mut_seq = sequence[:pos] + mt + sequence[(pos + 1):]
|
| 40 |
+
f.write('>' + str(wt) + str(pos+1+args.offset) + str(mt) + '\n')
|
| 41 |
+
f.write(mut_seq + '\n')
|
| 42 |
+
|
| 43 |
+
def get_top_n(args):
|
| 44 |
+
import pandas as pd
|
| 45 |
+
|
| 46 |
+
recs, rec_inds = [], []
|
| 47 |
+
scores_df = pd.read_csv(args.outpath).sort_values(by = 'log_likelihood', ascending = False)
|
| 48 |
+
|
| 49 |
+
for seqid in scores_df['seqid']:
|
| 50 |
+
res_ind = seqid[1:-1]
|
| 51 |
+
if (rec_inds.count(res_ind) < args.maxrep):
|
| 52 |
+
if args.upperbound == None or (int(res_ind) < int(args.upperbound)):
|
| 53 |
+
recs.append(seqid)
|
| 54 |
+
rec_inds.append(res_ind)
|
| 55 |
+
if len(recs) == args.n:
|
| 56 |
+
break
|
| 57 |
+
|
| 58 |
+
print(f'\n Chain {args.chain}')
|
| 59 |
+
print(*recs, sep='\n')
|
| 60 |
+
|
| 61 |
+
def get_model_checkpoint_path(filename):
|
| 62 |
+
# Expanding the user's home directory
|
| 63 |
+
return os.path.expanduser(f"~/.cache/torch/hub/checkpoints/{filename}")
|
| 64 |
+
|
| 65 |
+
def main():
|
| 66 |
+
parser = argparse.ArgumentParser(
|
| 67 |
+
description='Score sequences based on a given structure.'
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
'pdbfile', type=str,
|
| 71 |
+
help='input filepath, either .pdb or .cif',
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
'--seqpath', type=str,
|
| 75 |
+
help='filepath where fasta of dms library should be saveda',
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
'--outpath', type=str,
|
| 79 |
+
help='output filepath for scores of variant sequences',
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
'--chain', type=str,
|
| 83 |
+
help='chain id for the chain of interest', default='A',
|
| 84 |
+
)
|
| 85 |
+
parser.set_defaults(multichain_backbone=True)
|
| 86 |
+
parser.add_argument(
|
| 87 |
+
'--multichain-backbone', action='store_true',
|
| 88 |
+
help='use the backbones of all chains in the input for conditioning'
|
| 89 |
+
)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
'--singlechain-backbone', dest='multichain_backbone',
|
| 92 |
+
action='store_false',
|
| 93 |
+
help='use the backbone of only target chain in the input for conditioning'
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
'--order', type=str, default=None,
|
| 97 |
+
help='for multichain, option to specify order of chains'
|
| 98 |
+
)
|
| 99 |
+
parser.add_argument(
|
| 100 |
+
'--n', type=int,
|
| 101 |
+
help='number of desired predictions to be output',
|
| 102 |
+
default=10,
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
'--maxrep', type=int,
|
| 106 |
+
help='maximum representation of a single site in the top recommendations \
|
| 107 |
+
(eg: maxrep = 1 is a unique set where no wildtype residue is mutated more than once)',
|
| 108 |
+
default=1,
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
'--offset', type=int,
|
| 112 |
+
help='integer offset for labeling of residue indices encoded in the structure',
|
| 113 |
+
default=0,
|
| 114 |
+
)
|
| 115 |
+
parser.add_argument(
|
| 116 |
+
'--upperbound', type=int,
|
| 117 |
+
help='only residue positions less than the user-defined upperbound are considered to be recommended for screening \
|
| 118 |
+
(but all positions are still conditioned for scoring)',
|
| 119 |
+
default=None,
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--nogpu", action="store_true",
|
| 123 |
+
help="Do not use GPU even if available"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
args = parser.parse_args()
|
| 127 |
+
|
| 128 |
+
if args.seqpath is None:
|
| 129 |
+
args.seqpath = f'output/{args.pdbfile[:-4]}-chain{args.chain}_dms.fasta'
|
| 130 |
+
|
| 131 |
+
if args.outpath is None:
|
| 132 |
+
args.outpath = f'output/{args.pdbfile[:-4]}-chain{args.chain}_scores.csv'
|
| 133 |
+
|
| 134 |
+
#write dms library for target chain
|
| 135 |
+
write_dms_lib(args)
|
| 136 |
+
|
| 137 |
+
model_checkpoint_path = get_model_checkpoint_path('esm_if1_20220410.pt')
|
| 138 |
+
model, alphabet = load_esm_model(model_checkpoint_path)
|
| 139 |
+
model = model.eval()
|
| 140 |
+
|
| 141 |
+
import score_log_likelihoods
|
| 142 |
+
|
| 143 |
+
if args.multichain_backbone:
|
| 144 |
+
score_log_likelihoods.score_multichain_backbone(model, alphabet, args)
|
| 145 |
+
else:
|
| 146 |
+
score_log_likelihoods.score_singlechain_backbone(model, alphabet, args)
|
| 147 |
+
|
| 148 |
+
get_top_n(args)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == '__main__':
|
| 152 |
+
main()
|