aaronkollasch
/

aaronkollasch hemantn commited on
Commit
e766090
·
0 Parent(s):

Duplicate from hemantn/ablang2

Browse files

Co-authored-by: Hemant Nagar <hemantn@users.noreply.huggingface.co>

.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 hemantn
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,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ #language:
3
+ #- en
4
+ license: mit
5
+ tags:
6
+ - biology
7
+ - protein
8
+ - antibody
9
+ - ablang
10
+ - transformers
11
+ - pytorch
12
+ - chemistry
13
+ - oas
14
+ - cdr
15
+ - ablang2 hf implementation
16
+ - roberta
17
+ - ESM
18
+ - ablang2
19
+ - antibody-design
20
+
21
+ # datasets:
22
+ # - oas
23
+ metrics:
24
+ - sequence modeling
25
+ - protein language model
26
+ library_name: transformers
27
+ pipeline_tag: fill-mask
28
+ ---
29
+
30
+ # 🧬 AbLang2: Transformer-based Antibody Language Model
31
+
32
+ This repository provides HuggingFace-compatible 🤗 implementation of the AbLang2 language model for antibodies. The original AbLang2 model was developed by the [Oxford Protein Informatics Group (OPIG)](https://opig.stats.ox.ac.uk/) and is available at:
33
+ - **AbLang2**: [https://github.com/TobiasHeOl/AbLang2](https://github.com/TobiasHeOl/AbLang2)
34
+
35
+ ## 🎯 Model Available
36
+
37
+ - **ablang2**: AbLang2 model for antibody sequences
38
+
39
+ ## 📦 Installation
40
+
41
+ Install the required dependencies:
42
+
43
+ ```bash
44
+ # Install core dependencies
45
+ pip install transformers numpy pandas rotary-embedding-torch
46
+
47
+ # Install ANARCI from bioconda (required for antibody numbering)
48
+ conda install -c bioconda anarci
49
+ ```
50
+
51
+ **Note**: ANARCI is required for antibody sequence numbering and alignment features. It must be installed from the bioconda channel.
52
+
53
+ ## 🚀 Loading Model from Hugging Face Hub
54
+
55
+ ### Method 1: Load Model and Tokenizer, then Import Adapter
56
+ ```python
57
+ import sys
58
+ import os
59
+ from transformers import AutoModel, AutoTokenizer
60
+ from huggingface_hub import hf_hub_download
61
+
62
+ # Load model and tokenizer from Hugging Face Hub
63
+ model = AutoModel.from_pretrained("hemantn/ablang2", trust_remote_code=True)
64
+ tokenizer = AutoTokenizer.from_pretrained("hemantn/ablang2", trust_remote_code=True)
65
+
66
+ # Download adapter and add to path
67
+ adapter_path = hf_hub_download(repo_id="hemantn/ablang2", filename="adapter.py")
68
+ cached_model_dir = os.path.dirname(adapter_path)
69
+ sys.path.insert(0, cached_model_dir)
70
+
71
+ # Import and create the adapter
72
+ from adapter import AbLang2PairedHuggingFaceAdapter
73
+ ablang = AbLang2PairedHuggingFaceAdapter(model=model, tokenizer=tokenizer)
74
+ ```
75
+
76
+ ### Method 2: Using importlib (Alternative)
77
+ ```python
78
+ import importlib.util
79
+ from transformers import AutoModel, AutoTokenizer
80
+ from huggingface_hub import hf_hub_download
81
+
82
+ # Load model and tokenizer
83
+ model = AutoModel.from_pretrained("hemantn/ablang2", trust_remote_code=True)
84
+ tokenizer = AutoTokenizer.from_pretrained("hemantn/ablang2", trust_remote_code=True)
85
+
86
+ # Load adapter dynamically
87
+ adapter_path = hf_hub_download(repo_id="hemantn/ablang2", filename="adapter.py")
88
+ spec = importlib.util.spec_from_file_location("adapter", adapter_path)
89
+ adapter_module = importlib.util.module_from_spec(spec)
90
+ spec.loader.exec_module(adapter_module)
91
+
92
+ # Create the adapter
93
+ ablang = adapter_module.AbLang2PairedHuggingFaceAdapter(model=model, tokenizer=tokenizer)
94
+ ```
95
+
96
+ **Note**: Model automatically use GPU when available, otherwise fall back to CPU.
97
+
98
+ ## ⚙️ Available Utilities
99
+
100
+ This wrapper translates between HuggingFace's model format and AbLang2's expected input/output structure, making it easy to use AbLang2's powerful antibody analysis tools with model loaded from HuggingFace.
101
+
102
+ - **seqcoding**: Sequence-level representations (averaged across residues)
103
+ - **rescoding**: Residue-level representations (per-residue embeddings)
104
+ - **likelihood**: Raw logits for amino acid prediction at each position
105
+ - **probability**: Normalized probabilities for amino acid prediction
106
+ - **pseudo_log_likelihood**: Uncertainty scoring with stepwise masking (masks each residue)
107
+ - **confidence**: Fast uncertainty scoring (single forward pass, no masking)
108
+ - **restore**: Restore masked residues (*) with predicted amino acids
109
+
110
+ All these utilities work seamlessly with the HuggingFace-loaded model, maintaining the same API as the original AbLang2 implementation.
111
+
112
+ The `AbLang2PairedHuggingFaceAdapter` class is a wrapper that lets you use AbLang2 model utilities after loading the model from HuggingFace. This class enables you to:
113
+
114
+ - **Access all AbLang2 utilities** (seqcoding, rescoding, likelihood, probability, etc.) with the same interface as the original implementation
115
+ - **Work with antibody sequences** (heavy and light chains) seamlessly
116
+ - **Maintain compatibility** with the original AbLang2 API while leveraging HuggingFace's model loading and caching capabilities
117
+
118
+ ## 💡 Examples
119
+
120
+ ### 🔗 AbLang2 (Paired Sequences) - Restore Example
121
+ ```python
122
+ import sys
123
+ import os
124
+ from transformers import AutoModel, AutoTokenizer
125
+ from huggingface_hub import hf_hub_download
126
+
127
+ # 1. Load model and tokenizer from Hugging Face Hub
128
+ model = AutoModel.from_pretrained("hemantn/ablang2", trust_remote_code=True)
129
+ tokenizer = AutoTokenizer.from_pretrained("hemantn/ablang2", trust_remote_code=True)
130
+
131
+ # 2. Download adapter and add to path
132
+ adapter_path = hf_hub_download(repo_id="hemantn/ablang2", filename="adapter.py")
133
+ cached_model_dir = os.path.dirname(adapter_path)
134
+ sys.path.insert(0, cached_model_dir)
135
+ from adapter import AbLang2PairedHuggingFaceAdapter
136
+
137
+ # 3. Create adapter
138
+ ablang = AbLang2PairedHuggingFaceAdapter(model=model, tokenizer=tokenizer)
139
+
140
+ # 4. Restore masked sequences
141
+ masked_seqs = [
142
+ ['EVQ***SGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCAR**PGHGAAFMDVWGTGTTVTVSS',
143
+ 'DIQLTQSPLSLPVTLGQPASISCRSS*SLEASDTNIYLSWFQQRPGQSPRRLIYKI*NRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK']
144
+ ]
145
+ restored = ablang(masked_seqs, mode='restore')
146
+ print(f"Restored sequences: {restored}")
147
+ ```
148
+
149
+ ## 📚 Detailed Usage
150
+
151
+ For comprehensive examples of all utilities (seqcoding, rescoding, likelihood, probability, pseudo_log_likelihood, confidence, and more), see:
152
+ - **[`test_ablang2_HF_implementation.ipynb`](test_ablang2_HF_implementation.ipynb)** - Complete notebook with all utilities and advanced usage patterns
153
+
154
+ ## 📖 Citation
155
+
156
+ If you use these models in your research, please cite the original AbLang2 paper:
157
+
158
+ **AbLang2:**
159
+ ```
160
+ @article{Olsen2024,
161
+ title={Addressing the antibody germline bias and its effect on language models for improved antibody design},
162
+ author={Tobias H. Olsen, Iain H. Moal and Charlotte M. Deane},
163
+ journal={bioRxiv},
164
+ doi={https://doi.org/10.1101/2024.02.02.578678},
165
+ year={2024}
166
+ }
167
+ ```
README_Spaces.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧬 AbLang2 Sequence Restorer - Hugging Face Spaces
2
+
3
+ This is a Gradio web application that provides the AbLang2 sequence restoration utility through Hugging Face Spaces.
4
+
5
+ ## 🎯 What it does
6
+
7
+ The AbLang2 Sequence Restorer allows you to:
8
+ - **Restore masked residues** (*) in antibody sequences
9
+ - **Work with paired sequences** (heavy and light chains)
10
+ - **Handle single chains** (heavy or light chain only)
11
+ - **Use alignment** for variable missing lengths
12
+
13
+ ## 🚀 How to use
14
+
15
+ 1. **Enter sequences**: Provide heavy chain, light chain, or both sequences
16
+ 2. **Mask residues**: Use `*` to indicate residues you want to restore
17
+ 3. **Choose alignment**: Enable "Use Alignment" for variable missing lengths
18
+ 4. **Get results**: Click "Restore Sequences" to get the restored antibody sequences
19
+
20
+ ## 📝 Example Usage
21
+
22
+ ### Example 1: Both chains with masked residues
23
+ - **Heavy Chain**: `EVQ***SGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCAR**PGHGAAFMDVWGTGTTVTVSS`
24
+ - **Light Chain**: `DIQLTQSPLSLPVTLGQPASISCRSS*SLEASDTNIYLSWFQQRPGQSPRRLIYKI*NRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK`
25
+
26
+ ### Example 2: Heavy chain only
27
+ - **Heavy Chain**: `EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMGWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCARDY**GMDVWGQGTTVTVSS`
28
+ - **Light Chain**: (leave empty)
29
+
30
+ ## 🔧 Technical Details
31
+
32
+ - **Model**: AbLang2 from Hugging Face Hub (`hemantn/ablang2`)
33
+ - **Framework**: Gradio for the web interface
34
+ - **Backend**: PyTorch with Transformers library
35
+ - **Processing**: Automatic GPU acceleration when available
36
+
37
+ ## 📚 Related Resources
38
+
39
+ - **Original AbLang2**: [https://github.com/TobiasHeOl/AbLang2](https://github.com/TobiasHeOl/AbLang2)
40
+ - **Model Repository**: [https://huggingface.co/hemantn/ablang2](https://huggingface.co/hemantn/ablang2)
41
+ - **Full Documentation**: See the main README.md for comprehensive usage examples
42
+
43
+ ## 🤝 Citation
44
+
45
+ If you use this tool in your research, please cite the original AbLang2 paper:
46
+
47
+ ```
48
+ @article{Olsen2024,
49
+ title={Addressing the antibody germline bias and its effect on language models for improved antibody design},
50
+ author={Tobias H. Olsen, Iain H. Moal and Charlotte M. Deane},
51
+ journal={bioRxiv},
52
+ doi={https://doi.org/10.1101/2024.02.02.578678},
53
+ year={2024}
54
+ }
55
+ ```
__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .configuration_ablang2paired import AbLang2PairedConfig
2
+ from .modeling_ablang2paired import AbLang2PairedHFModel
3
+ from .tokenizer_ablang2paired import AbLang2PairedTokenizer
4
+ from ablang2 import pretrained
5
+
6
+ __all__ = ['AbLang2PairedConfig', 'AbLang2PairedHFModel', 'AbLang2PairedTokenizer']
__pycache__/__init__.cpython-310.pyc ADDED
Binary file (418 Bytes). View file
 
__pycache__/ablang.cpython-310.pyc ADDED
Binary file (4.35 kB). View file
 
__pycache__/ablang_encodings.cpython-310.pyc ADDED
Binary file (3.73 kB). View file
 
__pycache__/ablang_encodings.cpython-312.pyc ADDED
Binary file (5.64 kB). View file
 
__pycache__/adapter.cpython-310.pyc ADDED
Binary file (10.3 kB). View file
 
__pycache__/adapter.cpython-312.pyc ADDED
Binary file (17 kB). View file
 
__pycache__/alignment.cpython-310.pyc ADDED
Binary file (2.98 kB). View file
 
__pycache__/alignment.cpython-312.pyc ADDED
Binary file (3.77 kB). View file
 
__pycache__/configuration_ablang2paired.cpython-310.pyc ADDED
Binary file (1.05 kB). View file
 
__pycache__/extra_utils.cpython-310.pyc ADDED
Binary file (5.9 kB). View file
 
__pycache__/extra_utils.cpython-312.pyc ADDED
Binary file (8.55 kB). View file
 
__pycache__/modeling_ablang2paired.cpython-310.pyc ADDED
Binary file (3.89 kB). View file
 
__pycache__/restoration.cpython-310.pyc ADDED
Binary file (4.19 kB). View file
 
__pycache__/restoration.cpython-312.pyc ADDED
Binary file (6.46 kB). View file
 
__pycache__/scores.cpython-310.pyc ADDED
Binary file (3.02 kB). View file
 
__pycache__/scores.cpython-312.pyc ADDED
Binary file (5.44 kB). View file
 
ablang.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Tuple
3
+
4
+ import torch
5
+ from torch import nn
6
+ import torch.nn.functional as F
7
+
8
+ from .encoderblock import TransformerEncoder, get_activation_fn
9
+
10
+
11
+ class AbLang(torch.nn.Module):
12
+ """
13
+ AbLang inspired by ESM-2's architecture.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ vocab_size,
19
+ hidden_embed_size,
20
+ n_attn_heads,
21
+ n_encoder_blocks,
22
+ padding_tkn,
23
+ mask_tkn,
24
+ layer_norm_eps: float = 1e-12,
25
+ a_fn: str = "gelu",
26
+ dropout: float = 0.0,
27
+ ):
28
+ super().__init__()
29
+
30
+ self.AbRep = AbRep(
31
+ vocab_size,
32
+ hidden_embed_size,
33
+ n_attn_heads,
34
+ n_encoder_blocks,
35
+ padding_tkn,
36
+ mask_tkn,
37
+ layer_norm_eps,
38
+ a_fn,
39
+ dropout,
40
+ )
41
+ self.AbHead = AbHead(
42
+ vocab_size,
43
+ hidden_embed_size,
44
+ self.AbRep.aa_embed_layer.weight,
45
+ layer_norm_eps,
46
+ a_fn,
47
+ )
48
+
49
+ def forward(self, tokens, return_attn_weights=False, return_rep_layers=[]):
50
+
51
+ representations = self.AbRep(tokens, return_attn_weights, return_rep_layers)
52
+
53
+ if return_attn_weights:
54
+ return representations.attention_weights
55
+
56
+ elif return_rep_layers != []:
57
+ return representations.many_hidden_states
58
+ else:
59
+ likelihoods = self.AbHead(representations.last_hidden_states)
60
+ return likelihoods
61
+
62
+ def get_aa_embeddings(self):
63
+ "Extracts the trained aa_embeddings."
64
+ return self.AbRep.aa_embed_layer
65
+
66
+
67
+ class AbRep(torch.nn.Module):
68
+ """
69
+ AbRep (antibody representations), takes the tokenized sequence and create hidden_embed (representations).
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ vocab_size,
75
+ hidden_embed_size,
76
+ n_attn_heads,
77
+ n_encoder_blocks,
78
+ padding_tkn,
79
+ mask_tkn,
80
+ layer_norm_eps: float = 1e-12,
81
+ a_fn: str = "gelu",
82
+ dropout: float = 0.1,
83
+ ):
84
+ super().__init__()
85
+ self.padding_tkn = padding_tkn
86
+ self.mask_tkn = mask_tkn
87
+
88
+ self.aa_embed_layer = nn.Embedding(
89
+ vocab_size,
90
+ hidden_embed_size,
91
+ padding_idx=padding_tkn,
92
+ )
93
+ self.encoder_blocks = nn.ModuleList(
94
+ [TransformerEncoder(
95
+ hidden_embed_size,
96
+ n_attn_heads,
97
+ attn_dropout = dropout,
98
+ layer_norm_eps = layer_norm_eps,
99
+ a_fn = a_fn,
100
+ ) for _ in range(n_encoder_blocks)]
101
+ )
102
+ self.layer_norm_after_encoder_blocks = nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
103
+
104
+ def forward(self,
105
+ tokens,
106
+ return_attn_weights=False,
107
+ return_rep_layers=[],
108
+ ):
109
+
110
+ assert tokens.ndim == 2
111
+ padding_mask = tokens.eq(self.padding_tkn)
112
+
113
+ hidden_embed = self.aa_embed_layer(tokens)
114
+
115
+ return_rep_layers = set(return_rep_layers)
116
+ rep_layers = {}
117
+ if 0 in return_rep_layers: rep_layers[0] = hidden_embed
118
+
119
+ all_attn_weights = []
120
+
121
+ for n_layer, encoder_block in enumerate(self.encoder_blocks):
122
+ hidden_embed, attn_weights = encoder_block(hidden_embed, padding_mask, return_attn_weights)
123
+
124
+ if (n_layer + 1) in return_rep_layers:
125
+ rep_layers[n_layer + 1] = hidden_embed
126
+
127
+ if return_attn_weights:
128
+ all_attn_weights.append(attn_weights)
129
+
130
+ hidden_embed = self.layer_norm_after_encoder_blocks(hidden_embed)
131
+
132
+ return DataAbRep(
133
+ last_hidden_states=hidden_embed,
134
+ many_hidden_states=rep_layers,
135
+ attention_weights=all_attn_weights
136
+ )
137
+
138
+
139
+ class AbHead(torch.nn.Module):
140
+ """
141
+ AbHead (antibody head model), creates amino acid probabilities for each position based on the hidden_embed (representations).
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ vocab_size,
147
+ hidden_embed_size,
148
+ weights,
149
+ layer_norm_eps: float = 1e-12,
150
+ a_fn: str = "gelu",
151
+ ):
152
+ super().__init__()
153
+
154
+ activation_fn, scale = get_activation_fn(a_fn)
155
+
156
+ self.ff = torch.nn.Sequential(
157
+ nn.Linear(hidden_embed_size, hidden_embed_size * scale),
158
+ activation_fn(),
159
+ nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps),
160
+ )
161
+
162
+ self.weights = weights
163
+ self.bias = nn.Parameter(torch.zeros(vocab_size))
164
+
165
+ def forward(self, hidden_embed):
166
+
167
+ hidden_embed = self.ff(hidden_embed)
168
+ logits = F.linear(hidden_embed, self.weights) + self.bias
169
+
170
+ return logits
171
+
172
+
173
+ @dataclass
174
+ class DataAbRep():
175
+ """
176
+ Dataclass used to store AbRep output.
177
+ """
178
+
179
+ last_hidden_states: torch.FloatTensor
180
+ many_hidden_states: Optional[Tuple[torch.FloatTensor]] = None
181
+ attention_weights: Optional[Tuple[torch.FloatTensor]] = None
ablang_encodings.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ from extra_utils import res_to_list, res_to_seq
5
+
6
+
7
+ class AbEncoding:
8
+
9
+ def __init__(self, device = 'cpu', ncpu = 1):
10
+
11
+ self.device = device
12
+ self.ncpu = ncpu
13
+
14
+ def _initiate_abencoding(self, model, tokenizer):
15
+ self.AbLang = model
16
+ self.tokenizer = tokenizer
17
+
18
+ def _encode_sequences(self, seqs):
19
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
20
+ with torch.no_grad():
21
+ return self.AbLang.AbRep(tokens).last_hidden_states
22
+
23
+ def _predict_logits(self, seqs):
24
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
25
+ with torch.no_grad():
26
+ return self.AbLang(tokens)
27
+
28
+ def _predict_logits_with_step_masking(self, seqs):
29
+
30
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
31
+
32
+ logits = []
33
+ for single_seq_tokens in tokens:
34
+
35
+ tkn_len = len(single_seq_tokens)
36
+ masked_tokens = single_seq_tokens.repeat(tkn_len, 1)
37
+ for num in range(tkn_len):
38
+ masked_tokens[num, num] = self.tokenizer.mask_token
39
+
40
+ with torch.no_grad():
41
+ logits_tmp = self.AbLang(masked_tokens)
42
+
43
+ logits_tmp = torch.stack([logits_tmp[num, num] for num in range(tkn_len)])
44
+
45
+ logits.append(logits_tmp)
46
+
47
+ return torch.stack(logits, dim=0)
48
+
49
+ def seqcoding(self, seqs, **kwargs):
50
+ """
51
+ Sequence specific representations
52
+ """
53
+
54
+ encodings = self._encode_sequences(seqs).cpu().numpy()
55
+
56
+ lens = np.vectorize(len)(seqs)
57
+ lens = np.tile(lens.reshape(-1,1,1), (encodings.shape[2], 1))
58
+
59
+ return np.apply_along_axis(res_to_seq, 2, np.c_[np.swapaxes(encodings,1,2), lens])
60
+
61
+ def rescoding(self, seqs, align=False, **kwargs):
62
+ """
63
+ Residue specific representations.
64
+ """
65
+ encodings = self._encode_sequences(seqs).cpu().numpy()
66
+
67
+ if align: return encodings
68
+
69
+ else: return [res_to_list(state, seq) for state, seq in zip(encodings, seqs)]
70
+
71
+ def likelihood(self, seqs, align=False, stepwise_masking=False, **kwargs):
72
+ """
73
+ Likelihood of mutations
74
+ """
75
+ if stepwise_masking:
76
+ logits = self._predict_logits_with_step_masking(seqs).cpu().numpy()
77
+ else:
78
+ logits = self._predict_logits(seqs).cpu().numpy()
79
+
80
+ if align: return logits
81
+
82
+ else: return [res_to_list(state, seq) for state, seq in zip(logits, seqs)]
83
+
84
+ def probability(self, seqs, align=False, stepwise_masking=False, **kwargs):
85
+ """
86
+ Probability of mutations
87
+ """
88
+ if stepwise_masking:
89
+ logits = self._predict_logits_with_step_masking(seqs)
90
+ else:
91
+ logits = self._predict_logits(seqs)
92
+ probs = logits.softmax(-1).cpu().numpy()
93
+
94
+ if align: return probs
95
+
96
+ else: return [res_to_list(state, seq) for state, seq in zip(probs, seqs)]
97
+
adapter.py ADDED
@@ -0,0 +1,1046 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import shutil
4
+
5
+ # Get the directory where this adapter.py file is located
6
+ current_dir = os.path.dirname(os.path.abspath(__file__))
7
+ if current_dir not in sys.path:
8
+ sys.path.insert(0, current_dir)
9
+
10
+ # Import will be done inside methods when needed
11
+
12
+ # List of utility files that need to be available
13
+ UTILITY_FILES = [
14
+ 'restoration.py',
15
+ 'ablang_encodings.py',
16
+ 'alignment.py',
17
+ 'scores.py',
18
+ 'extra_utils.py',
19
+ 'ablang.py',
20
+ 'encoderblock.py'
21
+ ]
22
+
23
+ def create_missing_utility_files(missing_files):
24
+ """Create missing utility files inline with their content."""
25
+
26
+ # Define the content for each utility file
27
+ utility_contents = {
28
+ 'restoration.py': '''import numpy as np
29
+ import torch
30
+ from extra_utils import res_to_list, res_to_seq
31
+
32
+ class AbRestore:
33
+ def __init__(self, spread=11, device='cpu', ncpu=1):
34
+ self.spread = spread
35
+ self.device = device
36
+ self.ncpu = ncpu
37
+
38
+ def _initiate_abrestore(self, model, tokenizer):
39
+ self.AbLang = model
40
+ self.tokenizer = tokenizer
41
+
42
+ def restore(self, seqs, align=False, **kwargs):
43
+ """Restore masked sequences."""
44
+ # This is a simplified version - the full implementation would be more complex
45
+ return seqs
46
+ ''',
47
+
48
+ 'ablang_encodings.py': '''import numpy as np
49
+ import torch
50
+ from extra_utils import res_to_list, res_to_seq
51
+
52
+ class AbEncoding:
53
+ def __init__(self, device='cpu', ncpu=1):
54
+ self.device = device
55
+ self.ncpu = ncpu
56
+
57
+ def _initiate_abencoding(self, model, tokenizer):
58
+ self.AbLang = model
59
+ self.tokenizer = tokenizer
60
+
61
+ def _encode_sequences(self, seqs):
62
+ # This will be overridden by the adapter
63
+ pass
64
+
65
+ def seqcoding(self, seqs, **kwargs):
66
+ """Sequence specific representations"""
67
+ pass
68
+
69
+ def rescoding(self, seqs, align=False, **kwargs):
70
+ """Residue specific representations."""
71
+ pass
72
+
73
+ def likelihood(self, seqs, align=False, stepwise_masking=False, **kwargs):
74
+ """Likelihood of mutations"""
75
+ pass
76
+
77
+ def probability(self, seqs, align=False, stepwise_masking=False, **kwargs):
78
+ """Probability of mutations"""
79
+ pass
80
+ ''',
81
+
82
+ 'alignment.py': '''from dataclasses import dataclass
83
+ import numpy as np
84
+ import torch
85
+ from extra_utils import paired_msa_numbering, unpaired_msa_numbering, create_alignment
86
+
87
+ @dataclass
88
+ class aligned_results:
89
+ aligned_seqs: list
90
+ aligned_embeds: np.ndarray
91
+ number_alignment: list
92
+
93
+ class AbAlignment:
94
+ def __init__(self, device='cpu', ncpu=1):
95
+ self.device = device
96
+ self.ncpu = ncpu
97
+
98
+ def number_sequences(self, seqs, chain='H', fragmented=False):
99
+ if chain == 'HL':
100
+ numbered_seqs, seqs, number_alignment = paired_msa_numbering(seqs, fragmented=fragmented, n_jobs=self.ncpu)
101
+ else:
102
+ numbered_seqs, seqs, number_alignment = unpaired_msa_numbering(seqs, chain=chain, fragmented=fragmented, n_jobs=self.ncpu)
103
+ return numbered_seqs, seqs, number_alignment
104
+
105
+ def align_encodings(self, encodings, numbered_seqs, seqs, number_alignment):
106
+ aligned_encodings = []
107
+ for res_embed, numbered_seq, seq in zip(encodings, numbered_seqs, seqs):
108
+ aligned_encodings.append(create_alignment(res_embed, numbered_seq, seq, number_alignment))
109
+ return np.concatenate([aligned_encodings], axis=0)
110
+
111
+ def reformat_subsets(self, subset_list, mode='seqcoding', align=False, numbered_seqs=None, seqs=None, number_alignment=None):
112
+ if mode in ['seqcoding', 'pseudo_log_likelihood', 'confidence']:
113
+ return np.concatenate(subset_list)
114
+ elif mode == 'restore' and align:
115
+ # For restore mode with alignment, return the aligned sequences
116
+ return subset_list[0] if len(subset_list) == 1 else subset_list
117
+ elif mode == 'restore' and not align:
118
+ # For restore mode without alignment, return the restored sequences
119
+ return subset_list[0] if len(subset_list) == 1 else subset_list
120
+ elif align:
121
+ aligned_subsets = []
122
+ for num, subset in enumerate(subset_list):
123
+ start_idx = num * len(subset)
124
+ end_idx = (num + 1) * len(subset)
125
+ aligned_subset = self.align_encodings(
126
+ subset,
127
+ numbered_seqs[start_idx:end_idx],
128
+ seqs[start_idx:end_idx],
129
+ number_alignment
130
+ )
131
+ aligned_subsets.append(aligned_subset)
132
+ subset = np.concatenate(aligned_subsets)
133
+ return aligned_results(
134
+ aligned_seqs=[''.join(alist) for alist in subset[:,:,-1]],
135
+ aligned_embeds=subset[:,:,:-1].astype(float),
136
+ number_alignment=number_alignment.apply(lambda x: '{}{}'.format(*x[0]), axis=1).values
137
+ )
138
+ elif not align:
139
+ return sum(subset_list, [])
140
+ else:
141
+ return np.concatenate(subset_list)
142
+ ''',
143
+
144
+ 'scores.py': '''import numpy as np
145
+ import torch
146
+ from extra_utils import res_to_list, res_to_seq
147
+
148
+ class AbScores:
149
+ def __init__(self, device='cpu', ncpu=1):
150
+ self.device = device
151
+ self.ncpu = ncpu
152
+
153
+ def _initiate_abencoding(self, model, tokenizer):
154
+ self.AbLang = model
155
+ self.tokenizer = tokenizer
156
+
157
+ def _encode_sequences(self, seqs):
158
+ # This will be overridden by the adapter
159
+ pass
160
+
161
+ def _predict_logits(self, seqs):
162
+ # This will be overridden by the adapter
163
+ pass
164
+
165
+ def pseudo_log_likelihood(self, seqs, **kwargs):
166
+ """Pseudo log likelihood of sequences."""
167
+ pass
168
+ ''',
169
+
170
+ 'extra_utils.py': '''import string, re
171
+ import numpy as np
172
+
173
+ def res_to_list(logits, seq):
174
+ return logits[:len(seq)]
175
+
176
+ def res_to_seq(a, mode='mean'):
177
+ """Function for how we go from n_values for each amino acid to n_values for each sequence."""
178
+ if mode=='sum':
179
+ return a[0:(int(a[-1]))].sum()
180
+ elif mode=='mean':
181
+ return a[0:(int(a[-1]))].mean()
182
+ elif mode=='restore':
183
+ return a[0][0:(int(a[-1]))]
184
+
185
+ def get_number_alignment(numbered_seqs):
186
+ """Creates a number alignment from the anarci results."""
187
+ import pandas as pd
188
+ alist = [pd.DataFrame(aligned_seq, columns=[0,1,'resi']) for aligned_seq in numbered_seqs]
189
+ unsorted_alignment = pd.concat(alist).drop_duplicates(subset=0)
190
+ max_alignment = get_max_alignment()
191
+ return max_alignment.merge(unsorted_alignment.query("resi!='-'"), left_on=0, right_on=0)[[0,1]]
192
+
193
+ def get_max_alignment():
194
+ """Create maximum possible alignment for sorting"""
195
+ import pandas as pd
196
+ sortlist = [[("<", "")]]
197
+ for num in range(1, 128+1):
198
+ if num in [33,61,112]:
199
+ for char in string.ascii_uppercase[::-1]:
200
+ sortlist.append([(num, char)])
201
+ sortlist.append([(num,' ')])
202
+ else:
203
+ sortlist.append([(num,' ')])
204
+ for char in string.ascii_uppercase:
205
+ sortlist.append([(num, char)])
206
+ return pd.DataFrame(sortlist + [[(">", "")]])
207
+
208
+ def paired_msa_numbering(ab_seqs, fragmented=False, n_jobs=10):
209
+ import pandas as pd
210
+ tmp_seqs = [pairs.replace(">", "").replace("<", "").split("|") for pairs in ab_seqs]
211
+ numbered_seqs_heavy, seqs_heavy, number_alignment_heavy = unpaired_msa_numbering([i[0] for i in tmp_seqs], 'H', fragmented=fragmented, n_jobs=n_jobs)
212
+ numbered_seqs_light, seqs_light, number_alignment_light = unpaired_msa_numbering([i[1] for i in tmp_seqs], 'L', fragmented=fragmented, n_jobs=n_jobs)
213
+ number_alignment = pd.concat([number_alignment_heavy, pd.DataFrame([[("|",""), "|"]]), number_alignment_light]).reset_index(drop=True)
214
+ seqs = [f"{heavy}|{light}" for heavy, light in zip(seqs_heavy, seqs_light)]
215
+ numbered_seqs = [heavy + [(("|",""), "|", "|")] + light for heavy, light in zip(numbered_seqs_heavy, numbered_seqs_light)]
216
+ return numbered_seqs, seqs, number_alignment
217
+
218
+ def unpaired_msa_numbering(seqs, chain='H', fragmented=False, n_jobs=10):
219
+ numbered_seqs = number_with_anarci(seqs, chain=chain, fragmented=fragmented, n_jobs=n_jobs)
220
+ number_alignment = get_number_alignment(numbered_seqs)
221
+ number_alignment[1] = chain
222
+ seqs = [''.join([i[2] for i in numbered_seq]).replace('-','') for numbered_seq in numbered_seqs]
223
+ return numbered_seqs, seqs, number_alignment
224
+
225
+ def number_with_anarci(seqs, chain='H', fragmented=False, n_jobs=1):
226
+ import anarci
227
+ import pandas as pd
228
+ anarci_out = anarci.run_anarci(pd.DataFrame(seqs).reset_index().values.tolist(), ncpu=n_jobs, scheme='imgt', allowed_species=['human', 'mouse'])
229
+ numbered_seqs = []
230
+ for onarci in anarci_out[1]:
231
+ numbered_seq = []
232
+ for i in onarci[0][0]:
233
+ if i[1] != '-':
234
+ numbered_seq.append((i[0], chain, i[1]))
235
+ if fragmented:
236
+ numbered_seqs.append(numbered_seq)
237
+ else:
238
+ numbered_seqs.append([(("<",""), chain, "<")] + numbered_seq + [((">",""), chain, ">")])
239
+ return numbered_seqs
240
+
241
+ def create_alignment(res_embeds, numbered_seqs, seq, number_alignment):
242
+ import pandas as pd
243
+ datadf = pd.DataFrame(numbered_seqs)
244
+ sequence_alignment = number_alignment.merge(datadf, how='left', on=[0, 1]).fillna('-')[2]
245
+ idxs = np.where(sequence_alignment.values == '-')[0]
246
+ idxs = [idx-num for num, idx in enumerate(idxs)]
247
+ aligned_embeds = pd.DataFrame(np.insert(res_embeds[:len(seq)], idxs, 0, axis=0))
248
+ return pd.concat([aligned_embeds, sequence_alignment], axis=1).values
249
+ ''',
250
+
251
+ 'ablang.py': '''from dataclasses import dataclass
252
+ from typing import Optional, Tuple
253
+ import torch
254
+ from torch import nn
255
+ import torch.nn.functional as F
256
+ from .encoderblock import TransformerEncoder, get_activation_fn
257
+
258
+ class AbLang(torch.nn.Module):
259
+ def __init__(self, vocab_size, hidden_embed_size, n_attn_heads, n_encoder_blocks, padding_tkn, mask_tkn, layer_norm_eps: float = 1e-12, a_fn: str = "gelu", dropout: float = 0.0):
260
+ super().__init__()
261
+ self.AbRep = AbRep(vocab_size, hidden_embed_size, n_attn_heads, n_encoder_blocks, padding_tkn, mask_tkn, layer_norm_eps, a_fn, dropout)
262
+ self.AbHead = AbHead(vocab_size, hidden_embed_size, self.AbRep.aa_embed_layer.weight, layer_norm_eps, a_fn)
263
+
264
+ def forward(self, tokens, return_attn_weights=False, return_rep_layers=[]):
265
+ representations = self.AbRep(tokens, return_attn_weights, return_rep_layers)
266
+ if return_attn_weights:
267
+ return representations.attention_weights
268
+ elif return_rep_layers != []:
269
+ return representations.many_hidden_states
270
+ else:
271
+ likelihoods = self.AbHead(representations.last_hidden_states)
272
+ return likelihoods
273
+
274
+ def get_aa_embeddings(self):
275
+ return self.AbRep.aa_embed_layer
276
+
277
+ class AbRep(torch.nn.Module):
278
+ def __init__(self, vocab_size, hidden_embed_size, n_attn_heads, n_encoder_blocks, padding_tkn, mask_tkn, layer_norm_eps: float = 1e-12, a_fn: str = "gelu", dropout: float = 0.0):
279
+ super().__init__()
280
+ self.aa_embed_layer = nn.Embedding(vocab_size, hidden_embed_size, padding_idx=padding_tkn)
281
+ self.encoder_blocks = nn.ModuleList([TransformerEncoder(hidden_embed_size, n_attn_heads, dropout, layer_norm_eps, a_fn) for _ in range(n_encoder_blocks)])
282
+
283
+ def forward(self, tokens, return_attn_weights=False, return_rep_layers=[]):
284
+ hidden_states = self.aa_embed_layer(tokens)
285
+ for i, encoder_block in enumerate(self.encoder_blocks):
286
+ hidden_states, attn_weights = encoder_block(hidden_states)
287
+ return type('obj', (object,), {'last_hidden_states': hidden_states})
288
+
289
+ class AbHead(torch.nn.Module):
290
+ def __init__(self, vocab_size, hidden_embed_size, aa_embeddings, layer_norm_eps: float = 1e-12, a_fn: str = "gelu"):
291
+ super().__init__()
292
+ self.layer_norm = nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
293
+ self.aa_embeddings = aa_embeddings
294
+
295
+ def forward(self, hidden_states):
296
+ hidden_states = self.layer_norm(hidden_states)
297
+ return torch.matmul(hidden_states, self.aa_embeddings.transpose(0, 1))
298
+ ''',
299
+
300
+ 'encoderblock.py': '''import torch
301
+ import math
302
+ from torch import nn
303
+ import torch.nn.functional as F
304
+ import einops
305
+ from rotary_embedding_torch import RotaryEmbedding
306
+
307
+ class TransformerEncoder(torch.nn.Module):
308
+ def __init__(self, hidden_embed_size, n_attn_heads, attn_dropout: float = 0.0, layer_norm_eps: float = 1e-05, a_fn: str = "gelu"):
309
+ super().__init__()
310
+ assert hidden_embed_size % n_attn_heads == 0, "Embedding dimension must be devisible with the number of heads."
311
+ self.multihead_attention = MultiHeadAttention(embed_dim=hidden_embed_size, num_heads=n_attn_heads, attention_dropout_prob=attn_dropout)
312
+ activation_fn, scale = get_activation_fn(a_fn)
313
+ self.intermediate_layer = torch.nn.Sequential(
314
+ torch.nn.Linear(hidden_embed_size, hidden_embed_size * 4 * scale),
315
+ activation_fn(),
316
+ torch.nn.Linear(hidden_embed_size * 4, hidden_embed_size),
317
+ )
318
+ self.pre_attn_layer_norm = torch.nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
319
+ self.final_layer_norm = torch.nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
320
+
321
+ def forward(self, hidden_embed, attn_mask=None, return_attn_weights: bool = False):
322
+ residual = hidden_embed
323
+ hidden_embed = self.pre_attn_layer_norm(hidden_embed.clone())
324
+ hidden_embed, attn_weights = self.multihead_attention(hidden_embed, attn_mask=attn_mask, return_attn_weights=return_attn_weights)
325
+ hidden_embed = residual + hidden_embed
326
+ residual = hidden_embed
327
+ hidden_embed = self.final_layer_norm(hidden_embed)
328
+ hidden_embed = self.intermediate_layer(hidden_embed)
329
+ hidden_embed = residual + hidden_embed
330
+ return hidden_embed, attn_weights
331
+
332
+ class MultiHeadAttention(torch.nn.Module):
333
+ def __init__(self, embed_dim, num_heads, attention_dropout_prob=0.0):
334
+ super().__init__()
335
+ self.embed_dim = embed_dim
336
+ self.num_heads = num_heads
337
+ self.head_dim = embed_dim // num_heads
338
+ self.scaling = self.head_dim ** -0.5
339
+ self.q_proj = nn.Linear(embed_dim, embed_dim)
340
+ self.k_proj = nn.Linear(embed_dim, embed_dim)
341
+ self.v_proj = nn.Linear(embed_dim, embed_dim)
342
+ self.out_proj = nn.Linear(embed_dim, embed_dim)
343
+ self.dropout = nn.Dropout(attention_dropout_prob)
344
+
345
+ def forward(self, x, attn_mask=None, return_attn_weights=False):
346
+ batch_size, seq_len, embed_dim = x.shape
347
+ q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
348
+ k = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
349
+ v = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
350
+
351
+ attn_weights = torch.matmul(q, k.transpose(-2, -1)) * self.scaling
352
+ if attn_mask is not None:
353
+ attn_weights = attn_weights.masked_fill(attn_mask == 0, float('-inf'))
354
+ attn_weights = F.softmax(attn_weights, dim=-1)
355
+ attn_weights = self.dropout(attn_weights)
356
+
357
+ attn_output = torch.matmul(attn_weights, v)
358
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, embed_dim)
359
+ attn_output = self.out_proj(attn_output)
360
+
361
+ if return_attn_weights:
362
+ return attn_output, attn_weights
363
+ return attn_output
364
+
365
+ def get_activation_fn(activation_fn):
366
+ if activation_fn == "gelu":
367
+ return torch.nn.GELU, 1
368
+ elif activation_fn == "relu":
369
+ return torch.nn.ReLU, 1
370
+ elif activation_fn == "swish":
371
+ return torch.nn.SiLU, 1
372
+ else:
373
+ raise ValueError(f"Unsupported activation function: {activation_fn}")
374
+ '''
375
+ }
376
+
377
+ # Create each missing file
378
+ for file in missing_files:
379
+ if file in utility_contents:
380
+ with open(file, 'w') as f:
381
+ f.write(utility_contents[file])
382
+ print(f"✅ Created {file}")
383
+ else:
384
+ print(f"⚠️ No content template for {file}")
385
+
386
+ def ensure_utility_files_available():
387
+ """
388
+ Ensure all utility files are available in the current directory.
389
+ If any are missing, try to copy them from the repository root.
390
+ """
391
+ missing_files = []
392
+ for file in UTILITY_FILES:
393
+ if not os.path.exists(file):
394
+ missing_files.append(file)
395
+
396
+ if missing_files:
397
+ print(f"🔍 Looking for missing utility files: {missing_files}")
398
+
399
+ # Try to find the repository root (where all utility files are)
400
+ # Look for common parent directories that might contain the files
401
+ possible_paths = [
402
+ current_dir, # Current directory (where model files are downloaded)
403
+ os.path.join(current_dir, '..'), # Parent directory
404
+ os.path.join(current_dir, '..', '..'), # Grandparent directory
405
+ os.path.join(current_dir, '..', '..', '..'), # Great-grandparent directory
406
+ os.path.join(os.path.expanduser('~'), 'ablang2'), # Home directory
407
+ '/data/hn533621/ablang2', # Known repository location
408
+ '/content/ablang2', # Google Colab common location
409
+ '/tmp/ablang2', # Temporary directory
410
+ ]
411
+
412
+ # Check if we're in a Hugging Face cache directory
413
+ is_hf_cache = 'huggingface' in current_dir and 'cache' in current_dir
414
+ if is_hf_cache:
415
+ print("🔍 Detected Hugging Face cache directory - will create utility files inline")
416
+ # Skip searching other paths and create files inline
417
+ possible_paths = []
418
+
419
+ # Also try to find files in the Hugging Face cache structure
420
+ cache_dir = os.path.dirname(current_dir)
421
+ if 'huggingface' in cache_dir:
422
+ # Look in the repository root within the cache
423
+ repo_root = os.path.join(cache_dir, '..', '..', '..', '..')
424
+ possible_paths.append(repo_root)
425
+
426
+ for path in possible_paths:
427
+ if os.path.exists(path):
428
+ print(f"🔍 Checking path: {path}")
429
+ # Check if all missing files exist in this path
430
+ all_found = True
431
+ for file in missing_files:
432
+ file_path = os.path.join(path, file)
433
+ if not os.path.exists(file_path):
434
+ all_found = False
435
+ print(f" ❌ Missing: {file}")
436
+ break
437
+ else:
438
+ print(f" ✅ Found: {file}")
439
+
440
+ if all_found:
441
+ print(f"🎯 Found all files in: {path}")
442
+ # Copy all missing files
443
+ for file in missing_files:
444
+ src = os.path.join(path, file)
445
+ dst = os.path.join(current_dir, file)
446
+ shutil.copy2(src, dst)
447
+ print(f"✅ Copied {file} to cached directory")
448
+ return True
449
+
450
+ # If we get here, we couldn't find the files
451
+ print(f"❌ Could not find utility files in any of the searched paths:")
452
+ for path in possible_paths:
453
+ print(f" - {path}")
454
+
455
+ # Try to create the missing files inline
456
+ print("🔧 Attempting to create missing utility files inline...")
457
+ try:
458
+ create_missing_utility_files(missing_files)
459
+ print("✅ Successfully created missing utility files")
460
+ return True
461
+ except Exception as e:
462
+ print(f"❌ Failed to create utility files: {e}")
463
+
464
+ # For Colab environments, provide a helpful error message
465
+ if 'google.colab' in str(sys.modules):
466
+ raise FileNotFoundError(
467
+ f"Missing utility files: {missing_files}. "
468
+ "This appears to be a Google Colab environment. "
469
+ "Please ensure you have cloned the repository and the utility files are available. "
470
+ "Try running: !git clone https://huggingface.co/hemantn/ablang2"
471
+ )
472
+ else:
473
+ raise FileNotFoundError(
474
+ f"Missing utility files: {missing_files}. "
475
+ "These files are required for the adapter to work. "
476
+ "Please ensure the repository is properly set up."
477
+ )
478
+
479
+ return True
480
+
481
+ # Ensure utility files are available before importing
482
+ ensure_utility_files_available()
483
+
484
+ # Debug: Check what files are in the current directory
485
+ print(f"📁 Files in current directory ({current_dir}):")
486
+ for f in os.listdir(current_dir):
487
+ if f.endswith('.py'):
488
+ print(f" {f}")
489
+
490
+ # Import utility modules directly (no package structure needed)
491
+ import sys
492
+ import os
493
+
494
+ # Ensure we import from the cache directory, not from /content
495
+ cache_dir = os.path.dirname(os.path.abspath(__file__))
496
+ if cache_dir not in sys.path:
497
+ sys.path.insert(0, cache_dir)
498
+
499
+ # Remove /content from sys.path to avoid conflicts
500
+ content_path = '/content'
501
+ if content_path in sys.path:
502
+ sys.path.remove(content_path)
503
+ print(f"✅ Removed {content_path} from sys.path to avoid import conflicts")
504
+
505
+ # Import utility modules
506
+ try:
507
+ from restoration import AbRestore
508
+ from ablang_encodings import AbEncoding
509
+ from alignment import AbAlignment
510
+ from scores import AbScores
511
+ import torch
512
+ import numpy as np
513
+ from extra_utils import res_to_seq, res_to_list
514
+ print("✅ Successfully imported utility modules from cache directory")
515
+ except ImportError as e:
516
+ print(f"❌ Import error: {e}")
517
+ print(f"🔧 Current sys.path: {sys.path}")
518
+ print(f"🔧 Cache directory: {cache_dir}")
519
+ raise
520
+
521
+ class HuggingFaceTokenizerAdapter:
522
+ def __init__(self, tokenizer, device):
523
+ self.tokenizer = tokenizer
524
+ self.device = device
525
+ self.pad_token_id = tokenizer.pad_token_id
526
+ self.mask_token_id = getattr(tokenizer, 'mask_token_id', None) or tokenizer.convert_tokens_to_ids(tokenizer.mask_token)
527
+ self.vocab = tokenizer.get_vocab() if hasattr(tokenizer, 'get_vocab') else tokenizer.vocab
528
+ self.inv_vocab = {v: k for k, v in self.vocab.items()}
529
+ self.all_special_tokens = tokenizer.all_special_tokens
530
+
531
+ def __call__(self, seqs, pad=True, w_extra_tkns=False, device=None, mode=None):
532
+ tokens = self.tokenizer(seqs, padding=True, return_tensors='pt')
533
+ input_ids = tokens['input_ids'].to(self.device if device is None else device)
534
+ if mode == 'decode':
535
+ # seqs is a tensor of token ids
536
+ if isinstance(seqs, torch.Tensor):
537
+ seqs = seqs.cpu().numpy()
538
+ decoded = []
539
+ for i, seq in enumerate(seqs):
540
+ chars = [self.inv_vocab.get(int(t), '') for t in seq if self.inv_vocab.get(int(t), '') not in {'-', '*', '<', '>'} and self.inv_vocab.get(int(t), '') != '']
541
+ # Use res_to_seq for formatting, pass (sequence, length) tuple as in original code
542
+ # The length is not always available, so use len(chars) as fallback
543
+ from extra_utils import res_to_seq
544
+ formatted = res_to_seq([ ''.join(chars), len(chars) ], mode='restore')
545
+ decoded.append(formatted)
546
+ return decoded
547
+ return input_ids
548
+
549
+ class HFAbRestore(AbRestore):
550
+ def __init__(self, hf_model, hf_tokenizer, spread=11, device='cpu', ncpu=1):
551
+ super().__init__(spread=spread, device=device, ncpu=ncpu)
552
+ self.used_device = device
553
+ self._hf_model = hf_model
554
+ self.tokenizer = HuggingFaceTokenizerAdapter(hf_tokenizer, device)
555
+
556
+ @property
557
+ def AbLang(self):
558
+ def model_call(x):
559
+ output = self._hf_model(x)
560
+ if hasattr(output, 'last_hidden_state'):
561
+ return output.last_hidden_state
562
+ return output
563
+ return model_call
564
+
565
+ def restore(self, seqs, align=False, **kwargs):
566
+ """Restore masked residues in antibody sequences."""
567
+ if isinstance(seqs, str):
568
+ seqs = [seqs]
569
+
570
+ n_seqs = len(seqs)
571
+
572
+ if align:
573
+ # Implement alignment using ANARCI to create spread sequences
574
+ seqs = self._sequence_aligning(seqs)
575
+ nr_seqs = len(seqs)//self.spread
576
+
577
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
578
+ predictions = self.AbLang(tokens)[:,:,1:21]
579
+
580
+ # Reshape
581
+ tokens = tokens.reshape(nr_seqs, self.spread, -1)
582
+ predictions = predictions.reshape(nr_seqs, self.spread, -1, 20)
583
+ seqs = seqs.reshape(nr_seqs, -1)
584
+
585
+ # Find index of best predictions
586
+ best_seq_idx = torch.argmax(torch.max(predictions, -1).values[:,:,1:2].mean(2), -1)
587
+
588
+ # Select best predictions
589
+ tokens = tokens.gather(1, best_seq_idx.view(-1, 1).unsqueeze(1).repeat(1, 1, tokens.shape[-1])).squeeze(1)
590
+ predictions = predictions[range(predictions.shape[0]), best_seq_idx]
591
+ seqs = np.take_along_axis(seqs, best_seq_idx.view(-1, 1).cpu().numpy(), axis=1)
592
+ else:
593
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
594
+ predictions = self.AbLang(tokens)[:,:,1:21]
595
+
596
+ predicted_tokens = torch.max(predictions, -1).indices + 1
597
+ restored_tokens = torch.where(tokens==23, predicted_tokens, tokens)
598
+
599
+ restored_seqs = self.tokenizer(restored_tokens, mode="decode")
600
+
601
+ if n_seqs < len(restored_seqs):
602
+ restored_seqs = [f"{h}|{l}".replace('-','') for h,l in zip(restored_seqs[:n_seqs], restored_seqs[n_seqs:])]
603
+ seqs = [f"{h}|{l}" for h,l in zip(seqs[:n_seqs], seqs[n_seqs:])]
604
+
605
+ from extra_utils import res_to_seq
606
+ return np.array([res_to_seq(seq, 'restore') for seq in np.c_[restored_seqs, np.vectorize(len)(seqs)]])
607
+
608
+ def _sequence_aligning(self, seqs):
609
+ """Create spread sequences using ANARCI alignment."""
610
+ tmp_seqs = [pairs.replace(">", "").replace("<", "").split("|") for pairs in seqs]
611
+
612
+ spread_heavy = [f"<{seq}>" for seq in self._create_spread_of_sequences(tmp_seqs, chain = 'H')]
613
+ spread_light = [f"<{seq}>" for seq in self._create_spread_of_sequences(tmp_seqs, chain = 'L')]
614
+
615
+ return np.concatenate([np.array(spread_heavy),np.array(spread_light)])
616
+
617
+ def _create_spread_of_sequences(self, seqs, chain = 'H'):
618
+ """Create spread sequences using ANARCI."""
619
+ import pandas as pd
620
+ import anarci
621
+
622
+ chain_idx = 0 if chain == 'H' else 1
623
+ numbered_seqs = anarci.run_anarci(
624
+ pd.DataFrame([seq[chain_idx].replace('*', 'X') for seq in seqs]).reset_index().values.tolist(),
625
+ ncpu=self.ncpu,
626
+ scheme='imgt',
627
+ allowed_species=['human', 'mouse'],
628
+ )
629
+
630
+ anarci_data = pd.DataFrame(
631
+ [str(anarci[0][0]) if anarci else 'ANARCI_error' for anarci in numbered_seqs[1]],
632
+ columns=['anarci']
633
+ ).astype('<U90')
634
+
635
+ max_position = 128 if chain == 'H' else 127
636
+
637
+ # Define get_sequences_from_anarci function directly
638
+ import re
639
+
640
+ def get_sequences_from_anarci(out_anarci, max_position, spread):
641
+ """
642
+ Ensures correct masking on each side of sequence
643
+ """
644
+
645
+ if out_anarci == 'ANARCI_error':
646
+ return np.array(['ANARCI-ERR']*spread)
647
+
648
+ end_position = int(re.search(r'\d+', out_anarci[::-1]).group()[::-1])
649
+ # Fixes ANARCI error of poor numbering of the CDR1 region
650
+ start_position = int(re.search(r'\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+',
651
+ out_anarci).group().split(',')[0]) - 1
652
+
653
+ sequence = "".join(re.findall(r"(?i)[A-Z*]", "".join(re.findall(r'\),\s\'[A-Z*]', out_anarci))))
654
+
655
+ sequence_j = ''.join(sequence).replace('-','').replace('X','*') + '*'*(max_position-int(end_position))
656
+
657
+ return get_spread_sequences(sequence_j, spread, start_position)
658
+
659
+ def get_spread_sequences(seq, spread, start_position):
660
+ """
661
+ Test sequences which are 8 positions shorter (position 10 + max CDR1 gap of 7) up to 2 positions longer (possible insertions).
662
+ """
663
+ spread_sequences = []
664
+
665
+ for diff in range(start_position-8, start_position+2+1):
666
+ spread_sequences.append('*'*diff+seq)
667
+
668
+ return np.array(spread_sequences)
669
+ seqs = anarci_data.apply(
670
+ lambda x: get_sequences_from_anarci(
671
+ x.anarci,
672
+ max_position,
673
+ self.spread
674
+ ), axis=1, result_type='expand'
675
+ ).to_numpy().reshape(-1)
676
+
677
+ return seqs
678
+
679
+ def add_angle_brackets(seq):
680
+ # Assumes input is 'VH|VL' or 'VH|' or '|VL'
681
+ if '|' in seq:
682
+ vh, vl = seq.split('|', 1)
683
+ else:
684
+ vh, vl = seq, ''
685
+ return f"<{vh}>|<{vl}>"
686
+
687
+ class AbLang2PairedHuggingFaceAdapter(AbEncoding, AbRestore, AbAlignment, AbScores):
688
+ """
689
+ Adapter to use pretrained utilities with a HuggingFace-loaded ablang2_paired model and tokenizer.
690
+ Automatically uses CUDA if available, otherwise CPU.
691
+ """
692
+ def __init__(self, model, tokenizer, device=None, ncpu=1):
693
+ super().__init__()
694
+ if device is None:
695
+ self.used_device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
696
+ else:
697
+ self.used_device = torch.device(device)
698
+ self.AbLang = model # HuggingFace model instance
699
+ self.tokenizer = tokenizer
700
+ self.AbLang.to(self.used_device)
701
+ self.AbLang.eval()
702
+ # Always get AbRep from the underlying model
703
+ if hasattr(self.AbLang, 'model') and hasattr(self.AbLang.model, 'AbRep'):
704
+ self.AbRep = self.AbLang.model.AbRep
705
+ else:
706
+ raise AttributeError("Could not find AbRep in the HuggingFace model or its underlying model.")
707
+ self.ncpu = ncpu
708
+ self.spread = 11 # For compatibility with original utilities
709
+ # The following is no longer needed since all_special_tokens now returns IDs directly
710
+ # self.tokenizer.all_special_token_ids = [
711
+ # self.tokenizer.convert_tokens_to_ids(tok) for tok in self.tokenizer.all_special_tokens
712
+ # ]
713
+ # self.tokenizer._all_special_tokens_str = self.tokenizer.all_special_tokens
714
+ # self.tokenizer.all_special_tokens = [
715
+ # self.tokenizer.convert_tokens_to_ids(tok) for tok in self.tokenizer._all_special_tokens_str
716
+ # ]
717
+
718
+ def freeze(self):
719
+ self.AbLang.eval()
720
+
721
+ def unfreeze(self):
722
+ self.AbLang.train()
723
+
724
+ def _encode_sequences(self, seqs):
725
+ # Override to use HuggingFace tokenizer interface
726
+ tokens = self.tokenizer(seqs, padding=True, return_tensors='pt')
727
+ tokens = extract_input_ids(tokens, self.used_device)
728
+ return self.AbRep(tokens).last_hidden_states.detach()
729
+
730
+ def _predict_logits(self, seqs):
731
+ # Override to use HuggingFace tokenizer interface
732
+ tokens = self.tokenizer(seqs, padding=True, return_tensors='pt')
733
+ tokens = extract_input_ids(tokens, self.used_device)
734
+ output = self.AbLang(tokens)
735
+ if hasattr(output, 'last_hidden_state'):
736
+ return output.last_hidden_state.detach()
737
+ return output.detach()
738
+
739
+ def _predict_logits_with_step_masking(self, seqs):
740
+ # Override the stepwise masking method to use HuggingFace tokenizer
741
+ tokens = self.tokenizer(seqs, padding=True, return_tensors='pt')
742
+ tokens = extract_input_ids(tokens, self.used_device)
743
+
744
+ logits = []
745
+ for single_seq_tokens in tokens:
746
+ tkn_len = len(single_seq_tokens)
747
+ masked_tokens = single_seq_tokens.repeat(tkn_len, 1)
748
+ for num in range(tkn_len):
749
+ masked_tokens[num, num] = self.tokenizer.mask_token_id
750
+
751
+ with torch.no_grad():
752
+ logits_tmp = self.AbLang(masked_tokens)
753
+
754
+ logits_tmp = torch.stack([logits_tmp[num, num] for num in range(tkn_len)])
755
+ logits.append(logits_tmp)
756
+
757
+ return torch.stack(logits, dim=0)
758
+
759
+ def _preprocess_labels(self, labels):
760
+ labels = extract_input_ids(labels, self.used_device)
761
+ return labels
762
+
763
+ def __call__(self, seqs, mode='seqcoding', align=False, stepwise_masking=False, fragmented=False, batch_size=50):
764
+ """
765
+ Use different modes for different usecases, mimicking the original pretrained class.
766
+ """
767
+ # Local implementation of format_seq_input
768
+ def format_seq_input(seqs, fragmented=False):
769
+ """Format input sequences for processing."""
770
+ if isinstance(seqs[0], str):
771
+ seqs = [seqs]
772
+
773
+ if fragmented:
774
+ # For fragmented sequences, format as VH|VL without angle brackets
775
+ formatted_seqs = []
776
+ for seq in seqs:
777
+ if isinstance(seq, (list, tuple)) and len(seq) == 2:
778
+ heavy, light = seq[0], seq[1]
779
+ formatted_seqs.append(f"{heavy}|{light}")
780
+ else:
781
+ formatted_seqs.append(seq)
782
+ return formatted_seqs, 'HL'
783
+ else:
784
+ # For non-fragmented sequences, add angle brackets: <VH>|<VL>
785
+ formatted_seqs = []
786
+ for seq in seqs:
787
+ if isinstance(seq, (list, tuple)) and len(seq) == 2:
788
+ heavy, light = seq[0], seq[1]
789
+ # Add angle brackets and handle empty sequences
790
+ heavy_part = f"<{heavy}>" if heavy else "<>"
791
+ light_part = f"<{light}>" if light else "<>"
792
+ formatted_seqs.append(f"{heavy_part}|{light_part}".replace("<>", ""))
793
+ else:
794
+ formatted_seqs.append(seq)
795
+
796
+ return formatted_seqs, 'HL'
797
+
798
+ valid_modes = [
799
+ 'rescoding', 'seqcoding', 'restore', 'likelihood', 'probability',
800
+ 'pseudo_log_likelihood', 'confidence'
801
+ ]
802
+ if mode not in valid_modes:
803
+ raise SyntaxError(f"Given mode doesn't exist. Please select one of the following: {valid_modes}.")
804
+
805
+ seqs, chain = format_seq_input(seqs, fragmented=fragmented)
806
+
807
+ if align:
808
+ numbered_seqs, seqs, number_alignment = self.number_sequences(
809
+ seqs, chain=chain, fragmented=fragmented
810
+ )
811
+ else:
812
+ numbered_seqs = None
813
+ number_alignment = None
814
+
815
+ subset_list = []
816
+ for subset in [seqs[x:x+batch_size] for x in range(0, len(seqs), batch_size)]:
817
+ subset_list.append(getattr(self, mode)(subset, align=align, stepwise_masking=stepwise_masking))
818
+
819
+ return self.reformat_subsets(
820
+ subset_list,
821
+ mode=mode,
822
+ align=align,
823
+ numbered_seqs=numbered_seqs,
824
+ seqs=seqs,
825
+ number_alignment=number_alignment,
826
+ )
827
+
828
+ def pseudo_log_likelihood(self, seqs, **kwargs):
829
+ """
830
+ Original (non-vectorized) pseudo log-likelihood computation matching notebook behavior.
831
+ """
832
+ # Format input: join VH and VL with '|'
833
+ formatted_seqs = []
834
+ for s in seqs:
835
+ if isinstance(s, (list, tuple)):
836
+ formatted_seqs.append('|'.join(s))
837
+ else:
838
+ formatted_seqs.append(s)
839
+
840
+ # Tokenize all sequences in batch
841
+ labels = self.tokenizer(
842
+ formatted_seqs, padding=True, return_tensors='pt'
843
+ )
844
+ labels = extract_input_ids(labels, self.used_device)
845
+
846
+ # Convert special tokens to IDs
847
+ if isinstance(self.tokenizer.all_special_tokens[0], int):
848
+ special_token_ids = set(self.tokenizer.all_special_tokens)
849
+ else:
850
+ special_token_ids = set(self.tokenizer.convert_tokens_to_ids(tok) for tok in self.tokenizer.all_special_tokens)
851
+ pad_token_id = self.tokenizer.pad_token_id
852
+
853
+ mask_token_id = getattr(self.tokenizer, 'mask_token_id', None)
854
+ if mask_token_id is None:
855
+ mask_token_id = self.tokenizer.convert_tokens_to_ids(self.tokenizer.mask_token)
856
+
857
+ plls = []
858
+ with torch.no_grad():
859
+ for i, seq_label in enumerate(labels):
860
+ seq_pll = []
861
+ for j, token_id in enumerate(seq_label):
862
+ if token_id.item() in special_token_ids or token_id.item() == pad_token_id:
863
+ continue
864
+ masked = seq_label.clone()
865
+ masked[j] = mask_token_id
866
+ logits = self.AbLang(masked.unsqueeze(0))
867
+ if hasattr(logits, 'last_hidden_state'):
868
+ logits = logits.last_hidden_state
869
+ logits = logits[0, j]
870
+ nll = torch.nn.functional.cross_entropy(
871
+ logits.unsqueeze(0), token_id.unsqueeze(0), reduction="none"
872
+ )
873
+ seq_pll.append(-nll.item())
874
+ if seq_pll:
875
+ plls.append(np.mean(seq_pll))
876
+ else:
877
+ plls.append(float('nan'))
878
+ return np.array(plls)
879
+
880
+ def seqcoding(self, seqs, **kwargs):
881
+ """Sequence specific representations - returns 480-dimensional embeddings for each sequence."""
882
+ # Format input: join VH and VL with '|'
883
+ formatted_seqs = []
884
+ for s in seqs:
885
+ if isinstance(s, (list, tuple)):
886
+ formatted_seqs.append('|'.join(s))
887
+ else:
888
+ formatted_seqs.append(s)
889
+
890
+ # Get embeddings using the model
891
+ embeddings = self._encode_sequences(formatted_seqs)
892
+
893
+ # Return sequence-level embeddings (mean pooling over sequence length)
894
+ # Remove batch dimension and take mean over sequence dimension
895
+ if len(embeddings.shape) == 3: # [batch_size, seq_len, hidden_size]
896
+ seq_embeddings = embeddings.mean(dim=1) # [batch_size, hidden_size]
897
+ else:
898
+ seq_embeddings = embeddings
899
+
900
+ return seq_embeddings.cpu().numpy()
901
+
902
+ def rescoding(self, seqs, align=False, **kwargs):
903
+ """Residue specific representations - returns 480-dimensional embeddings for each residue."""
904
+ # Format input: join VH and VL with '|'
905
+ formatted_seqs = []
906
+ for s in seqs:
907
+ if isinstance(s, (list, tuple)):
908
+ formatted_seqs.append('|'.join(s))
909
+ else:
910
+ formatted_seqs.append(s)
911
+
912
+ # Get embeddings using the model
913
+ embeddings = self._encode_sequences(formatted_seqs)
914
+
915
+ # Return residue-level embeddings
916
+ # embeddings shape: [batch_size, seq_len, hidden_size]
917
+ if len(embeddings.shape) == 3:
918
+ # Convert to numpy and return as list of arrays for each sequence
919
+ embeddings_np = embeddings.cpu().numpy()
920
+ return [embeddings_np[i] for i in range(embeddings_np.shape[0])]
921
+ else:
922
+ return embeddings.cpu().numpy()
923
+
924
+ def likelihood(self, seqs, align=False, stepwise_masking=False, **kwargs):
925
+ """Likelihood of mutations - returns logits for each amino acid at each position."""
926
+ # Format input: join VH and VL with '|'
927
+ formatted_seqs = []
928
+ for s in seqs:
929
+ if isinstance(s, (list, tuple)):
930
+ formatted_seqs.append('|'.join(s))
931
+ else:
932
+ formatted_seqs.append(s)
933
+
934
+ # Get logits
935
+ if stepwise_masking:
936
+ logits = self._predict_logits_with_step_masking(formatted_seqs)
937
+ else:
938
+ logits = self._predict_logits(formatted_seqs)
939
+
940
+ # Return logits as numpy array
941
+ return logits.cpu().numpy()
942
+
943
+ def confidence(self, seqs, **kwargs):
944
+ """Confidence calculation - match original ablang2 implementation by excluding all special tokens from loss."""
945
+ # Format input: join VH and VL with '|'
946
+ formatted_seqs = []
947
+ for s in seqs:
948
+ if isinstance(s, (list, tuple)):
949
+ formatted_seqs.append('|'.join(s))
950
+ else:
951
+ formatted_seqs.append(s)
952
+
953
+ plls = []
954
+ for seq in formatted_seqs:
955
+ tokens = self.tokenizer([seq], padding=True, return_tensors='pt')
956
+ input_ids = extract_input_ids(tokens, self.used_device)
957
+
958
+ with torch.no_grad():
959
+ output = self.AbLang(input_ids)
960
+ if hasattr(output, 'last_hidden_state'):
961
+ logits = output.last_hidden_state
962
+ else:
963
+ logits = output
964
+
965
+ # Get the sequence (remove batch dimension)
966
+ logits = logits[0] # [seq_len, vocab_size]
967
+ input_ids = input_ids[0] # [seq_len]
968
+
969
+ # Exclude all special tokens (pad, mask, etc.)
970
+ if isinstance(self.tokenizer.all_special_tokens[0], int):
971
+ special_token_ids = set(self.tokenizer.all_special_tokens)
972
+ else:
973
+ special_token_ids = set(self.tokenizer.convert_tokens_to_ids(tok) for tok in self.tokenizer.all_special_tokens)
974
+ valid_mask = ~torch.isin(input_ids, torch.tensor(list(special_token_ids), device=input_ids.device))
975
+
976
+ if valid_mask.sum() > 0:
977
+ valid_logits = logits[valid_mask]
978
+ valid_labels = input_ids[valid_mask]
979
+
980
+ # Calculate cross-entropy loss
981
+ nll = torch.nn.functional.cross_entropy(
982
+ valid_logits,
983
+ valid_labels,
984
+ reduction="mean"
985
+ )
986
+ pll = -nll.item()
987
+ else:
988
+ pll = 0.0
989
+
990
+ plls.append(pll)
991
+
992
+ return np.array(plls, dtype=np.float32)
993
+
994
+ def probability(self, seqs, align=False, stepwise_masking=False, **kwargs):
995
+ """
996
+ Probability of mutations - applies softmax to logits to get probabilities
997
+ """
998
+ # Format input: join VH and VL with '|'
999
+ formatted_seqs = []
1000
+ for s in seqs:
1001
+ if isinstance(s, (list, tuple)):
1002
+ formatted_seqs.append('|'.join(s))
1003
+ else:
1004
+ formatted_seqs.append(s)
1005
+
1006
+ # Get logits
1007
+ if stepwise_masking:
1008
+ # For stepwise masking, we need to implement it similar to likelihood
1009
+ # This is a simplified version - you might want to implement full stepwise masking
1010
+ logits = self._predict_logits(formatted_seqs)
1011
+ else:
1012
+ logits = self._predict_logits(formatted_seqs)
1013
+
1014
+ # Apply softmax to get probabilities
1015
+ probs = logits.softmax(-1).cpu().numpy()
1016
+
1017
+ if align:
1018
+ return probs
1019
+ else:
1020
+ # Return residue-level probabilities (excluding special tokens)
1021
+ return [res_to_list(state, seq) for state, seq in zip(probs, formatted_seqs)]
1022
+
1023
+ def restore(self, seqs, align=False, **kwargs):
1024
+ hf_abrestore = HFAbRestore(self.AbLang, self.tokenizer, spread=self.spread, device=self.used_device, ncpu=self.ncpu)
1025
+ restored = hf_abrestore.restore(seqs, align=align)
1026
+ # Apply angle brackets formatting to match original format
1027
+ if isinstance(restored, np.ndarray):
1028
+ restored = np.array([add_angle_brackets(seq) for seq in restored])
1029
+ else:
1030
+ restored = [add_angle_brackets(seq) for seq in restored]
1031
+ return restored
1032
+
1033
+ def extract_input_ids(tokens, device):
1034
+ if hasattr(tokens, 'input_ids'):
1035
+ return tokens.input_ids.to(device)
1036
+ elif isinstance(tokens, dict):
1037
+ if 'input_ids' in tokens:
1038
+ return tokens['input_ids'].to(device)
1039
+ else:
1040
+ for v in tokens.values():
1041
+ if hasattr(v, 'ndim') or torch.is_tensor(v):
1042
+ return v.to(device)
1043
+ elif torch.is_tensor(tokens):
1044
+ return tokens.to(device)
1045
+ else:
1046
+ raise ValueError("Could not extract input_ids from tokenizer output")
alignment.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import numpy as np
3
+ import torch
4
+
5
+ from extra_utils import paired_msa_numbering, unpaired_msa_numbering, create_alignment
6
+
7
+
8
+ class AbAlignment:
9
+
10
+ def __init__(self, device = 'cpu', ncpu = 1):
11
+
12
+ self.device = device
13
+ self.ncpu = ncpu
14
+
15
+ def number_sequences(self, seqs, chain = 'H', fragmented = False):
16
+ if chain == 'HL':
17
+ numbered_seqs, seqs, number_alignment = paired_msa_numbering(seqs, fragmented = fragmented, n_jobs = self.ncpu)
18
+ else:
19
+ assert chain == 'HL', 'Currently "Align==True" only works for paired sequences. \nPlease use paired sequences or Align=False.'
20
+ numbered_seqs, seqs, number_alignment = unpaired_msa_numbering(
21
+ seqs, chain = chain, fragmented = fragmented, n_jobs = self.ncpu
22
+ )
23
+
24
+ return numbered_seqs, seqs, number_alignment
25
+
26
+ def align_encodings(self, encodings, numbered_seqs, seqs, number_alignment):
27
+
28
+ aligned_list = [
29
+ create_alignment(
30
+ res_embed, numbered_seq, seq, number_alignment
31
+ ) for res_embed, numbered_seq, seq in zip(encodings, numbered_seqs, seqs)
32
+ ]
33
+ aligned_encodings = np.concatenate([aligned_list], axis=0)
34
+ return aligned_encodings
35
+
36
+
37
+ def reformat_subsets(
38
+ self,
39
+ subset_list,
40
+ mode = 'seqcoding',
41
+ align = False,
42
+ numbered_seqs = None,
43
+ seqs = None,
44
+ number_alignment = None,
45
+ ):
46
+
47
+ if mode in [
48
+ 'seqcoding',
49
+ 'restore',
50
+ 'pseudo_log_likelihood',
51
+ 'confidence'
52
+ ]:
53
+ return np.concatenate(subset_list)
54
+ elif align:
55
+ subset_list = [
56
+ self.align_encodings(
57
+ subset,
58
+ numbered_seqs[num*len(subset):(num+1)*len(subset)],
59
+ seqs[num*len(subset):(num+1)*len(subset)],
60
+ number_alignment
61
+ ) for num, subset in enumerate(subset_list)
62
+ ]
63
+
64
+ subset = np.concatenate(subset_list)
65
+
66
+ return aligned_results(
67
+ aligned_seqs = [''.join(alist) for alist in subset[:,:,-1]],
68
+ aligned_embeds = subset[:,:,:-1].astype(float),
69
+ number_alignment=number_alignment.apply(lambda x: '{}{}'.format(*x[0]), axis=1).values
70
+ )
71
+
72
+ elif not align:
73
+ return sum(subset_list, [])
74
+ else:
75
+ return np.concatenate(subset_list) # this needs to be changed
76
+
77
+
78
+ @dataclass
79
+ class aligned_results():
80
+ """
81
+ Dataclass used to store output.
82
+ """
83
+
84
+ aligned_seqs: None
85
+ aligned_embeds: None
86
+ number_alignment: None
app.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import sys
3
+ import os
4
+ from transformers import AutoModel, AutoTokenizer
5
+ from transformers.utils import cached_file
6
+
7
+ # Load model and tokenizer from Hugging Face Hub
8
+ model = AutoModel.from_pretrained("hemantn/ablang2", trust_remote_code=True)
9
+ tokenizer = AutoTokenizer.from_pretrained("hemantn/ablang2", trust_remote_code=True)
10
+
11
+ # Find the cached model directory and import adapter
12
+ adapter_path = cached_file("hemantn/ablang2", "adapter.py")
13
+ cached_model_dir = os.path.dirname(adapter_path)
14
+ sys.path.insert(0, cached_model_dir)
15
+
16
+ # Import and create the adapter
17
+ from adapter import AbLang2PairedHuggingFaceAdapter
18
+ ablang = AbLang2PairedHuggingFaceAdapter(model=model, tokenizer=tokenizer)
19
+
20
+ def restore_sequences(heavy_chain, light_chain, use_align=False):
21
+ """
22
+ Restore masked residues in antibody sequences.
23
+
24
+ Args:
25
+ heavy_chain (str): Heavy chain sequence with masked residues (*)
26
+ light_chain (str): Light chain sequence with masked residues (*)
27
+ use_align (bool): Whether to use alignment for variable missing lengths
28
+
29
+ Returns:
30
+ tuple: (restored_heavy, restored_light, highlighted_heavy, highlighted_light)
31
+ """
32
+ try:
33
+ # Check if alignment is requested but not available
34
+ if use_align:
35
+ try:
36
+ import anarci
37
+ except ImportError:
38
+ return "Alignment feature requires 'anarci' package which is not available. Please disable alignment option.", "", ""
39
+ # Prepare input sequences
40
+ if heavy_chain.strip() and light_chain.strip():
41
+ # Both chains provided
42
+ sequences = [[heavy_chain.strip(), light_chain.strip()]]
43
+ elif heavy_chain.strip():
44
+ # Only heavy chain provided
45
+ sequences = [[heavy_chain.strip(), ""]]
46
+ elif light_chain.strip():
47
+ # Only light chain provided
48
+ sequences = [["", light_chain.strip()]]
49
+ else:
50
+ return "Please provide at least one antibody chain sequence.", "", "", ""
51
+
52
+ # Perform restoration
53
+ restored = ablang(sequences, mode='restore', align=use_align)
54
+
55
+ # Format output
56
+ if hasattr(restored, '__len__') and len(restored) > 0:
57
+ result = restored[0] # Get the first (and only) result
58
+
59
+ # Parse the result to separate heavy and light chains
60
+ if '>|<' in result:
61
+ # Both chains present
62
+ heavy_part = result.split('>|<')[0].replace('<', '').replace('>', '')
63
+ light_part = result.split('>|<')[1].replace('<', '').replace('>', '')
64
+ elif result.startswith('<') and result.endswith('>'):
65
+ # Only one chain present
66
+ if heavy_chain.strip():
67
+ heavy_part = result.replace('<', '').replace('>', '')
68
+ light_part = ""
69
+ else:
70
+ heavy_part = ""
71
+ light_part = result.replace('<', '').replace('>', '')
72
+ else:
73
+ return "Error: Unexpected result format.", "", "", ""
74
+
75
+ # Create highlighted versions
76
+ highlighted_heavy = highlight_restored_residues(heavy_chain.strip(), heavy_part)
77
+ highlighted_light = highlight_restored_residues(light_chain.strip(), light_part)
78
+
79
+ # Create HTML outputs with proper styling - no scroll, wrap text
80
+ heavy_html = f'<div class="restored-sequence-box" style="padding: 10px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">{highlighted_heavy}</div>'
81
+ light_html = f'<div class="restored-sequence-box" style="padding: 10px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">{highlighted_light}</div>'
82
+
83
+ return heavy_html, light_html
84
+ else:
85
+ return "Error: No restoration result obtained.", "", ""
86
+
87
+ except Exception as e:
88
+ return f"Error during restoration: {str(e)}", "", ""
89
+
90
+ def highlight_restored_residues(original_seq, restored_seq):
91
+ """
92
+ Highlight restored residues in green.
93
+ """
94
+ if not original_seq or not restored_seq:
95
+ return restored_seq
96
+
97
+ highlighted = ""
98
+ for i, (orig_char, rest_char) in enumerate(zip(original_seq, restored_seq)):
99
+ if orig_char == '*' and rest_char != '*':
100
+ # This residue was restored
101
+ highlighted += f'<span class="restored-highlight">{rest_char}</span>'
102
+ else:
103
+ highlighted += rest_char
104
+
105
+ # Add any remaining characters from restored sequence
106
+ if len(restored_seq) > len(original_seq):
107
+ highlighted += restored_seq[len(original_seq):]
108
+
109
+ return highlighted
110
+
111
+ # Create Gradio interface
112
+ with gr.Blocks(title="AbLang2 Sequence Restorer", theme=gr.themes.Soft(), css="""
113
+ * {
114
+ font-family: 'Courier New', monospace !important;
115
+ }
116
+ .sequence-input, .sequence-output {
117
+ font-family: 'Courier New', monospace !important;
118
+ font-size: 14px !important;
119
+ letter-spacing: 0.5px !important;
120
+ }
121
+ .restored-highlight {
122
+ background-color: #90EE90 !important;
123
+ color: #000 !important;
124
+ font-weight: bold !important;
125
+ }
126
+ .examples {
127
+ font-family: 'Courier New', monospace !important;
128
+ font-size: 14px !important;
129
+ letter-spacing: 0.5px !important;
130
+ }
131
+ .restored-sequence-box {
132
+ font-family: 'Courier New', monospace !important;
133
+ font-size: 14px !important;
134
+ letter-spacing: 0.5px !important;
135
+ white-space: pre-wrap !important;
136
+ word-wrap: break-word !important;
137
+ overflow-wrap: break-word !important;
138
+ }
139
+ .restored-heading {
140
+ color: #2E8B57 !important;
141
+ font-weight: bold !important;
142
+ font-size: 18px !important;
143
+ }
144
+ .example-text {
145
+ font-family: 'Courier New', monospace !important;
146
+ font-size: 12px !important;
147
+ white-space: pre-wrap !important;
148
+ word-wrap: break-word !important;
149
+ }
150
+ .examples-table {
151
+ font-family: 'Courier New', monospace !important;
152
+ font-size: 12px !important;
153
+ white-space: pre-wrap !important;
154
+ word-wrap: break-word !important;
155
+ max-width: none !important;
156
+ overflow: visible !important;
157
+ }
158
+ .examples-table td {
159
+ font-family: 'Courier New', monospace !important;
160
+ font-size: 12px !important;
161
+ white-space: pre-wrap !important;
162
+ word-wrap: break-word !important;
163
+ max-width: none !important;
164
+ overflow: visible !important;
165
+ text-overflow: unset !important;
166
+ }
167
+ .sequence-output label {
168
+ font-weight: bold !important;
169
+ color: #495057 !important;
170
+ font-size: 14px !important;
171
+ margin-bottom: 5px !important;
172
+ }
173
+ /* Force full display of examples */
174
+ .examples-container {
175
+ font-family: 'Courier New', monospace !important;
176
+ font-size: 12px !important;
177
+ }
178
+ .examples-container table {
179
+ width: 100% !important;
180
+ table-layout: auto !important;
181
+ }
182
+ .examples-container td {
183
+ white-space: pre-wrap !important;
184
+ word-wrap: break-word !important;
185
+ overflow-wrap: break-word !important;
186
+ max-width: none !important;
187
+ text-overflow: unset !important;
188
+ padding: 8px !important;
189
+ vertical-align: top !important;
190
+ }
191
+ .examples-container th {
192
+ white-space: nowrap !important;
193
+ padding: 8px !important;
194
+ }
195
+ /* Override any Gradio default truncation */
196
+ .examples table td {
197
+ white-space: pre-wrap !important;
198
+ word-wrap: break-word !important;
199
+ overflow-wrap: break-word !important;
200
+ max-width: none !important;
201
+ text-overflow: unset !important;
202
+ overflow: visible !important;
203
+ font-family: 'Courier New', monospace !important;
204
+ font-size: 12px !important;
205
+ }
206
+ .examples table {
207
+ table-layout: auto !important;
208
+ width: 100% !important;
209
+ }
210
+ /* Target the specific examples component */
211
+ div[data-testid="examples"] table td {
212
+ white-space: pre-wrap !important;
213
+ word-wrap: break-word !important;
214
+ overflow-wrap: break-word !important;
215
+ max-width: none !important;
216
+ text-overflow: unset !important;
217
+ overflow: visible !important;
218
+ font-family: 'Courier New', monospace !important;
219
+ font-size: 12px !important;
220
+ }
221
+ /* Force examples to show full content */
222
+ .examples table, .examples table td, .examples table th {
223
+ white-space: pre-wrap !important;
224
+ word-wrap: break-word !important;
225
+ overflow-wrap: break-word !important;
226
+ max-width: none !important;
227
+ text-overflow: unset !important;
228
+ overflow: visible !important;
229
+ font-family: 'Courier New', monospace !important;
230
+ font-size: 12px !important;
231
+ table-layout: auto !important;
232
+ width: auto !important;
233
+ min-width: 100% !important;
234
+ }
235
+ /* Override any inline styles */
236
+ .examples * {
237
+ white-space: pre-wrap !important;
238
+ word-wrap: break-word !important;
239
+ overflow-wrap: break-word !important;
240
+ max-width: none !important;
241
+ text-overflow: unset !important;
242
+ overflow: visible !important;
243
+ }
244
+ /* Style output labels to match input labels exactly */
245
+ .output-label {
246
+ font-weight: 600 !important;
247
+ color: var(--label-text-color) !important;
248
+ font-size: 14px !important;
249
+ margin-bottom: 8px !important;
250
+ margin-top: 16px !important;
251
+ line-height: 1.4 !important;
252
+ display: block !important;
253
+ }
254
+ """) as demo:
255
+ gr.Markdown("""
256
+ # 🧬 AbLang2 Sequence Restorer
257
+
258
+ This app uses the AbLang2 model to restore masked residues (*) in antibody sequences.
259
+ You can provide either one or both heavy and light chain sequences.
260
+
261
+ **Instructions:**
262
+ - Use `*` to mask residues you want to restore
263
+ - Provide heavy chain, light chain, or both
264
+ - Enable "Use Alignment" for variable missing lengths
265
+ """)
266
+
267
+ with gr.Row():
268
+ with gr.Column():
269
+ heavy_input = gr.Textbox(
270
+ label="Heavy Chain Sequence",
271
+ placeholder="Enter heavy chain sequence with masked residues (*)...",
272
+ lines=3,
273
+ max_lines=5,
274
+ elem_classes=["sequence-input"]
275
+ )
276
+
277
+ light_input = gr.Textbox(
278
+ label="Light Chain Sequence",
279
+ placeholder="Enter light chain sequence with masked residues (*)...",
280
+ lines=3,
281
+ max_lines=5,
282
+ elem_classes=["sequence-input"]
283
+ )
284
+
285
+ align_checkbox = gr.Checkbox(
286
+ label="Use Alignment (for variable missing lengths) - Requires anarci package",
287
+ value=False
288
+ )
289
+
290
+ restore_btn = gr.Button("🔄 Restore Sequences", variant="primary")
291
+
292
+ with gr.Column():
293
+ gr.Markdown("### 🧬 Restored Sequences", elem_classes=["restored-heading"])
294
+ gr.Markdown("*Green highlighting shows restored residues*")
295
+
296
+ gr.Markdown("**Heavy Chain Sequence**", elem_classes=["output-label"])
297
+ heavy_output = gr.HTML(label="")
298
+
299
+ gr.Markdown("**Light Chain Sequence**", elem_classes=["output-label"])
300
+ light_output = gr.HTML(label="")
301
+
302
+ # Example sequences
303
+ gr.Examples(
304
+ examples=[
305
+ [
306
+ "EVQ***SGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCAR**PGHGAAFMDVWGTGTTVTVSS",
307
+ "DIQLTQSPLSLPVTLGQPASISCRSS*SLEASDTNIYLSWFQQRPGQSPRRLIYKI*NRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK"
308
+ ],
309
+ [
310
+ "EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMGWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCARDY**GMDVWGQGTTVTVSS",
311
+ ""
312
+ ],
313
+ [
314
+ "",
315
+ "DIQLTQSPSSLSASVGDRVTITCRASQSISSYLNWYQQKPGKAPKLLIY*ASSLQSGVPSRFSGSGSGTDFTLTISSLQPEDFATYYCQQSYSTP*TFGQGTKVEIK"
316
+ ]
317
+ ],
318
+ inputs=[heavy_input, light_input],
319
+ label="Example Sequences"
320
+ )
321
+
322
+ # Connect the button to the function
323
+ restore_btn.click(
324
+ fn=restore_sequences,
325
+ inputs=[heavy_input, light_input, align_checkbox],
326
+ outputs=[heavy_output, light_output]
327
+ )
328
+
329
+ gr.Markdown("""
330
+ ---
331
+ **Note:** This app uses the AbLang2 model from Hugging Face Hub.
332
+ The restoration process may take a few seconds depending on sequence length and complexity.
333
+ """)
334
+
335
+ if __name__ == "__main__":
336
+ demo.launch()
config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ablang2-paired",
3
+ "vocab_size": 26,
4
+ "hidden_embed_size": 480,
5
+ "n_attn_heads": 20,
6
+ "n_encoder_blocks": 12,
7
+ "padding_tkn": 21,
8
+ "mask_tkn": 23,
9
+ "layer_norm_eps": 1e-12,
10
+ "a_fn": "swiglu",
11
+ "dropout": 0.0,
12
+ "tokenizer_class": "AbLang2PairedTokenizer",
13
+ "auto_map": {
14
+ "AutoConfig": "configuration_ablang2paired.AbLang2PairedConfig",
15
+ "AutoModel": "modeling_ablang2paired.AbLang2PairedHFModel",
16
+ "AutoTokenizer": ["tokenizer_ablang2paired.AbLang2PairedTokenizer", "tokenizer_ablang2paired.AbLang2PairedTokenizer"],
17
+ "AbRestore": "restoration.AbRestore",
18
+ "AbScores": "scores.AbScores",
19
+ "AbAlignment": "alignment.AbAlignment",
20
+ "AbEncoding": "ablang_encodings.AbEncoding",
21
+ "ExtraUtils": "extra_utils"
22
+ }
23
+ }
configuration_ablang2paired.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+ class AbLang2PairedConfig(PretrainedConfig):
4
+ model_type = "ablang2-paired"
5
+
6
+ def __init__(
7
+ self,
8
+ vocab_size=26,
9
+ hidden_embed_size=480,
10
+ n_attn_heads=20,
11
+ n_encoder_blocks=12,
12
+ padding_tkn=21,
13
+ mask_tkn=23,
14
+ layer_norm_eps=1e-12,
15
+ a_fn="swiglu",
16
+ dropout=0.0,
17
+ **kwargs
18
+ ):
19
+ super().__init__(**kwargs)
20
+ self.vocab_size = vocab_size
21
+ self.hidden_embed_size = hidden_embed_size
22
+ self.hidden_size = hidden_embed_size # Add this for Hugging Face compatibility
23
+ self.n_attn_heads = n_attn_heads
24
+ self.num_attention_heads = n_attn_heads # Add this for Hugging Face compatibility
25
+ self.num_hidden_layers = n_encoder_blocks # Add this for Hugging Face compatibility
26
+ self.n_encoder_blocks = n_encoder_blocks
27
+ self.padding_tkn = padding_tkn
28
+ self.mask_tkn = mask_tkn
29
+ self.layer_norm_eps = layer_norm_eps
30
+ self.a_fn = a_fn
31
+ self.dropout = dropout
encoderblock.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import math
3
+ from torch import nn
4
+ import torch.nn.functional as F
5
+ import einops
6
+ from rotary_embedding_torch import RotaryEmbedding
7
+
8
+ class TransformerEncoder(torch.nn.Module):
9
+ """
10
+ Single Transformer Encoder.
11
+
12
+ """
13
+ def __init__(
14
+ self,
15
+ hidden_embed_size,
16
+ n_attn_heads,
17
+ attn_dropout: float = 0.0,
18
+ layer_norm_eps: float = 1e-05,
19
+ a_fn: str = "gelu",
20
+ ):
21
+ super().__init__()
22
+
23
+ assert hidden_embed_size % n_attn_heads == 0, \
24
+ "Embedding dimension must be devisible with the number of heads."
25
+
26
+ self.multihead_attention = MultiHeadAttention(
27
+ embed_dim = hidden_embed_size,
28
+ num_heads = n_attn_heads,
29
+ attention_dropout_prob = attn_dropout
30
+ )
31
+
32
+ activation_fn, scale = get_activation_fn(a_fn)
33
+
34
+ self.intermediate_layer = torch.nn.Sequential(
35
+ torch.nn.Linear(hidden_embed_size, hidden_embed_size * 4 * scale),
36
+ activation_fn(),
37
+ torch.nn.Linear(hidden_embed_size * 4, hidden_embed_size),
38
+ )
39
+
40
+ self.pre_attn_layer_norm = torch.nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
41
+ self.final_layer_norm = torch.nn.LayerNorm(hidden_embed_size, eps=layer_norm_eps)
42
+
43
+ def forward(self, hidden_embed, attn_mask=None, return_attn_weights: bool = False):
44
+
45
+ residual = hidden_embed
46
+ hidden_embed = self.pre_attn_layer_norm(hidden_embed.clone())
47
+ hidden_embed, attn_weights = self.multihead_attention(
48
+ hidden_embed,
49
+ attn_mask=attn_mask,
50
+ return_attn_weights=return_attn_weights
51
+ )
52
+ hidden_embed = residual + hidden_embed
53
+
54
+ residual = hidden_embed
55
+ hidden_embed = self.final_layer_norm(hidden_embed)
56
+ hidden_embed = self.intermediate_layer(hidden_embed)
57
+ hidden_embed = residual + hidden_embed
58
+ return hidden_embed, attn_weights
59
+
60
+ class MultiHeadAttention(torch.nn.Module):
61
+
62
+ def __init__(
63
+ self,
64
+ embed_dim,
65
+ num_heads,
66
+ attention_dropout_prob: float = 0.0,
67
+ bias: bool = True,
68
+ ):
69
+ super().__init__()
70
+
71
+ self.attention_dropout = torch.nn.Dropout(attention_dropout_prob)
72
+
73
+ self.embed_dim = embed_dim
74
+ self.num_heads = num_heads
75
+ self.head_dim = embed_dim // num_heads
76
+ assert (self.head_dim * num_heads == self.embed_dim), "embed_dim must be divisible by num_heads"
77
+ self.scaling = self.head_dim**-0.5
78
+
79
+ self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
80
+ self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
81
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
82
+
83
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
84
+
85
+ self.reset_parameters()
86
+
87
+ self.rotary_emb = RotaryEmbedding(dim = self.head_dim)
88
+
89
+ def reset_parameters(self):
90
+
91
+ nn.init.xavier_uniform_(self.k_proj.weight, gain=1 / math.sqrt(2))
92
+ nn.init.xavier_uniform_(self.v_proj.weight, gain=1 / math.sqrt(2))
93
+ nn.init.xavier_uniform_(self.q_proj.weight, gain=1 / math.sqrt(2))
94
+
95
+ nn.init.xavier_uniform_(self.out_proj.weight)
96
+ if self.out_proj.bias is not None:
97
+ nn.init.constant_(self.out_proj.bias, 0.0)
98
+
99
+ def attention(self, q, k, v, attn_mask=None):
100
+
101
+ attn_weights = torch.matmul(q, k.transpose(-2, -1))
102
+ attn_weights = attn_weights / math.sqrt(self.head_dim)
103
+
104
+ if attn_mask is not None:
105
+ attn_mask = einops.rearrange(
106
+ attn_mask,
107
+ 'b_size (h1 h2 seq_len) -> b_size h1 h2 seq_len',
108
+ h1=1, h2=1
109
+ )
110
+ attn_weights = attn_weights.masked_fill(attn_mask, float("-inf"))
111
+
112
+ attn_weights = F.softmax(attn_weights, dim=-1)
113
+
114
+ attn = self.attention_dropout(attn_weights)
115
+ attn = torch.matmul(attn, v)
116
+ return attn, attn_weights
117
+
118
+ def forward(self, x, attn_mask=None, return_attn_weights: bool = False):
119
+
120
+ batch_size, seq_len, embed_dim = x.size()
121
+
122
+ q, k, v = self.q_proj(x), self.k_proj(x), self.v_proj(x)
123
+ q *= self.scaling
124
+
125
+ q = q.contiguous().view(
126
+ batch_size,
127
+ seq_len,
128
+ self.num_heads,
129
+ self.head_dim
130
+ ).transpose(1, 2) # [n_batch, n_heads, seq_len, head_dim]
131
+ k = k.contiguous().view(
132
+ batch_size,
133
+ seq_len,
134
+ self.num_heads,
135
+ self.head_dim
136
+ ).transpose(1, 2) # [n_batch, n_heads, seq_len, head_dim]
137
+ v = v.contiguous().view(
138
+ batch_size,
139
+ seq_len,
140
+ self.num_heads,
141
+ self.head_dim
142
+ ).transpose(1, 2) # [n_batch, n_heads, seq_len, head_dim]
143
+
144
+ q = self.rotary_emb.rotate_queries_or_keys(q)
145
+ k = self.rotary_emb.rotate_queries_or_keys(k)
146
+
147
+ # Determine value outputs
148
+ attn, attn_weights = self.attention(
149
+ q, k, v,
150
+ attn_mask=attn_mask
151
+ ) # attn_weights [n_batch, n_heads, seq_len (target), seq_len (source)]
152
+
153
+ attn = attn.transpose(1, 2).reshape(batch_size, seq_len, embed_dim)
154
+ attn = self.out_proj(attn)
155
+
156
+ if return_attn_weights:
157
+ return attn, attn_weights
158
+ else:
159
+ return attn, None
160
+
161
+ class SwiGLU(torch.nn.Module):
162
+ def forward(self, x):
163
+ x, gate = x.chunk(2, dim=-1)
164
+ return F.silu(gate) * x
165
+
166
+ def get_activation_fn(a_fn):
167
+
168
+ if a_fn == "gelu":
169
+ return torch.nn.GELU, 1
170
+
171
+ elif a_fn == "swiglu":
172
+ return SwiGLU, 2
173
+
environment.yaml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: AbLang
2
+ channels:
3
+ - conda-forge
4
+ - pytorch
5
+ - bioconda
6
+ - defaults
7
+ dependencies:
8
+ - python=3.10.18
9
+ - pip
10
+ - pytorch=2.5.1
11
+ - pytorch-cuda=12.4
12
+ - numpy=2.2.6
13
+ - pandas=2.3.1
14
+ - transformers=4.53.3
15
+ - anarci=2024.05.21
16
+ - jupyter=7.4.4
17
+ - notebook=7.4.4
18
+ - ipython=8.37.0
19
+ - ipykernel=6.29.5
20
+ - matplotlib-inline=0.1.7
21
+ - scikit-learn
22
+ - matplotlib
23
+ - seaborn
24
+ - biopython=1.85
25
+ - huggingface_hub=0.33.4
26
+ - tokenizers=0.21.3
27
+ - safetensors=0.5.3
28
+ - einops=0.8.1
29
+ - tqdm=4.67.1
30
+ - requests=2.32.4
31
+ - urllib3=2.5.0
32
+ - certifi=2025.7.14
33
+ - filelock=3.18.0
34
+ - fsspec=2025.3.0
35
+ - packaging=25.0
36
+ - regex=2024.11.6
37
+ - sympy=1.13.3
38
+ - networkx=3.4.2
39
+ - jinja2=3.1.6
40
+ - pyyaml=6.0.2
41
+ - typing_extensions=4.14.1
42
+ - pip:
43
+ - numba=0.61.2
44
+ - llvmlite=0.44.0
extra_utils.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import string, re
2
+ import numpy as np
3
+
4
+
5
+ def res_to_list(logits, seq):
6
+ return logits[:len(seq)]
7
+
8
+ def res_to_seq(a, mode='mean'):
9
+ """
10
+ Function for how we go from n_values for each amino acid to n_values for each sequence.
11
+
12
+ We leave out padding tokens.
13
+ """
14
+
15
+ if mode=='sum':
16
+ return a[0:(int(a[-1]))].sum()
17
+
18
+ elif mode=='mean':
19
+ return a[0:(int(a[-1]))].mean()
20
+
21
+ elif mode=='restore':
22
+ return a[0][0:(int(a[-1]))]
23
+
24
+ def get_number_alignment(numbered_seqs):
25
+ """
26
+ Creates a number alignment from the anarci results.
27
+ """
28
+ import pandas as pd
29
+
30
+ alist = [pd.DataFrame(aligned_seq, columns = [0,1,'resi']) for aligned_seq in numbered_seqs]
31
+ unsorted_alignment = pd.concat(alist).drop_duplicates(subset=0)
32
+ max_alignment = get_max_alignment()
33
+
34
+ return max_alignment.merge(unsorted_alignment.query("resi!='-'"), left_on=0, right_on=0)[[0,1]]
35
+
36
+ def get_max_alignment():
37
+ """
38
+ Create maximum possible alignment for sorting
39
+ """
40
+ import pandas as pd
41
+
42
+ sortlist = [[("<", "")]]
43
+ for num in range(1, 128+1):
44
+ if num in [33,61,112]:
45
+ for char in string.ascii_uppercase[::-1]:
46
+ sortlist.append([(num, char)])
47
+
48
+ sortlist.append([(num,' ')])
49
+ else:
50
+ sortlist.append([(num,' ')])
51
+ for char in string.ascii_uppercase:
52
+ sortlist.append([(num, char)])
53
+
54
+ return pd.DataFrame(sortlist + [[(">", "")]])
55
+
56
+
57
+ def paired_msa_numbering(ab_seqs, fragmented = False, n_jobs = 10):
58
+
59
+ import pandas as pd
60
+
61
+ tmp_seqs = [pairs.replace(">", "").replace("<", "").split("|") for pairs in ab_seqs]
62
+
63
+ numbered_seqs_heavy, seqs_heavy, number_alignment_heavy = unpaired_msa_numbering(
64
+ [i[0] for i in tmp_seqs], 'H', fragmented = fragmented, n_jobs = n_jobs
65
+ )
66
+ numbered_seqs_light, seqs_light, number_alignment_light = unpaired_msa_numbering(
67
+ [i[1] for i in tmp_seqs], 'L', fragmented = fragmented, n_jobs = n_jobs
68
+ )
69
+
70
+ number_alignment = pd.concat([
71
+ number_alignment_heavy,
72
+ pd.DataFrame([[("|",""), "|"]]),
73
+ number_alignment_light]
74
+ ).reset_index(drop=True)
75
+
76
+ seqs = [f"{heavy}|{light}" for heavy, light in zip(seqs_heavy, seqs_light)]
77
+ numbered_seqs = [
78
+ heavy + [(("|",""), "|", "|")] + light for heavy, light in zip(numbered_seqs_heavy, numbered_seqs_light)
79
+ ]
80
+
81
+ return numbered_seqs, seqs, number_alignment
82
+
83
+
84
+ def unpaired_msa_numbering(seqs, chain = 'H', fragmented = False, n_jobs = 10):
85
+
86
+ numbered_seqs = number_with_anarci(seqs, chain = chain, fragmented = fragmented, n_jobs = n_jobs)
87
+ number_alignment = get_number_alignment(numbered_seqs)
88
+ number_alignment[1] = chain
89
+
90
+ seqs = [''.join([i[2] for i in numbered_seq]).replace('-','') for numbered_seq in numbered_seqs]
91
+ return numbered_seqs, seqs, number_alignment
92
+
93
+
94
+ def number_with_anarci(seqs, chain = 'H', fragmented = False, n_jobs = 1):
95
+
96
+ import anarci
97
+ import pandas as pd
98
+
99
+ anarci_out = anarci.run_anarci(
100
+ pd.DataFrame(seqs).reset_index().values.tolist(),
101
+ ncpu=n_jobs,
102
+ scheme='imgt',
103
+ allowed_species=['human', 'mouse'],
104
+ )
105
+
106
+ numbered_seqs = []
107
+ for onarci in anarci_out[1]:
108
+ numbered_seq = []
109
+ for i in onarci[0][0]:
110
+ if i[1] != '-':
111
+ numbered_seq.append((i[0], chain, i[1]))
112
+
113
+ if fragmented:
114
+ numbered_seqs.append(numbered_seq)
115
+ else:
116
+ numbered_seqs.append([(("<",""), chain, "<")] + numbered_seq + [((">",""), chain, ">")])
117
+
118
+ return numbered_seqs
119
+
120
+
121
+ def create_alignment(res_embeds, numbered_seqs, seq, number_alignment):
122
+
123
+ import pandas as pd
124
+
125
+ datadf = pd.DataFrame(numbered_seqs)
126
+ sequence_alignment = number_alignment.merge(datadf, how='left', on=[0, 1]).fillna('-')[2]
127
+
128
+ idxs = np.where(sequence_alignment.values == '-')[0]
129
+ idxs = [idx-num for num, idx in enumerate(idxs)]
130
+
131
+ aligned_embeds = pd.DataFrame(np.insert(res_embeds[:len(seq)], idxs , 0, axis=0))
132
+
133
+ return pd.concat([aligned_embeds, sequence_alignment], axis=1).values
134
+
135
+
136
+ def get_spread_sequences(seq, spread, start_position):
137
+ """
138
+ Test sequences which are 8 positions shorter (position 10 + max CDR1 gap of 7) up to 2 positions longer (possible insertions).
139
+ """
140
+ spread_sequences = []
141
+
142
+ for diff in range(start_position-8, start_position+2+1):
143
+ spread_sequences.append('*'*diff+seq)
144
+
145
+ return np.array(spread_sequences)
146
+
147
+ def get_sequences_from_anarci(out_anarci, max_position, spread):
148
+ """
149
+ Ensures correct masking on each side of sequence
150
+ """
151
+
152
+ if out_anarci == 'ANARCI_error':
153
+ return np.array(['ANARCI-ERR']*spread)
154
+
155
+ end_position = int(re.search(r'\d+', out_anarci[::-1]).group()[::-1])
156
+ # Fixes ANARCI error of poor numbering of the CDR1 region
157
+ start_position = int(re.search(r'\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+\'\),\s\(\(\d+,\s\'.\'\),\s\'[^-]+',
158
+ out_anarci).group().split(',')[0]) - 1
159
+
160
+ sequence = "".join(re.findall(r"(?i)[A-Z*]", "".join(re.findall(r'\),\s\'[A-Z*]', out_anarci))))
161
+
162
+ sequence_j = ''.join(sequence).replace('-','').replace('X','*') + '*'*(max_position-int(end_position))
163
+
164
+ return get_spread_sequences(sequence_j, spread, start_position)
165
+
hparams.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name": "AbLang-2", "n_encoder_blocks": 12, "hidden_embed_size": 480, "n_attn_heads": 20, "a_fn": "swiglu", "layer_norm_eps": 1e-12, "pad_tkn": 21, "start_tkn": 0, "end_tkn": 22, "sep_tkn": 25, "mask_tkn": 23, "vocab_size": 26}
model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:56d6f07862a6f824f88c8707bbc03e4026c9db762be2d3041e9767e2e6f86386
3
+ size 179314477
modeling_ablang2paired.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import os
3
+ from torch import nn
4
+ from transformers import PreTrainedModel
5
+
6
+ # Import configuration
7
+ try:
8
+ from .configuration_ablang2paired import AbLang2PairedConfig
9
+ except ImportError:
10
+ from configuration_ablang2paired import AbLang2PairedConfig
11
+
12
+ # Import the AbLang model from local files
13
+ try:
14
+ from ablang import AbLang
15
+ except ImportError:
16
+ # Fallback: try to import from the current directory
17
+ try:
18
+ from .ablang import AbLang
19
+ except ImportError:
20
+ raise ImportError(
21
+ "Could not find AbLang module. Please ensure ablang.py is present in the repository."
22
+ )
23
+
24
+
25
+
26
+
27
+
28
+ class AbLang2PairedHFModel(PreTrainedModel):
29
+ config_class = AbLang2PairedConfig
30
+ model_type = "ablang2-paired"
31
+
32
+ def __init__(self, config: AbLang2PairedConfig):
33
+ super().__init__(config)
34
+ self.model = AbLang(
35
+ vocab_size=config.vocab_size,
36
+ hidden_embed_size=config.hidden_embed_size,
37
+ n_attn_heads=config.n_attn_heads,
38
+ n_encoder_blocks=config.n_encoder_blocks,
39
+ padding_tkn=config.padding_tkn,
40
+ mask_tkn=config.mask_tkn,
41
+ layer_norm_eps=config.layer_norm_eps,
42
+ a_fn=config.a_fn,
43
+ dropout=config.dropout,
44
+ )
45
+
46
+ def forward(self, input_ids=None, x=None, attention_mask=None, **kwargs):
47
+ # Handle both Hugging Face format (input_ids) and original format (x)
48
+ if input_ids is not None:
49
+ x = input_ids
50
+ elif x is None:
51
+ raise ValueError("Either input_ids or x must be provided")
52
+
53
+ # Get the output from the underlying model
54
+ output = self.model(x, attention_mask)
55
+
56
+ # Return as a simple object with last_hidden_state attribute
57
+ class ModelOutput:
58
+ def __init__(self, last_hidden_state):
59
+ self.last_hidden_state = last_hidden_state
60
+
61
+ return ModelOutput(output)
62
+
63
+ @classmethod
64
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
65
+ # Load config first
66
+ config = kwargs.get("config")
67
+ if config is None:
68
+ from transformers import AutoConfig
69
+ config = AutoConfig.from_pretrained(pretrained_model_name_or_path, trust_remote_code=True)
70
+
71
+ # Create model with config
72
+ model = cls(config)
73
+
74
+ # Try to load custom weights
75
+ try:
76
+ from transformers.utils import cached_file
77
+ custom_weights_path = cached_file(
78
+ pretrained_model_name_or_path,
79
+ "model.pt",
80
+ cache_dir=kwargs.get("cache_dir"),
81
+ force_download=kwargs.get("force_download", False),
82
+ resume_download=kwargs.get("resume_download", False),
83
+ proxies=kwargs.get("proxies"),
84
+ token=kwargs.get("token"),
85
+ revision=kwargs.get("revision"),
86
+ local_files_only=kwargs.get("local_files_only", False),
87
+ )
88
+
89
+ if custom_weights_path is not None and os.path.exists(custom_weights_path):
90
+ # Load custom weights
91
+ state_dict = torch.load(custom_weights_path, map_location="cpu", weights_only=True)
92
+ model.model.load_state_dict(state_dict)
93
+ print(f"✅ Loaded custom weights from: {custom_weights_path}")
94
+ else:
95
+ print("⚠️ No custom weights found, using initialized model")
96
+
97
+ except Exception as e:
98
+ print(f"⚠️ Could not load custom weights: {e}")
99
+ print("Using initialized model")
100
+
101
+ # Move model to appropriate device (GPU if available, otherwise CPU)
102
+ device = kwargs.get("device", None)
103
+ if device is None:
104
+ device = "cuda" if torch.cuda.is_available() else "cpu"
105
+ model = model.to(device)
106
+
107
+ return model
108
+
109
+ def save_pretrained(self, save_directory, **kwargs):
110
+ os.makedirs(save_directory, exist_ok=True)
111
+ # Save custom weights
112
+ torch.save(self.model.state_dict(), f"{save_directory}/model.pt")
113
+ # Save config
114
+ self.config.save_pretrained(save_directory)
115
+ # Call parent method for any additional saving
116
+ super().save_pretrained(save_directory, **kwargs)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ transformers>=4.30.0
3
+ numpy>=1.21.0
4
+ pandas>=1.3.0
5
+ rotary-embedding-torch>=0.2.0
6
+ git+https://github.com/oxpig/ANARCI.git
restoration.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ from extra_utils import res_to_seq, get_sequences_from_anarci
5
+
6
+
7
+ class AbRestore:
8
+ def __init__(self, spread = 11, device = 'cpu', ncpu = 1):
9
+ self.spread = spread
10
+ self.device = device
11
+ self.ncpu = ncpu
12
+
13
+ def _initiate_abrestore(self, model, tokenizer):
14
+ self.AbLang = model
15
+ self.tokenizer = tokenizer
16
+
17
+ def restore(self, seqs, align = False, **kwargs):
18
+ """
19
+ Restore sequences
20
+ """
21
+ n_seqs = len(seqs)
22
+
23
+ if align:
24
+
25
+ seqs = self._sequence_aligning(seqs)
26
+ nr_seqs = len(seqs)//self.spread
27
+
28
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
29
+ predictions = self.AbLang(tokens)[:,:,1:21]
30
+
31
+ # Reshape
32
+ tokens = tokens.reshape(nr_seqs, self.spread, -1)
33
+ predictions = predictions.reshape(nr_seqs, self.spread, -1, 20)
34
+ seqs = seqs.reshape(nr_seqs, -1)
35
+
36
+ # Find index of best predictions
37
+ best_seq_idx = torch.argmax(torch.max(predictions, -1).values[:,:,1:2].mean(2), -1)
38
+
39
+ # Select best predictions
40
+ tokens = tokens.gather(1, best_seq_idx.view(-1, 1).unsqueeze(1).repeat(1, 1, tokens.shape[-1])).squeeze(1)
41
+ predictions = predictions[range(predictions.shape[0]), best_seq_idx]
42
+ seqs = np.take_along_axis(seqs, best_seq_idx.view(-1, 1).cpu().numpy(), axis=1)
43
+
44
+ else:
45
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
46
+ predictions = self.AbLang(tokens)[:,:,1:21]
47
+
48
+ predicted_tokens = torch.max(predictions, -1).indices + 1
49
+ restored_tokens = torch.where(tokens==23, predicted_tokens, tokens)
50
+
51
+ restored_seqs = self.tokenizer(restored_tokens, mode="decode")
52
+
53
+ if n_seqs < len(restored_seqs):
54
+ restored_seqs = [f"{h}|{l}".replace('-','') for h,l in zip(restored_seqs[:n_seqs], restored_seqs[n_seqs:])]
55
+ seqs = [f"{h}|{l}" for h,l in zip(seqs[:n_seqs], seqs[n_seqs:])]
56
+
57
+ return np.array([res_to_seq(seq, 'restore') for seq in np.c_[restored_seqs, np.vectorize(len)(seqs)]])
58
+
59
+ def _create_spread_of_sequences(self, seqs, chain = 'H'):
60
+ import pandas as pd
61
+ import anarci
62
+
63
+ chain_idx = 0 if chain == 'H' else 1
64
+ numbered_seqs = anarci.run_anarci(
65
+ pd.DataFrame([seq[chain_idx].replace('*', 'X') for seq in seqs]).reset_index().values.tolist(),
66
+ ncpu=self.ncpu,
67
+ scheme='imgt',
68
+ allowed_species=['human', 'mouse'],
69
+ )
70
+
71
+ anarci_data = pd.DataFrame(
72
+ [str(anarci[0][0]) if anarci else 'ANARCI_error' for anarci in numbered_seqs[1]],
73
+ columns=['anarci']
74
+ ).astype('<U90')
75
+
76
+ max_position = 128 if chain == 'H' else 127
77
+
78
+ seqs = anarci_data.apply(
79
+ lambda x: get_sequences_from_anarci(
80
+ x.anarci,
81
+ max_position,
82
+ self.spread
83
+ ), axis=1, result_type='expand'
84
+ ).to_numpy().reshape(-1)
85
+
86
+ return seqs
87
+
88
+
89
+ def _sequence_aligning(self, seqs):
90
+
91
+ tmp_seqs = [pairs.replace(">", "").replace("<", "").split("|") for pairs in seqs]
92
+
93
+ spread_heavy = [f"<{seq}>" for seq in self._create_spread_of_sequences(tmp_seqs, chain = 'H')]
94
+ spread_light = [f"<{seq}>" for seq in self._create_spread_of_sequences(tmp_seqs, chain = 'L')]
95
+
96
+ return np.concatenate([np.array(spread_heavy),np.array(spread_light)])
scores.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ from extra_utils import res_to_list, res_to_seq
5
+
6
+
7
+ class AbScores:
8
+
9
+ def __init__(self, device = 'cpu', ncpu = 1):
10
+
11
+ self.device = device
12
+ self.ncpu = ncpu
13
+
14
+ def _initiate_abencoding(self, model, tokenizer):
15
+ self.AbLang = model
16
+ self.tokenizer = tokenizer
17
+
18
+ def _encode_sequences(self, seqs):
19
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
20
+ with torch.no_grad():
21
+ return self.AbLang.AbRep(tokens).last_hidden_states.numpy()
22
+
23
+ def _predict_logits(self, seqs):
24
+ tokens = self.tokenizer(seqs, pad=True, w_extra_tkns=False, device=self.used_device)
25
+ with torch.no_grad():
26
+ return self.AbLang(tokens), tokens
27
+
28
+ def pseudo_log_likelihood(self, seqs, **kwargs):
29
+ """
30
+ Pseudo log likelihood of sequences.
31
+ """
32
+
33
+ plls = []
34
+ for seq in seqs:
35
+
36
+ labels = self.tokenizer(
37
+ seq, pad=True, w_extra_tkns=False, device=self.used_device
38
+ )
39
+
40
+ idxs = (
41
+ ~torch.isin(labels, torch.Tensor(self.tokenizer.all_special_tokens).to(self.used_device))
42
+ ).nonzero()
43
+
44
+ masked_tokens = labels.repeat(len(idxs), 1)
45
+ for num, idx in enumerate(idxs):
46
+ masked_tokens[num, idx[1]] = self.tokenizer.mask_token
47
+
48
+ with torch.no_grad():
49
+ logits = self.AbLang(masked_tokens)
50
+
51
+ logits[:, :, self.tokenizer.all_special_tokens] = -float("inf")
52
+ logits = torch.stack([logits[num, idx[1]] for num, idx in enumerate(idxs)])
53
+
54
+ labels = labels[:,idxs[:,1:]].squeeze(2)[0]
55
+
56
+ nll = torch.nn.functional.cross_entropy(
57
+ logits,
58
+ labels,
59
+ reduction="mean",
60
+ )
61
+
62
+ pll = -nll
63
+
64
+ plls.append(pll)
65
+
66
+ plls = torch.stack(plls, dim=0).cpu().numpy()
67
+
68
+ return plls
69
+
70
+ def confidence(self, seqs, **kwargs):
71
+ """
72
+ Log likelihood of sequences without masking.
73
+ """
74
+
75
+ labels = self.tokenizer(
76
+ seqs, pad=True, w_extra_tkns=False, device=self.used_device
77
+ )
78
+ with torch.no_grad():
79
+ logits = self.AbLang(labels)
80
+ logits[:, :, self.tokenizer.all_special_tokens] = -float("inf")
81
+
82
+ plls = []
83
+ for label, logit in zip(labels, logits):
84
+
85
+ idxs = (
86
+ ~torch.isin(label, torch.Tensor(self.tokenizer.all_special_tokens).to(self.used_device))
87
+ ).nonzero().squeeze(1)
88
+
89
+ nll = torch.nn.functional.cross_entropy(
90
+ logit[idxs],
91
+ label[idxs],
92
+ reduction="mean",
93
+ )
94
+
95
+ pll = -nll
96
+ plls.append(pll)
97
+
98
+ return torch.stack(plls, dim=0).cpu().numpy()
test_ablang2_HF_implementation.ipynb ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "458aed0f",
6
+ "metadata": {},
7
+ "source": [
8
+ "<small>Note: This notebook is adapted from the [AbLang2](https://github.com/TobiasHeOl/AbLang2) model's GitHub repository. It is used to verify that the Hugging Face implementation functions correctly and produces the same output as the original model."
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": 1,
14
+ "id": "a51e7ed2",
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "!rm -rf ~/.cache/huggingface/hub/models--hemantn--ablang2"
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "code",
23
+ "execution_count": 2,
24
+ "id": "7ae54cd0-6253-46dd-a316-4f20b12041e0",
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "import sys\n",
29
+ "import os\n",
30
+ "import numpy as np\n",
31
+ "from transformers import AutoModel, AutoTokenizer\n",
32
+ "from huggingface_hub import hf_hub_download"
33
+ ]
34
+ },
35
+ {
36
+ "cell_type": "markdown",
37
+ "id": "10801511-770d-46ac-a15d-a02d4ef9ec87",
38
+ "metadata": {},
39
+ "source": [
40
+ "# **0. Sequence input and its format**\n",
41
+ "\n",
42
+ "AbLang2 takes as input either the individual heavy variable domain (VH), light variable domain (VL), or the full variable domain (Fv).\n",
43
+ "\n",
44
+ "Each record (antibody) needs to be a list with the VH as the first element and the VL as the second. If either the VH or VL is not known, leave an empty string.\n",
45
+ "\n",
46
+ "An asterisk (\\*) is used for masking. It is recommended to mask residues which you are interested in mutating.\n",
47
+ "\n",
48
+ "**NB:** It is important that the VH and VL sequence is ordered correctly."
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": 3,
54
+ "id": "99192978-a008-4a32-a80e-bba238e0ec7c",
55
+ "metadata": {},
56
+ "outputs": [],
57
+ "source": [
58
+ "seq1 = [\n",
59
+ " 'EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS', # VH sequence\n",
60
+ " 'DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK' # VL sequence\n",
61
+ "]\n",
62
+ "seq2 = [\n",
63
+ " 'EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTT',\n",
64
+ " 'PVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK'\n",
65
+ "]\n",
66
+ "seq3 = [\n",
67
+ " 'EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS',\n",
68
+ " '' # The VL sequence is not known, so an empty string is left instead. \n",
69
+ "]\n",
70
+ "seq4 = [\n",
71
+ " '',\n",
72
+ " 'DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK'\n",
73
+ "]\n",
74
+ "seq5 = [\n",
75
+ " 'EVQ***SGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCAR**PGHGAAFMDVWGTGTTVTVSS', # (*) is used to mask certain residues\n",
76
+ " 'DIQLTQSPLSLPVTLGQPASISCRSS*SLEASDTNIYLSWFQQRPGQSPRRLIYKI*NRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK'\n",
77
+ "]\n",
78
+ "\n",
79
+ "all_seqs = [seq1, seq2, seq3, seq4, seq5]\n",
80
+ "only_both_chains_seqs = [seq1, seq2, seq5]"
81
+ ]
82
+ },
83
+ {
84
+ "cell_type": "markdown",
85
+ "id": "dffbacfa-8642-4d94-9572-2205a05c18f9",
86
+ "metadata": {},
87
+ "source": [
88
+ "# **1. How to use AbLang2**\n",
89
+ "\n",
90
+ "AbLang2 can be downloaded and used in its raw form as seen below. For convenience, we have also developed different \"modes\" which can be used for specific use cases (see Section 2) "
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "execution_count": 4,
96
+ "id": "6d66ad84",
97
+ "metadata": {},
98
+ "outputs": [
99
+ {
100
+ "data": {
101
+ "application/vnd.jupyter.widget-view+json": {
102
+ "model_id": "ed2d5574bd21463c9244070ab762c31e",
103
+ "version_major": 2,
104
+ "version_minor": 0
105
+ },
106
+ "text/plain": [
107
+ "config.json: 0%| | 0.00/763 [00:00<?, ?B/s]"
108
+ ]
109
+ },
110
+ "metadata": {},
111
+ "output_type": "display_data"
112
+ },
113
+ {
114
+ "data": {
115
+ "application/vnd.jupyter.widget-view+json": {
116
+ "model_id": "10e1a02037f74d2da6e0860ef914829b",
117
+ "version_major": 2,
118
+ "version_minor": 0
119
+ },
120
+ "text/plain": [
121
+ "configuration_ablang2paired.py: 0.00B [00:00, ?B/s]"
122
+ ]
123
+ },
124
+ "metadata": {},
125
+ "output_type": "display_data"
126
+ },
127
+ {
128
+ "name": "stderr",
129
+ "output_type": "stream",
130
+ "text": [
131
+ "A new version of the following files was downloaded from https://huggingface.co/hemantn/ablang2:\n",
132
+ "- configuration_ablang2paired.py\n",
133
+ ". Make sure to double-check they do not contain any added malicious code. To avoid downloading new versions of the code file, you can pin a revision.\n"
134
+ ]
135
+ },
136
+ {
137
+ "data": {
138
+ "application/vnd.jupyter.widget-view+json": {
139
+ "model_id": "eaf036440107433f950cf4b8c652d756",
140
+ "version_major": 2,
141
+ "version_minor": 0
142
+ },
143
+ "text/plain": [
144
+ "modeling_ablang2paired.py: 0.00B [00:00, ?B/s]"
145
+ ]
146
+ },
147
+ "metadata": {},
148
+ "output_type": "display_data"
149
+ },
150
+ {
151
+ "name": "stderr",
152
+ "output_type": "stream",
153
+ "text": [
154
+ "A new version of the following files was downloaded from https://huggingface.co/hemantn/ablang2:\n",
155
+ "- modeling_ablang2paired.py\n",
156
+ ". Make sure to double-check they do not contain any added malicious code. To avoid downloading new versions of the code file, you can pin a revision.\n",
157
+ "/home/hn533621/.conda/envs/lib_transformer/lib/python3.10/site-packages/huggingface_hub/file_download.py:943: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
158
+ " warnings.warn(\n"
159
+ ]
160
+ },
161
+ {
162
+ "data": {
163
+ "application/vnd.jupyter.widget-view+json": {
164
+ "model_id": "22b9a58a3100420c9e353415e7194af6",
165
+ "version_major": 2,
166
+ "version_minor": 0
167
+ },
168
+ "text/plain": [
169
+ "model.pt: 0%| | 0.00/179M [00:00<?, ?B/s]"
170
+ ]
171
+ },
172
+ "metadata": {},
173
+ "output_type": "display_data"
174
+ },
175
+ {
176
+ "name": "stdout",
177
+ "output_type": "stream",
178
+ "text": [
179
+ "✅ Loaded custom weights from: /home/hn533621/.cache/huggingface/hub/models--hemantn--ablang2/snapshots/13d4401549c368256c517dc13b8ed3d8b28d5e87/model.pt\n"
180
+ ]
181
+ },
182
+ {
183
+ "data": {
184
+ "application/vnd.jupyter.widget-view+json": {
185
+ "model_id": "e1c40183f9104aa1a67bf9b1c3daea0c",
186
+ "version_major": 2,
187
+ "version_minor": 0
188
+ },
189
+ "text/plain": [
190
+ "tokenizer_ablang2paired.py: 0.00B [00:00, ?B/s]"
191
+ ]
192
+ },
193
+ "metadata": {},
194
+ "output_type": "display_data"
195
+ },
196
+ {
197
+ "name": "stderr",
198
+ "output_type": "stream",
199
+ "text": [
200
+ "A new version of the following files was downloaded from https://huggingface.co/hemantn/ablang2:\n",
201
+ "- tokenizer_ablang2paired.py\n",
202
+ ". Make sure to double-check they do not contain any added malicious code. To avoid downloading new versions of the code file, you can pin a revision.\n"
203
+ ]
204
+ },
205
+ {
206
+ "data": {
207
+ "application/vnd.jupyter.widget-view+json": {
208
+ "model_id": "3fadab1179e2438ba88e08efb7819680",
209
+ "version_major": 2,
210
+ "version_minor": 0
211
+ },
212
+ "text/plain": [
213
+ "vocab.json: 0%| | 0.00/331 [00:00<?, ?B/s]"
214
+ ]
215
+ },
216
+ "metadata": {},
217
+ "output_type": "display_data"
218
+ },
219
+ {
220
+ "data": {
221
+ "application/vnd.jupyter.widget-view+json": {
222
+ "model_id": "5673cfaa95ac4da78e627c36ad6191b0",
223
+ "version_major": 2,
224
+ "version_minor": 0
225
+ },
226
+ "text/plain": [
227
+ "adapter.py: 0.00B [00:00, ?B/s]"
228
+ ]
229
+ },
230
+ "metadata": {},
231
+ "output_type": "display_data"
232
+ },
233
+ {
234
+ "name": "stdout",
235
+ "output_type": "stream",
236
+ "text": [
237
+ "📁 Files in current directory (/home/hn533621/.cache/huggingface/hub/models--hemantn--ablang2/snapshots/13d4401549c368256c517dc13b8ed3d8b28d5e87):\n",
238
+ " adapter.py\n",
239
+ " configuration_ablang2paired.py\n",
240
+ " tokenizer_ablang2paired.py\n",
241
+ " modeling_ablang2paired.py\n",
242
+ "✅ Successfully imported utility modules from cache directory\n"
243
+ ]
244
+ }
245
+ ],
246
+ "source": [
247
+ "# Load model and tokenizer from Hugging Face Hub\n",
248
+ "model = AutoModel.from_pretrained(\"hemantn/ablang2\", trust_remote_code=True)\n",
249
+ "tokenizer = AutoTokenizer.from_pretrained(\"hemantn/ablang2\", trust_remote_code=True)\n",
250
+ "\n",
251
+ "# Find the cached model directory and import adapter\n",
252
+ "adapter_path = hf_hub_download(repo_id=\"hemantn/ablang2\", filename=\"adapter.py\")\n",
253
+ "cached_model_dir = os.path.dirname(adapter_path)\n",
254
+ "sys.path.insert(0, cached_model_dir)\n",
255
+ "\n",
256
+ "# Import and create the adapter\n",
257
+ "from adapter import AbLang2PairedHuggingFaceAdapter\n",
258
+ "ablang = AbLang2PairedHuggingFaceAdapter(model=model, tokenizer=tokenizer)"
259
+ ]
260
+ },
261
+ {
262
+ "cell_type": "markdown",
263
+ "id": "48562761-6ebe-4025-be97-918c9f9eff7e",
264
+ "metadata": {},
265
+ "source": [
266
+ "# **2. Different modes for specific usecases**\n",
267
+ "\n",
268
+ "AbLang2 has already been implemented for a variety of different usecases. The benefit of these modes is that they handle extra tokens such as start, stop and separation tokens.\n",
269
+ "\n",
270
+ "1. seqcoding: Generates sequence representations for each sequence\n",
271
+ "2. rescoding: Generates residue representations for each residue in each sequence\n",
272
+ "3. likelihood: Generates likelihoods for each amino acid at each position in each sequence\n",
273
+ "4. probability: Generates probabilities for each amino acid at each position in each sequence\n",
274
+ "5. pseudo_log_likelihood: Returns the pseudo log likelihood for a sequence (based on masking each residue one at a time)\n",
275
+ "6. confidence: Returns a fast calculation of the log likelihood for a sequence (based on a single pass with no masking)\n",
276
+ "7. restore: Restores masked residues\n",
277
+ "\n",
278
+ "### **AbLang2 can also align the resulting representations using ANARCI**\n",
279
+ "\n",
280
+ "This can be done for 'rescoding', 'likelihood', and 'probability'. This is done by setting the argument \"align=True\".\n",
281
+ "\n",
282
+ "**NB**: Align can only be used on input with the same format, i.e. either all heavy, all light, or all both heavy and light.\n",
283
+ "\n",
284
+ "### **The align argument can also be used to restore variable missing lengths**\n",
285
+ "\n",
286
+ "For this, use \"align=True\" with the 'restore' mode."
287
+ ]
288
+ },
289
+ {
290
+ "cell_type": "code",
291
+ "execution_count": 5,
292
+ "id": "ceae4a88-0679-4704-8bad-c06a4569c497",
293
+ "metadata": {},
294
+ "outputs": [],
295
+ "source": [
296
+ "valid_modes = [\n",
297
+ " 'seqcoding', 'rescoding', 'likelihood', 'probability',\n",
298
+ " 'pseudo_log_likelihood', 'confidence', 'restore' \n",
299
+ "]"
300
+ ]
301
+ },
302
+ {
303
+ "cell_type": "markdown",
304
+ "id": "aa333732-7508-4826-92ec-3acdd54bc1bb",
305
+ "metadata": {},
306
+ "source": [
307
+ "## **seqcoding** \n",
308
+ "\n",
309
+ "The seqcodings represents each sequence as a 480 sized embedding. It is derived from averaging across each rescoding embedding for a given sequence, including extra tokens. \n",
310
+ "\n",
311
+ "**NB:** Seqcodings can also be derived in other ways like using the sum or averaging across only parts of the input such as the CDRs. For such cases please use and adapt the below rescoding."
312
+ ]
313
+ },
314
+ {
315
+ "cell_type": "code",
316
+ "execution_count": 6,
317
+ "id": "d22f4302-1262-4cc1-8a1c-a36daa8c710c",
318
+ "metadata": {},
319
+ "outputs": [
320
+ {
321
+ "data": {
322
+ "text/plain": [
323
+ "array([[-0.2520631 , 0.18189636, 0.00887137, ..., 0.15365516,\n",
324
+ " -0.14508602, -0.13381316],\n",
325
+ " [-0.24383117, 0.20946886, 0.07412891, ..., 0.15079288,\n",
326
+ " -0.13847049, -0.07304662],\n",
327
+ " [-0.20084268, 0.23405147, -0.00103735, ..., 0.07450922,\n",
328
+ " -0.08084311, -0.21812904],\n",
329
+ " [-0.12659703, 0.3051279 , -0.15117611, ..., -0.20749238,\n",
330
+ " -0.10453435, -0.0787883 ],\n",
331
+ " [-0.2955319 , 0.17239201, 0.05676926, ..., 0.15943624,\n",
332
+ " -0.16615382, -0.15569784]], shape=(5, 480), dtype=float32)"
333
+ ]
334
+ },
335
+ "execution_count": 6,
336
+ "metadata": {},
337
+ "output_type": "execute_result"
338
+ }
339
+ ],
340
+ "source": [
341
+ "ablang(all_seqs, mode='seqcoding')\n"
342
+ ]
343
+ },
344
+ {
345
+ "cell_type": "markdown",
346
+ "id": "4b5d9d60",
347
+ "metadata": {},
348
+ "source": [
349
+ "## **rescoding / likelihood / probability**\n",
350
+ "\n",
351
+ "The rescodings represents each residue as a 480 sized embedding. The likelihoods represents each residue as the predicted logits for each character in the vocabulary. The probabilities represents the normalised likelihoods.\n",
352
+ "\n",
353
+ "**NB:** The output includes extra tokens (start, stop and separation tokens) in the format \"<VH_seq>|<VL_seq>\". The length of the output is therefore 5 longer than the VH and VL.\n",
354
+ "\n",
355
+ "**NB:** By default the representations are derived using a single forward pass. To prevent the predicted likelihood and probability to be affected by the input residue at each position, setting the \"stepwise_masking\" argument to True can be used. This will run a forward pass for each position with the residue at that position masked. This is much slower than running a single forward pass."
356
+ ]
357
+ },
358
+ {
359
+ "cell_type": "code",
360
+ "execution_count": 7,
361
+ "id": "6227f661-575f-4b1e-9646-cfba7b10c3b4",
362
+ "metadata": {},
363
+ "outputs": [
364
+ {
365
+ "data": {
366
+ "text/plain": [
367
+ "[array([[-0.40741208, -0.5118987 , 0.06096708, ..., 0.3268144 ,\n",
368
+ " 0.03920235, -0.36715826],\n",
369
+ " [-0.5768883 , 0.38245413, -0.21791998, ..., 0.01250262,\n",
370
+ " -0.08844463, -0.32367525],\n",
371
+ " [-0.1475935 , 0.39639047, -0.38226923, ..., -0.10119921,\n",
372
+ " -0.41469565, -0.00319315],\n",
373
+ " ...,\n",
374
+ " [-0.14358369, 0.3124389 , -0.30157998, ..., -0.13289244,\n",
375
+ " -0.45353398, -0.07878865],\n",
376
+ " [ 0.17538925, 0.24394299, 0.20141171, ..., 0.14587352,\n",
377
+ " -0.38479003, 0.07409196],\n",
378
+ " [-0.23031706, -0.35487285, 0.1960684 , ..., -0.1283362 ,\n",
379
+ " 0.31107333, -0.3265108 ]], shape=(238, 480), dtype=float32),\n",
380
+ " array([[-0.41981837, -0.3666375 , 0.10595217, ..., 0.3903574 ,\n",
381
+ " 0.0382378 , -0.36337993],\n",
382
+ " [-0.5054137 , 0.38347068, -0.10992069, ..., -0.05231472,\n",
383
+ " -0.13636623, -0.34830108],\n",
384
+ " [-0.06784609, 0.69349885, -0.4212398 , ..., -0.24805346,\n",
385
+ " -0.39583805, -0.10972726],\n",
386
+ " ...,\n",
387
+ " [-0.02212614, 0.26338235, -0.5558968 , ..., -0.24067189,\n",
388
+ " -0.11965694, 0.07879876],\n",
389
+ " [-0.20650092, 0.43451664, -0.09650223, ..., -0.05296766,\n",
390
+ " -0.04297376, 0.41854134],\n",
391
+ " [-0.02653179, 0.03729444, 0.13194172, ..., -0.4554279 ,\n",
392
+ " 0.03723941, 0.17769177]], shape=(238, 480), dtype=float32),\n",
393
+ " array([[-0.40043733, -0.48596814, 0.0886725 , ..., 0.38941646,\n",
394
+ " 0.06195956, -0.40999672],\n",
395
+ " [-0.54576075, 0.4312959 , -0.3451486 , ..., -0.09285564,\n",
396
+ " 0.03116508, -0.45269737],\n",
397
+ " [ 0.0221165 , 0.53196615, -0.30137214, ..., -0.1889072 ,\n",
398
+ " -0.32587305, 0.05078396],\n",
399
+ " ...,\n",
400
+ " [-0.03700298, 0.7739084 , 0.3454928 , ..., -0.03060072,\n",
401
+ " 0.02420983, -0.48005292],\n",
402
+ " [-0.03366657, 0.74771184, -0.35423476, ..., -0.08759108,\n",
403
+ " -0.17898935, -0.4540483 ],\n",
404
+ " [-0.16625853, 0.2701079 , -0.19761363, ..., 0.10313392,\n",
405
+ " 0.44890267, -0.64840287]], shape=(238, 480), dtype=float32),\n",
406
+ " array([[-0.26863217, 0.32259187, 0.10813517, ..., 0.03953876,\n",
407
+ " 0.18312076, -0.00498045],\n",
408
+ " [-0.2165424 , -0.38562432, -0.02696264, ..., 0.20541488,\n",
409
+ " 0.18698391, -0.22639504],\n",
410
+ " [-0.41950518, 0.04743317, 0.0048816 , ..., 0.11408642,\n",
411
+ " -0.05384652, 0.1025871 ],\n",
412
+ " ...,\n",
413
+ " [-0.14095458, 0.5860325 , -0.44657114, ..., -0.39150292,\n",
414
+ " -0.22395667, -0.42516366],\n",
415
+ " [ 0.29816052, 0.40440455, -0.52062094, ..., 0.08969188,\n",
416
+ " -0.20792632, -0.2045222 ],\n",
417
+ " [-0.21370608, 0.23035707, -0.355185 , ..., -0.36726946,\n",
418
+ " -0.05693531, -0.37847823]], shape=(238, 480), dtype=float32),\n",
419
+ " array([[-0.42062947, -0.44009134, 0.00152371, ..., 0.27141467,\n",
420
+ " 0.03798106, -0.397461 ],\n",
421
+ " [-0.57318133, 0.5258899 , -0.17001636, ..., -0.23864633,\n",
422
+ " 0.2088059 , -0.57877594],\n",
423
+ " [-0.38988614, 0.46168196, -0.3429413 , ..., -0.14872643,\n",
424
+ " -0.46576905, -0.21224979],\n",
425
+ " ...,\n",
426
+ " [-0.21528634, 0.30046722, -0.25216463, ..., -0.11576828,\n",
427
+ " -0.4704907 , -0.0740136 ],\n",
428
+ " [ 0.0633081 , 0.22700705, 0.28184187, ..., 0.15967266,\n",
429
+ " -0.377182 , 0.06188517],\n",
430
+ " [-0.27826303, -0.37297496, 0.21229912, ..., -0.14886017,\n",
431
+ " 0.24998347, -0.35954213]], shape=(238, 480), dtype=float32)]"
432
+ ]
433
+ },
434
+ "execution_count": 7,
435
+ "metadata": {},
436
+ "output_type": "execute_result"
437
+ }
438
+ ],
439
+ "source": [
440
+ "ablang(all_seqs, mode='rescoding', stepwise_masking = False)"
441
+ ]
442
+ },
443
+ {
444
+ "cell_type": "markdown",
445
+ "id": "6da2183b-4306-49bd-a7fc-23e78a23f305",
446
+ "metadata": {},
447
+ "source": [
448
+ "## **Align rescoding/likelihood/probability output**\n",
449
+ "\n",
450
+ "For the 'rescoding', 'likelihood', and 'probability' modes, the output can also be aligned using the argument \"align=True\".\n",
451
+ "\n",
452
+ "This is done using the antibody numbering tool ANARCI, and requires manually installing **Pandas** and **[ANARCI](https://github.com/oxpig/ANARCI)**.\n",
453
+ "\n",
454
+ "**NB**: Align can only be used on input with the same format, i.e. either all heavy, all light, or all both heavy and light."
455
+ ]
456
+ },
457
+ {
458
+ "cell_type": "code",
459
+ "execution_count": 8,
460
+ "id": "e4bc0cb1-f5b0-4255-9e93-d643ae1396df",
461
+ "metadata": {},
462
+ "outputs": [
463
+ {
464
+ "name": "stdout",
465
+ "output_type": "stream",
466
+ "text": [
467
+ "['<' '1 ' '2 ' '3 ' '4 ' '5 ' '6 ' '7 ' '8 ' '9 ' '11 ' '12 ' '13 ' '14 '\n",
468
+ " '15 ' '16 ' '17 ' '18 ' '19 ' '20 ' '21 ' '22 ' '23 ' '24 ' '25 ' '26 '\n",
469
+ " '27 ' '28 ' '29 ' '30 ' '35 ' '36 ' '37 ' '38 ' '39 ' '40 ' '41 ' '42 '\n",
470
+ " '43 ' '44 ' '45 ' '46 ' '47 ' '48 ' '49 ' '50 ' '51 ' '52 ' '53 ' '54 '\n",
471
+ " '55 ' '56 ' '57 ' '58 ' '59 ' '62 ' '63 ' '64 ' '65 ' '66 ' '67 ' '68 '\n",
472
+ " '69 ' '70 ' '71 ' '72 ' '74 ' '75 ' '76 ' '77 ' '78 ' '79 ' '80 ' '81 '\n",
473
+ " '82 ' '83 ' '84 ' '85 ' '86 ' '87 ' '88 ' '89 ' '90 ' '91 ' '92 ' '93 '\n",
474
+ " '94 ' '95 ' '96 ' '97 ' '98 ' '99 ' '100 ' '101 ' '102 ' '103 ' '104 '\n",
475
+ " '105 ' '106 ' '107 ' '108 ' '109 ' '110 ' '111 ' '112A' '112 ' '113 '\n",
476
+ " '114 ' '115 ' '116 ' '117 ' '118 ' '119 ' '120 ' '121 ' '122 ' '123 '\n",
477
+ " '124 ' '125 ' '126 ' '127 ' '128 ' '>' '|' '<' '1 ' '2 ' '3 ' '4 ' '5 '\n",
478
+ " '6 ' '7 ' '8 ' '9 ' '10 ' '11 ' '12 ' '13 ' '14 ' '15 ' '16 ' '17 ' '18 '\n",
479
+ " '19 ' '20 ' '21 ' '22 ' '23 ' '24 ' '25 ' '26 ' '27 ' '28 ' '29 ' '30 '\n",
480
+ " '31 ' '32 ' '34 ' '35 ' '36 ' '37 ' '38 ' '39 ' '40 ' '41 ' '42 ' '43 '\n",
481
+ " '44 ' '45 ' '46 ' '47 ' '48 ' '49 ' '50 ' '51 ' '52 ' '53 ' '54 ' '55 '\n",
482
+ " '56 ' '57 ' '64 ' '65 ' '66 ' '67 ' '68 ' '69 ' '70 ' '71 ' '72 ' '74 '\n",
483
+ " '75 ' '76 ' '77 ' '78 ' '79 ' '80 ' '83 ' '84 ' '85 ' '86 ' '87 ' '88 '\n",
484
+ " '89 ' '90 ' '91 ' '92 ' '93 ' '94 ' '95 ' '96 ' '97 ' '98 ' '99 ' '100 '\n",
485
+ " '101 ' '102 ' '103 ' '104 ' '105 ' '106 ' '107 ' '108 ' '109 ' '114 '\n",
486
+ " '115 ' '116 ' '117 ' '118 ' '119 ' '120 ' '121 ' '122 ' '123 ' '124 '\n",
487
+ " '125 ' '126 ' '127 ' '>']\n",
488
+ "['<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKI-SNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>', '<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTT----->|<-----------PVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKI-SNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>', '<------SGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCAR**PGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSS*SLEASDTNIYLSWFQQRPGQSPRRLIYKI*N-RDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>']\n",
489
+ "[[[ 9.31621838 -3.42184329 -3.59397745 ... -14.73707485 -6.8935833\n",
490
+ " -0.23662776]\n",
491
+ " [ -3.54718232 -5.84866619 -4.02423859 ... -12.93966579 -9.5614481\n",
492
+ " -4.48473835]\n",
493
+ " [-11.94997597 -2.245543 -5.69481373 ... -15.19639015 -17.97454071\n",
494
+ " -12.56952095]\n",
495
+ " ...\n",
496
+ " [ -8.94504833 -0.42261261 -4.95588207 ... -16.66817474 -15.2224741\n",
497
+ " -10.37267494]\n",
498
+ " [-11.65150356 -5.44477606 -2.95585775 ... -16.25555801 -9.75158596\n",
499
+ " -11.75897026]\n",
500
+ " [ 1.79469728 -1.95846701 -3.59784532 ... -14.95585823 -7.47080708\n",
501
+ " -0.95226753]]\n",
502
+ "\n",
503
+ " [[ 8.55518723 -3.83663297 -2.33595967 ... -13.87456799 -8.14840603\n",
504
+ " -0.42472434]\n",
505
+ " [ -4.40701294 -5.53201008 -3.69397402 ... -12.97877789 -9.86258411\n",
506
+ " -4.95414352]\n",
507
+ " [-11.95642853 -3.86210871 -5.80935192 ... -14.89213085 -16.94556236\n",
508
+ " -11.36959839]\n",
509
+ " ...\n",
510
+ " [ -7.75924015 -0.66524202 -4.08643246 ... -16.16580772 -14.76507473\n",
511
+ " -8.3507061 ]\n",
512
+ " [-11.91039753 -4.86995983 -2.74777436 ... -16.07694817 -8.44974899\n",
513
+ " -10.45223904]\n",
514
+ " [ 0.86006832 -2.37964034 -3.58130741 ... -15.35423565 -7.73035526\n",
515
+ " -1.11989737]]\n",
516
+ "\n",
517
+ " [[ -4.37902737 -7.55587149 1.21958363 ... -15.48622513 -6.021842\n",
518
+ " -3.79647374]\n",
519
+ " [ 0. 0. 0. ... 0. 0.\n",
520
+ " 0. ]\n",
521
+ " [ 0. 0. 0. ... 0. 0.\n",
522
+ " 0. ]\n",
523
+ " ...\n",
524
+ " [ -8.94207573 -0.51090252 -5.09760332 ... -16.69521713 -15.45450687\n",
525
+ " -10.50823212]\n",
526
+ " [-11.92354965 -5.55152607 -2.87666893 ... -16.40607834 -10.19431686\n",
527
+ " -12.1328764 ]\n",
528
+ " [ 2.42200375 -2.01573253 -3.61701298 ... -14.9590435 -7.19029331\n",
529
+ " -0.89830256]]]\n"
530
+ ]
531
+ }
532
+ ],
533
+ "source": [
534
+ "results = ablang(only_both_chains_seqs, mode='likelihood', align=True)\n",
535
+ "\n",
536
+ "print(results.number_alignment)\n",
537
+ "print(results.aligned_seqs)\n",
538
+ "print(results.aligned_embeds)"
539
+ ]
540
+ },
541
+ {
542
+ "cell_type": "code",
543
+ "execution_count": 9,
544
+ "id": "56be8cad",
545
+ "metadata": {},
546
+ "outputs": [
547
+ {
548
+ "data": {
549
+ "text/plain": [
550
+ "[array([[9.9955505e-01, 2.9358694e-06, 2.4716087e-06, ..., 3.5776201e-11,\n",
551
+ " 9.1196831e-08, 7.0967326e-05],\n",
552
+ " [4.1573694e-06, 4.1619489e-07, 2.5800944e-06, ..., 3.4650952e-10,\n",
553
+ " 1.0159109e-08, 1.6279575e-06],\n",
554
+ " [7.8059600e-08, 1.2794037e-03, 4.0645118e-05, ..., 3.0375720e-09,\n",
555
+ " 1.8879491e-10, 4.2010839e-08],\n",
556
+ " ...,\n",
557
+ " [3.4210879e-07, 1.7195340e-03, 1.8477240e-05, ..., 1.5137445e-10,\n",
558
+ " 6.4255873e-10, 8.2064140e-08],\n",
559
+ " [9.1038084e-09, 4.5161755e-06, 5.4411950e-05, ..., 9.1139631e-11,\n",
560
+ " 6.0862085e-08, 8.1761966e-09],\n",
561
+ " [8.5759175e-04, 2.0104915e-05, 3.9023766e-06, ..., 4.5562460e-11,\n",
562
+ " 8.1156479e-08, 5.4990651e-05]], shape=(238, 26), dtype=float32),\n",
563
+ " array([[9.9939799e-01, 4.1499175e-06, 1.8611167e-05, ..., 1.8139243e-10,\n",
564
+ " 5.5649299e-08, 1.2583815e-04],\n",
565
+ " [1.6735513e-06, 5.4332406e-07, 3.4143472e-06, ..., 3.1693398e-10,\n",
566
+ " 7.1501400e-09, 9.6832969e-07],\n",
567
+ " [3.7784993e-08, 1.2377645e-04, 1.7658784e-05, ..., 2.0061326e-09,\n",
568
+ " 2.5737484e-10, 6.7947965e-08],\n",
569
+ " ...,\n",
570
+ " [1.1050455e-06, 1.3312638e-03, 4.3497097e-05, ..., 2.4686178e-10,\n",
571
+ " 1.0018089e-09, 6.1165900e-07],\n",
572
+ " [5.7270397e-09, 6.5396339e-06, 5.4601755e-05, ..., 8.8801404e-11,\n",
573
+ " 1.8233513e-07, 2.4615032e-08],\n",
574
+ " [7.3952030e-04, 2.8970928e-05, 8.7113440e-06, ..., 6.7168833e-11,\n",
575
+ " 1.3746008e-07, 1.0210846e-04]], shape=(222, 26), dtype=float32),\n",
576
+ " array([[9.99685407e-01, 3.35662639e-06, 1.14241482e-06, ...,\n",
577
+ " 2.32460891e-11, 6.88188067e-08, 5.69467156e-05],\n",
578
+ " [6.38133372e-07, 1.01300586e-07, 5.64459742e-06, ...,\n",
579
+ " 4.09234556e-11, 2.53804799e-09, 4.31722100e-07],\n",
580
+ " [1.49096788e-08, 2.04515047e-04, 9.23794141e-06, ...,\n",
581
+ " 7.46306961e-10, 2.92107380e-11, 2.21786500e-08],\n",
582
+ " ...,\n",
583
+ " [2.15093763e-07, 1.06453872e-03, 1.62486140e-05, ...,\n",
584
+ " 1.12102910e-10, 1.47300866e-10, 4.73037538e-08],\n",
585
+ " [4.30136682e-09, 3.09317988e-06, 3.96632568e-05, ...,\n",
586
+ " 5.24226877e-11, 2.39579450e-08, 3.86403221e-09],\n",
587
+ " [9.77773685e-04, 1.29533228e-05, 2.78623725e-06, ...,\n",
588
+ " 2.73364300e-11, 3.96418649e-08, 4.04014427e-05]],\n",
589
+ " shape=(238, 26), dtype=float32)]"
590
+ ]
591
+ },
592
+ "execution_count": 9,
593
+ "metadata": {},
594
+ "output_type": "execute_result"
595
+ }
596
+ ],
597
+ "source": [
598
+ "ablang(only_both_chains_seqs, mode='probability')"
599
+ ]
600
+ },
601
+ {
602
+ "cell_type": "markdown",
603
+ "id": "8f0a71ec-e916-4330-90d0-13a4b1121a89",
604
+ "metadata": {},
605
+ "source": [
606
+ "## **Pseudo log likelihood and Confidence scores**\n",
607
+ "\n",
608
+ "The pseudo log likelihood and confidence represents two methods for calculating the uncertainty for the input sequence.\n",
609
+ "\n",
610
+ "- pseudo_log_likelihood: For each position, the pseudo log likelihood is calculated when predicting the masked residue. The final score is an average across the whole input. This is similar to the approach taken in the ESM-2 paper for calculating pseudo perplexity [(Lin et al., 2023)](https://doi.org/10.1126/science.ade2574).\n",
611
+ "\n",
612
+ "- confidence: For each position, the log likelihood is calculated without masking the residue. The final score is an average across the whole input. \n",
613
+ "\n",
614
+ "**NB:** The **confidence is fast** to compute, requiring only a single forward pass per input. **Pseudo log likelihood is slow** to calculate, requiring L forward passes per input, where L is the length of the input.\n",
615
+ "\n",
616
+ "**NB:** It is recommended to use **pseudo log likelihood for final results** and **confidence for exploratory work**."
617
+ ]
618
+ },
619
+ {
620
+ "cell_type": "code",
621
+ "execution_count": 10,
622
+ "id": "83f3064b-48a7-42fb-ba82-ec153ea946da",
623
+ "metadata": {},
624
+ "outputs": [
625
+ {
626
+ "data": {
627
+ "text/plain": [
628
+ "array([1.96673731, 2.04801253, 2.09881898, 1.82533665, 1.97255249])"
629
+ ]
630
+ },
631
+ "execution_count": 10,
632
+ "metadata": {},
633
+ "output_type": "execute_result"
634
+ }
635
+ ],
636
+ "source": [
637
+ "results = ablang(all_seqs, mode='pseudo_log_likelihood')\n",
638
+ "np.exp(-results) # convert to pseudo perplexity"
639
+ ]
640
+ },
641
+ {
642
+ "cell_type": "code",
643
+ "execution_count": 11,
644
+ "id": "42cc8b34-5ae9-4857-93fe-a438a0f2a868",
645
+ "metadata": {},
646
+ "outputs": [
647
+ {
648
+ "data": {
649
+ "text/plain": [
650
+ "array([1.2636038, 1.126463 , 1.3123759, 1.2140924, 1.1805094],\n",
651
+ " dtype=float32)"
652
+ ]
653
+ },
654
+ "execution_count": 11,
655
+ "metadata": {},
656
+ "output_type": "execute_result"
657
+ }
658
+ ],
659
+ "source": [
660
+ "results = ablang(all_seqs, mode='confidence')\n",
661
+ "np.exp(-results)"
662
+ ]
663
+ },
664
+ {
665
+ "cell_type": "markdown",
666
+ "id": "e0b63e48-b2a1-4a8e-8ecb-449748a2cb25",
667
+ "metadata": {},
668
+ "source": [
669
+ "## **restore**\n",
670
+ "\n",
671
+ "This mode can be used to restore masked residues, and fragmented regions with \"align=True\". "
672
+ ]
673
+ },
674
+ {
675
+ "cell_type": "code",
676
+ "execution_count": 12,
677
+ "id": "2d5b725c-4eac-4a4b-9331-357c3ac140f7",
678
+ "metadata": {},
679
+ "outputs": [
680
+ {
681
+ "data": {
682
+ "text/plain": [
683
+ "array(['<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>',\n",
684
+ " '<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTT>|<PVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>',\n",
685
+ " '<EVQLVQSGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDPPGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>'],\n",
686
+ " dtype='<U238')"
687
+ ]
688
+ },
689
+ "execution_count": 12,
690
+ "metadata": {},
691
+ "output_type": "execute_result"
692
+ }
693
+ ],
694
+ "source": [
695
+ "restored = ablang(only_both_chains_seqs, mode='restore')\n",
696
+ "restored"
697
+ ]
698
+ },
699
+ {
700
+ "cell_type": "code",
701
+ "execution_count": 13,
702
+ "id": "0e9615f7-c490-4947-96f4-7617266c686e",
703
+ "metadata": {},
704
+ "outputs": [
705
+ {
706
+ "data": {
707
+ "text/plain": [
708
+ "array(['<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>',\n",
709
+ " '<EVQLLESGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDVPGHGAAFMDVWGTGTTVTVSS>|<DVVMTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>',\n",
710
+ " '<QVQLVQSGGEVKKPGASVKVSCRASGYTFRNYGLTWVRQAPGQGLEWMGWISAYNGNTNYAQKFQGRVTLTTDTSTSTAYMELRSLRSDDTAVYFCARDPPGHGAAFMDVWGTGTTVTVSS>|<DIQLTQSPLSLPVTLGQPASISCRSSQSLEASDTNIYLSWFQQRPGQSPRRLIYKISNRDSGVPDRFSGSGSGTHFTLRISRVEADDVAVYYCMQGTHWPPAFGQGTKVDIK>'],\n",
711
+ " dtype='<U238')"
712
+ ]
713
+ },
714
+ "execution_count": 13,
715
+ "metadata": {},
716
+ "output_type": "execute_result"
717
+ }
718
+ ],
719
+ "source": [
720
+ "restored = ablang(only_both_chains_seqs, mode='restore', align = True)\n",
721
+ "restored"
722
+ ]
723
+ },
724
+ {
725
+ "cell_type": "code",
726
+ "execution_count": null,
727
+ "id": "d80020ce",
728
+ "metadata": {},
729
+ "outputs": [],
730
+ "source": []
731
+ }
732
+ ],
733
+ "metadata": {
734
+ "kernelspec": {
735
+ "display_name": "lib_transformer",
736
+ "language": "python",
737
+ "name": "python3"
738
+ },
739
+ "language_info": {
740
+ "codemirror_mode": {
741
+ "name": "ipython",
742
+ "version": 3
743
+ },
744
+ "file_extension": ".py",
745
+ "mimetype": "text/x-python",
746
+ "name": "python",
747
+ "nbconvert_exporter": "python",
748
+ "pygments_lexer": "ipython3",
749
+ "version": "3.10.18"
750
+ }
751
+ },
752
+ "nbformat": 4,
753
+ "nbformat_minor": 5
754
+ }
tokenizer_ablang2paired.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from transformers import PreTrainedTokenizer
4
+
5
+
6
+ class AbLang2PairedTokenizer(PreTrainedTokenizer):
7
+ vocab_files_names = {"vocab_file": "vocab.json"}
8
+ model_input_names = ["input_ids"]
9
+
10
+ def __init__(self, vocab_file=None, **kwargs):
11
+ if vocab_file is None:
12
+ # Try to find vocab file in the current directory
13
+ vocab_file = "vocab.json"
14
+
15
+ self.vocab_file = vocab_file
16
+ with open(vocab_file, "r", encoding="utf-8") as f:
17
+ self.vocab = json.load(f)
18
+
19
+ # Set required token attributes (all as strings, standard for HF)
20
+ kwargs.setdefault("pad_token", "-")
21
+ kwargs.setdefault("mask_token", "*")
22
+ kwargs.setdefault("unk_token", "X")
23
+
24
+ super().__init__(**kwargs)
25
+
26
+ @property
27
+ def pad_token_id(self):
28
+ return self.vocab[self.pad_token]
29
+
30
+ @property
31
+ def mask_token_id(self):
32
+ return self.vocab[self.mask_token]
33
+
34
+ def _tokenize(self, text):
35
+ return list(text)
36
+
37
+ def tokenize(self, text, text_pair=None, **kwargs):
38
+ """Tokenize text or text pair."""
39
+ if text_pair is not None:
40
+ # For paired sequences, combine them with a separator
41
+ combined_text = text + "|" + text_pair
42
+ return self._tokenize(combined_text)
43
+ else:
44
+ return self._tokenize(text)
45
+
46
+ def _convert_token_to_id(self, token):
47
+ return self.vocab.get(token, self.vocab[self.unk_token])
48
+
49
+ def _convert_id_to_token(self, index):
50
+ inv_vocab = {v: k for k, v in self.vocab.items()}
51
+ return inv_vocab.get(index, self.unk_token)
52
+
53
+ def get_vocab(self):
54
+ return self.vocab
55
+
56
+ def save_vocabulary(self, save_directory, filename_prefix=None):
57
+ os.makedirs(save_directory, exist_ok=True)
58
+ path = os.path.join(save_directory, (filename_prefix or "") + "vocab.json")
59
+ with open(path, "w", encoding="utf-8") as f:
60
+ json.dump(self.vocab, f)
61
+ return (path,)
62
+
63
+ @classmethod
64
+ def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
65
+ try:
66
+ from transformers.utils import cached_file
67
+ vocab_file = cached_file(
68
+ pretrained_model_name_or_path,
69
+ "vocab.json",
70
+ cache_dir=kwargs.get("cache_dir"),
71
+ force_download=kwargs.get("force_download", False),
72
+ resume_download=kwargs.get("resume_download", False),
73
+ proxies=kwargs.get("proxies"),
74
+ token=kwargs.get("token"),
75
+ revision=kwargs.get("revision"),
76
+ local_files_only=kwargs.get("local_files_only", False),
77
+ )
78
+
79
+ if vocab_file is None or not os.path.exists(vocab_file):
80
+ raise ValueError(f"Vocabulary file vocab.json not found in {pretrained_model_name_or_path}")
81
+
82
+ return cls(vocab_file=vocab_file, **kwargs)
83
+ except Exception as e:
84
+ # Fallback for local paths
85
+ vocab_file = os.path.join(pretrained_model_name_or_path, "vocab.json")
86
+ if not os.path.exists(vocab_file):
87
+ raise ValueError(f"Vocabulary file {vocab_file} not found")
88
+ return cls(vocab_file=vocab_file, **kwargs)
89
+
90
+ def save_pretrained(self, save_directory, filename_prefix=None):
91
+ os.makedirs(save_directory, exist_ok=True)
92
+ vocab_files = self.save_vocabulary(save_directory, filename_prefix)
93
+
94
+ tokenizer_config = {
95
+ "tokenizer_class": f"{self.__class__.__module__}.{self.__class__.__name__}"
96
+ }
97
+ with open(os.path.join(save_directory, "tokenizer_config.json"), "w") as f:
98
+ json.dump(tokenizer_config, f, indent=2)
99
+
100
+ return vocab_files
101
+
102
+ def __call__(self, sequences, padding=False, return_tensors=None, **kwargs):
103
+ # Handle different input formats
104
+ if isinstance(sequences, str):
105
+ # Single string: "VH|VL"
106
+ sequences = [sequences]
107
+ elif isinstance(sequences, list) and len(sequences) > 0:
108
+ if isinstance(sequences[0], list):
109
+ # List of lists: [['VH', 'VL'], ['VH2', 'VL2']]
110
+ sequences = [f"{pair[0]}|{pair[1]}" for pair in sequences]
111
+ # List of strings: ["VH|VL", "VH2|VL2"] - already correct format
112
+
113
+ # Tokenize each sequence
114
+ input_ids = [[self._convert_token_to_id(tok) for tok in self._tokenize(seq)] for seq in sequences]
115
+ # Padding
116
+ if padding:
117
+ maxlen = max(len(ids) for ids in input_ids)
118
+ input_ids = [ids + [self.pad_token_id] * (maxlen - len(ids)) for ids in input_ids]
119
+ # Return tensors if requested
120
+ if return_tensors == 'pt':
121
+ import torch
122
+ input_ids = torch.tensor(input_ids)
123
+ return {'input_ids': input_ids}
124
+
vocab.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "<": 0,
3
+ "M": 1,
4
+ "R": 2,
5
+ "H": 3,
6
+ "K": 4,
7
+ "D": 5,
8
+ "E": 6,
9
+ "S": 7,
10
+ "T": 8,
11
+ "N": 9,
12
+ "Q": 10,
13
+ "C": 11,
14
+ "G": 12,
15
+ "P": 13,
16
+ "A": 14,
17
+ "V": 15,
18
+ "I": 16,
19
+ "F": 17,
20
+ "Y": 18,
21
+ "W": 19,
22
+ "L": 20,
23
+ "-": 21,
24
+ ">": 22,
25
+ "*": 23,
26
+ "X": 24,
27
+ "|": 25
28
+ }
vocab.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ablang_vocab = {
2
+ "<": 0,
3
+ "M": 1,
4
+ "R": 2,
5
+ "H": 3,
6
+ "K": 4,
7
+ "D": 5,
8
+ "E": 6,
9
+ "S": 7,
10
+ "T": 8,
11
+ "N": 9,
12
+ "Q": 10,
13
+ "C": 11,
14
+ "G": 12,
15
+ "P": 13,
16
+ "A": 14,
17
+ "V": 15,
18
+ "I": 16,
19
+ "F": 17,
20
+ "Y": 18,
21
+ "W": 19,
22
+ "L": 20,
23
+ "-": 21,
24
+ ">": 22,
25
+ "*": 23,
26
+ "X": 24,
27
+ "|": 25
28
+ }