anzhi2710gmailcom commited on
Commit
53e66de
·
verified ·
1 Parent(s): 76514c4

Upload folder using huggingface_hub

Browse files
Files changed (42) hide show
  1. .gitattributes +22 -10
  2. LICENSE +201 -0
  3. README.md +444 -0
  4. conf/CODE_OF_CONDUCT.md +128 -0
  5. conf/Makefile +9 -0
  6. config.json +28 -0
  7. model/CodonTransformer/CodonData.py +682 -0
  8. model/CodonTransformer/CodonEvaluation.py +278 -0
  9. model/CodonTransformer/CodonJupyter.py +311 -0
  10. model/CodonTransformer/CodonPrediction.py +855 -0
  11. model/CodonTransformer/CodonUtils.py +830 -0
  12. model/CodonTransformer/__init__.py +1 -0
  13. model/src/CodonTransformerTokenizer.json +1 -0
  14. model/src/CodonTransformer_inference_template.xlsx +0 -0
  15. model/src/__init__.py +1 -0
  16. model/src/banner_final.png +3 -0
  17. model/src/organism2id.pkl +3 -0
  18. pyproject.toml +60 -0
  19. requirements.txt +14 -0
  20. scripts/demo/sample_dataset.csv +6 -0
  21. scripts/demo/sample_predictions.csv +6 -0
  22. scripts/finetune.py +274 -0
  23. scripts/notebooks/CodonTransformerDemo.ipynb +162 -0
  24. scripts/pretrain.py +283 -0
  25. scripts/slurm/export_finetuned_model.sh +61 -0
  26. scripts/slurm/finetune.sh +39 -0
  27. scripts/slurm/prepare_finetune_data.sh +45 -0
  28. scripts/slurm/prepare_pretrain_data.sh +45 -0
  29. scripts/slurm/pretrain.sh +38 -0
  30. scripts/slurm/run_finetune.sh +73 -0
  31. scripts/slurm/run_inference_batch.sh +89 -0
  32. scripts/slurm/run_inference_finetuned.sh +70 -0
  33. scripts/slurm/run_inference_multiple.sh +113 -0
  34. scripts/slurm/run_inference_single.sh +73 -0
  35. scripts/slurm/run_pretrain.sh +78 -0
  36. scripts/tests/__init__.py +0 -0
  37. scripts/tests/test_CodonData.py +111 -0
  38. scripts/tests/test_CodonJupyter.py +118 -0
  39. scripts/tests/test_CodonPrediction.py +594 -0
  40. scripts/tests/test_CodonUtils.py +125 -0
  41. setup.py +42 -0
  42. weight/checkpoints/README.md +1 -0
.gitattributes CHANGED
@@ -1,35 +1,47 @@
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
  *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
  *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
  *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
11
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
 
 
17
  *.pt filter=lfs diff=lfs merge=lfs -text
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.tar filter=lfs diff=lfs merge=lfs -text
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
+ *tfevents* filter=lfs diff=lfs merge=lfs -textmodel/src/banner_final.png filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2024 Adibvafa Fallahpour
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - SCNet
8
+ - DCU
9
+ - codon-optimization
10
+ - protein-design
11
+ - sequence-generation
12
+ - transformer
13
+ frameworks:
14
+ - PyTorch
15
+ ---
16
+
17
+ <p align="center">
18
+ <strong>
19
+ <span style="font-size: 30px;">CodonTransformer</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Introduction
24
+
25
+ CodonTransformer is a deep learning model for multispecies codon optimization. Given an input protein sequence and a target host organism, it generates a host-specific DNA coding sequence.
26
+
27
+ Because of the degeneracy of the genetic code, the same protein can be encoded by many different DNA sequences, while different hosts have distinct preferences for synonymous codons. CodonTransformer uses a Transformer to model the contextual relationships among proteins, codons, and host organisms, generating DNA sequences that better match the natural codon distribution of the target host while preserving the translated protein sequence as much as possible.
28
+
29
+ Paper:
30
+ > **CodonTransformer: a multispecies codon optimizer using context-aware neural networks**
31
+ > *Nature Communications*, 2025
32
+
33
+ # Model Description
34
+
35
+ CodonTransformer is a Transformer model designed for multispecies codon optimization. Its core network is based on a BigBird masked language model and incorporates the STREAM representation proposed by the authors, which encodes target host information, amino acid information, and codon information into a unified sequence representation.
36
+
37
+ Unlike traditional optimization methods based on global codon frequencies, CodonTransformer considers not only host preferences for individual codons but also uses the contextual modeling capability of Transformers to learn local dependencies between neighboring codons. This enables it to generate DNA sequences that more closely resemble the natural coding patterns of the target host. The model can be used for multispecies codon optimization, heterologous protein expression sequence design, and further fine-tuning on custom DNA-protein-host datasets.
38
+
39
+ The official model was trained on more than one million DNA-protein paired samples spanning 164 species, including bacteria, archaea, plants, animals, and fungi.
40
+
41
+ # Use Cases
42
+
43
+ | Scenario | Description |
44
+ | --- | --- |
45
+ | Codon optimization | Redesign protein-coding DNA for a target host |
46
+ | Heterologous protein expression | Generate coding sequences that better match host codon preferences for different hosts |
47
+ | Multispecies sequence design | Switch the target organism among multiple supported hosts |
48
+ | Multiple candidate sequence generation | Generate multiple distinct DNA candidate sequences through temperature sampling |
49
+ | Batch codon optimization | Perform batch inference for multiple protein-host combinations |
50
+ | Model fine-tuning | Continue fine-tuning using custom DNA-protein-host data |
51
+ | Model pretraining | Train the model from scratch using large-scale processed training data |
52
+
53
+ # Usage
54
+
55
+ ## 1. Using OneCode
56
+
57
+ You can use the OneCode online environment for intelligent one-click AI4S programming:
58
+
59
+ [Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
60
+
61
+ ## 2. Manual Installation and Usage
62
+
63
+ ### Hardware Requirements
64
+
65
+ - CodonTransformer supports inference on both CPUs and accelerator devices.
66
+ - A CPU can be used for single protein sequence inference.
67
+ - GPU/DCU devices are recommended for batch inference, long-sequence inference, fine-tuning, and pretraining.
68
+
69
+ ### Environment Setup
70
+
71
+ #### DCU/SCNet Environment
72
+
73
+ ```bash
74
+ conda create -n onescience311 python=3.11 -y
75
+ conda activate onescience311
76
+
77
+ pip install onescience[bio] \
78
+ -i http://mirrors.onescience.ai:3141/pypi/simple/ \
79
+ --trusted-host mirrors.onescience.ai
80
+ ```
81
+
82
+ - If you encounter missing dependencies or version incompatibilities during execution, refer to the dependency versions specified in `requirements.txt` at the repository root and install or adjust the corresponding dependencies as needed.
83
+
84
+ ### Model and Data Preparation
85
+
86
+ #### 1) CodonTransformer Model Weights
87
+
88
+ The official model is available on Hugging Face:
89
+
90
+ ```text
91
+ https://huggingface.co/adibvafa/CodonTransformer
92
+ ```
93
+
94
+ In an online environment, `from_pretrained` automatically downloads the model weights when they are not available in the local cache. In an offline environment, the weights must be downloaded in advance to the cache directory corresponding to `HF_HOME`. The recommended cache location is:
95
+
96
+ ```text
97
+ /path/to/.cache/huggingface/hub/
98
+ ```
99
+
100
+ Set the following environment variables when running the scripts:
101
+
102
+ ```bash
103
+ export HF_HOME=/path/to/.cache/huggingface
104
+ export HF_HUB_OFFLINE=1
105
+ export TRANSFORMERS_OFFLINE=1
106
+ ```
107
+
108
+ The inference and fine-tuning scripts in this repository support using the local cache through `HF_HOME` and `OFFLINE=1`.
109
+
110
+ #### 2) Training Dataset
111
+
112
+ The official training data can be obtained from Zenodo or Hugging Face Datasets:
113
+
114
+ ```text
115
+ https://zenodo.org/records/12509224
116
+ https://huggingface.co/datasets/adibvafa/CodonTransformer
117
+ ```
118
+
119
+ It is recommended to place the downloaded raw data in:
120
+
121
+ ```text
122
+ scripts/data/raw/
123
+ ```
124
+
125
+ Accordingly, the complete dataset file is recommended to be saved as:
126
+
127
+ ```text
128
+ scripts/data/raw/dataset.csv
129
+ ```
130
+
131
+ The processed training JSON files are recommended to be saved in:
132
+
133
+ ```text
134
+ scripts/data/processed/
135
+ ```
136
+ ## 3. Quick Start
137
+
138
+ ### Download the Model Package
139
+
140
+ ```bash
141
+ hf download OneScience-Group/CodonTransformer \
142
+ --local-dir ./CodonTransformer
143
+
144
+ cd CodonTransformer
145
+ ```
146
+
147
+ # Inference Examples
148
+
149
+ ## Single-Sequence Codon Optimization
150
+
151
+ Run the script:
152
+
153
+ ```bash
154
+ bash scripts/slurm/run_inference_single.sh
155
+ ```
156
+
157
+ The default parameters are:
158
+
159
+ ```bash
160
+ PROTEIN="MFWY"
161
+ ORGANISM="Escherichia coli general"
162
+ OFFLINE=1
163
+ ```
164
+
165
+ To change the input protein and host:
166
+
167
+ ```bash
168
+ PROTEIN="MALWMRLLPLLALLALWGPDPAAA" \
169
+ ORGANISM="Homo sapiens" \
170
+ bash scripts/slurm/run_inference_single.sh
171
+ ```
172
+
173
+ ## Generate Multiple Candidate DNA Sequences
174
+
175
+ Run:
176
+
177
+ ```bash
178
+ bash scripts/slurm/run_inference_multiple.sh
179
+ ```
180
+
181
+ By default, multiple candidate DNA sequences are generated for the same protein sequence and saved to:
182
+
183
+ ```text
184
+ outputs/multiple_predictions.csv
185
+ ```
186
+
187
+ The main parameters include:
188
+
189
+ ```text
190
+ deterministic=False
191
+ temperature=0.5
192
+ top_p=0.95
193
+ num_sequences=5
194
+ match_protein=True
195
+ ```
196
+
197
+ where:
198
+
199
+ - `deterministic=False` enables probabilistic sampling.
200
+ - `temperature` controls sampling diversity, with a typical range of `0.2 ~ 0.8`.
201
+ - `top_p` controls nucleus sampling.
202
+ - `num_sequences` specifies the number of candidate sequences to generate.
203
+ - `match_protein=True` constrains the generated DNA to translate to the same input protein sequence.
204
+
205
+ ## Change the Target Host
206
+
207
+ The target host is specified directly through the `organism` parameter, for example:
208
+
209
+ ```python
210
+ organism = "Escherichia coli general"
211
+ ```
212
+
213
+ It can be changed to:
214
+
215
+ ```python
216
+ organism = "Homo sapiens"
217
+ ```
218
+
219
+ or:
220
+
221
+ ```python
222
+ organism = "Saccharomyces cerevisiae"
223
+ ```
224
+
225
+ Then rerun inference to obtain a codon-optimized DNA sequence for the corresponding host.
226
+
227
+ ## Batch Inference
228
+
229
+ The example data is currently located at:
230
+
231
+ ```text
232
+ scripts/demo/sample_dataset.csv
233
+ ```
234
+
235
+ Run directly:
236
+
237
+ ```bash
238
+ bash scripts/slurm/run_inference_batch.sh
239
+ ```
240
+
241
+ The default output is saved to:
242
+
243
+ ```text
244
+ outputs/sample_predictions.csv
245
+ ```
246
+
247
+ The input CSV must contain at least:
248
+
249
+ ```text
250
+ protein_sequence
251
+ organism
252
+ ```
253
+
254
+ To use a custom CSV:
255
+
256
+ ```bash
257
+ INPUT_CSV=/path/to/input.csv \
258
+ OUTPUT_CSV=/path/to/output.csv \
259
+ bash scripts/slurm/run_inference_batch.sh
260
+ ```
261
+
262
+ # Training
263
+
264
+ ## Fine-Tuning Data Preparation
265
+
266
+ For custom fine-tuning, first prepare your own CSV file. It is recommended to place it at:
267
+
268
+ ```text
269
+ scripts/data/raw/your_data.csv
270
+ ```
271
+
272
+ It must contain at least:
273
+
274
+ ```text
275
+ dna
276
+ protein
277
+ organism
278
+ ```
279
+
280
+ Then run the data preprocessing script provided in the repository:
281
+
282
+ ```bash
283
+ INPUT_CSV=$PWD/scripts/data/raw/your_data.csv \
284
+ OUTPUT_JSON=$PWD/scripts/data/processed/finetune_data.json \
285
+ bash scripts/slurm/prepare_finetune_data.sh
286
+ ```
287
+
288
+ ## CodonTransformer Fine-Tuning
289
+
290
+ Run the script:
291
+
292
+ ```bash
293
+ bash scripts/slurm/run_finetune.sh
294
+ ```
295
+
296
+ By default, it reads:
297
+
298
+ ```text
299
+ scripts/data/processed/finetune_data.json
300
+ ```
301
+
302
+ and saves checkpoints to:
303
+
304
+ ```text
305
+ weight/checkpoints/finetune
306
+ ```
307
+
308
+ To fine-tune using custom data, run:
309
+
310
+ ```bash
311
+ DATASET_JSON=$PWD/scripts/data/processed/finetune_data.json \
312
+ CHECKPOINT_DIR=$PWD/weight/checkpoints/finetune \
313
+ CHECKPOINT_FILENAME=finetune.ckpt \
314
+ BATCH_SIZE=6 \
315
+ MAX_EPOCHS=15 \
316
+ NUM_WORKERS=5 \
317
+ ACCUMULATE_GRAD_BATCHES=1 \
318
+ NUM_GPUS=4 \
319
+ LEARNING_RATE=0.00005 \
320
+ WARMUP_FRACTION=0.1 \
321
+ SAVE_EVERY_N_STEPS=512 \
322
+ SEED=123 \
323
+ DEBUG=0 \
324
+ bash scripts/slurm/run_finetune.sh
325
+ ```
326
+
327
+ Here, `NUM_GPUS=4`, `BATCH_SIZE=6`, and `MAX_EPOCHS=15` are the default training settings. In practice, these parameters should be adjusted according to the number of allocated GPU/DCU devices, available device memory, and dataset size.
328
+
329
+ ## Export the Fine-Tuned Model and Run Inference
330
+
331
+ After fine-tuning is complete, the following script can be used to export the checkpoint into a model file suitable for inference:
332
+
333
+ ```text
334
+ scripts/slurm/export_finetuned_model.sh
335
+ ```
336
+
337
+ Run it as follows:
338
+
339
+ ```bash
340
+ CHECKPOINT_PATH=/path/to/finetuned_checkpoint.ckpt \
341
+ OUTPUT_MODEL_PATH=/path/to/output_finetuned_model.pt \
342
+ NUM_ORGANISMS=164 \
343
+ bash scripts/slurm/export_finetuned_model.sh
344
+ ```
345
+
346
+ After export, the following script can be used to load the fine-tuned model for inference:
347
+
348
+ ```text
349
+ scripts/slurm/run_inference_finetuned.sh
350
+ ```
351
+
352
+ Run it as follows:
353
+
354
+ ```bash
355
+ PROTEIN="MFWY" \
356
+ ORGANISM="Escherichia coli general" \
357
+ MODEL_PATH=/path/to/output_finetuned_model.pt \
358
+ bash scripts/slurm/run_inference_finetuned.sh
359
+ ```
360
+
361
+ ## Pretraining
362
+
363
+ The current pretraining entry point is:
364
+
365
+ ```text
366
+ scripts/pretrain.py
367
+ ```
368
+
369
+ Pretraining is a complete model training workflow and requires large-scale processed DNA-protein-host data. Before full pretraining, the complete `dataset.csv` must first be converted into a JSONL file that can be read by the training script.
370
+
371
+ The repository provides the following preprocessing script:
372
+
373
+ ```text
374
+ scripts/slurm/prepare_pretrain_data.sh
375
+ ```
376
+
377
+ By default, this script reads:
378
+
379
+ ```text
380
+ scripts/data/raw/dataset.csv
381
+ ```
382
+
383
+ and outputs:
384
+
385
+ ```text
386
+ scripts/data/processed/pretrain_data.json
387
+ ```
388
+
389
+ Therefore, before full pretraining, first run:
390
+
391
+ ```bash
392
+ bash scripts/slurm/prepare_pretrain_data.sh
393
+ ```
394
+
395
+ If the raw data or output directory differs from the default path, modify it using environment variables:
396
+
397
+ ```bash
398
+ INPUT_CSV=$PWD/scripts/data/raw/dataset.csv \
399
+ OUTPUT_JSON=$PWD/scripts/data/processed/pretrain_data.json \
400
+ bash scripts/slurm/prepare_pretrain_data.sh
401
+ ```
402
+
403
+ The complete generated pretraining data is recommended to be saved as:
404
+
405
+ ```text
406
+ scripts/data/processed/pretrain_data.json
407
+ ```
408
+
409
+ Full pretraining can be started with:
410
+
411
+ ```bash
412
+ TRAIN_DATA_PATH=$PWD/scripts/data/processed/pretrain_data.json \
413
+ CHECKPOINT_DIR=$PWD/weight/checkpoints/pretrain \
414
+ BATCH_SIZE=6 \
415
+ MAX_EPOCHS=5 \
416
+ NUM_WORKERS=5 \
417
+ ACCUMULATE_GRAD_BATCHES=1 \
418
+ NUM_GPUS=16 \
419
+ LEARNING_RATE=0.00005 \
420
+ WARMUP_FRACTION=0.1 \
421
+ SAVE_INTERVAL=5 \
422
+ SEED=123 \
423
+ DEBUG=0 \
424
+ bash scripts/slurm/run_pretrain.sh
425
+ ```
426
+
427
+ Here, `NUM_GPUS=16` corresponds to the default multi-device setting in the pretraining script. In practice, adjust `NUM_GPUS` according to the number of allocated GPU/DCU devices. Pretraining checkpoints are saved by default to:
428
+
429
+ ```text
430
+ weight/checkpoints/pretrain
431
+ ```
432
+ # OneScience Official Information
433
+
434
+ | Platform | OneScience Main Repository | Skills Repository |
435
+ | --- | --- | --- |
436
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
437
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
438
+
439
+
440
+ # Citation and License
441
+
442
+ - The official CodonTransformer source code repository is licensed under the **Apache License 2.0**. See the `LICENSE` file in the repository root for details.
443
+ - CodonTransformer model weights are distributed separately through Hugging Face, while the training data is available through Zenodo and Hugging Face Datasets. The model weights, training data, and related third-party resources must each be used in accordance with the licenses and terms of use specified on their respective pages.
444
+ - This repository is the **DCU-adapted version** of CodonTransformer. The use of the repository code, model weights, and related data remains subject to the licenses and terms of use of their respective original projects.
conf/CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ Adibvafa.fallahpour@mail.utoronto.ca.
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
conf/Makefile ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile
2
+
3
+ .PHONY: test
4
+ test:
5
+ python -m unittest discover -s tests
6
+
7
+ .PHONY: test_with_coverage
8
+ test_with_coverage:
9
+ coverage run -m unittest discover -s tests
config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "CodonTransformer",
3
+ "version": "1.6.7",
4
+ "layout": {
5
+ "conf": "Repository configuration and auxiliary project files.",
6
+ "model": "CodonTransformer Python package and bundled model resources.",
7
+ "scripts": "Executable training, inference, SLURM, notebook, test, and demo files.",
8
+ "weight": "Local model weights and checkpoints."
9
+ },
10
+ "entrypoints": {
11
+ "prepare_pretrain_data": "scripts/slurm/prepare_pretrain_data.sh",
12
+ "prepare_finetune_data": "scripts/slurm/prepare_finetune_data.sh",
13
+ "export_finetuned_model": "scripts/slurm/export_finetuned_model.sh",
14
+ "inference_finetuned": "scripts/slurm/run_inference_finetuned.sh",
15
+ "finetune": "scripts/finetune.py",
16
+ "pretrain": "scripts/pretrain.py",
17
+ "inference_single": "scripts/slurm/run_inference_single.sh",
18
+ "inference_multiple": "scripts/slurm/run_inference_multiple.sh",
19
+ "inference_batch": "scripts/slurm/run_inference_batch.sh"
20
+ },
21
+ "resources": {
22
+ "tokenizer": "model/src/CodonTransformerTokenizer.json",
23
+ "demo_dataset": "scripts/demo/sample_dataset.csv",
24
+ "raw_training_data": "scripts/data/raw",
25
+ "processed_training_data": "scripts/data/processed",
26
+ "checkpoints": "weight/checkpoints"
27
+ }
28
+ }
model/CodonTransformer/CodonData.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: CodonData.py
3
+ ---------------------
4
+ Includes helper functions for preprocessing NCBI or Kazusa databases and
5
+ preparing the data for training and inference of the CodonTransformer model.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import random
11
+ from typing import Dict, List, Optional, Tuple, Union
12
+
13
+ import pandas as pd
14
+ import python_codon_tables as pct
15
+ from Bio import SeqIO
16
+ from Bio.Seq import Seq
17
+ from sklearn.utils import shuffle as sk_shuffle
18
+ from tqdm import tqdm
19
+
20
+ from CodonTransformer.CodonUtils import (
21
+ AMBIGUOUS_AMINOACID_MAP,
22
+ AMINO2CODON_TYPE,
23
+ AMINO_ACIDS,
24
+ ORGANISM2ID,
25
+ START_CODONS,
26
+ STOP_CODONS,
27
+ STOP_SYMBOL,
28
+ STOP_SYMBOLS,
29
+ ProteinConfig,
30
+ find_pattern_in_fasta,
31
+ get_taxonomy_id,
32
+ sort_amino2codon_skeleton,
33
+ )
34
+
35
+
36
+ def prepare_training_data(
37
+ dataset: Union[str, pd.DataFrame], output_file: str, shuffle: bool = True
38
+ ) -> None:
39
+ """
40
+ Prepare a JSON dataset for training the CodonTransformer model.
41
+
42
+ Input dataset should have columns below:
43
+ - dna: str (DNA sequence)
44
+ - protein: str (Protein sequence)
45
+ - organism: Union[int, str] (ID or Name of the organism)
46
+
47
+ The output JSON dataset will have the following format:
48
+ {"idx": 0, "codons": "M_ATG R_AGG L_TTG L_CTA R_CGA __TAG", "organism": 51}
49
+ {"idx": 1, "codons": "M_ATG K_AAG C_TGC F_TTT F_TTC __TAA", "organism": 59}
50
+
51
+ Args:
52
+ dataset (Union[str, pd.DataFrame]): Input dataset in CSV or DataFrame format.
53
+ output_file (str): Path to save the output JSON dataset.
54
+ shuffle (bool, optional): Whether to shuffle the dataset before saving.
55
+ Defaults to True.
56
+
57
+ Returns:
58
+ None
59
+ """
60
+ if isinstance(dataset, str):
61
+ dataset = pd.read_csv(dataset)
62
+
63
+ required_columns = {"dna", "protein", "organism"}
64
+ if not required_columns.issubset(dataset.columns):
65
+ raise ValueError(f"Input dataset must have columns: {required_columns}")
66
+
67
+ # Prepare the dataset for finetuning
68
+ dataset["codons"] = dataset.apply(
69
+ lambda row: get_merged_seq(row["protein"], row["dna"], separator="_"), axis=1
70
+ )
71
+
72
+ # Replace organism str with organism id using ORGANISM2ID
73
+ dataset["organism"] = dataset["organism"].apply(
74
+ lambda org: process_organism(org, ORGANISM2ID)
75
+ )
76
+
77
+ # Save the dataset to a JSON file
78
+ dataframe_to_json(dataset[["codons", "organism"]], output_file, shuffle=shuffle)
79
+
80
+
81
+ def dataframe_to_json(df: pd.DataFrame, output_file: str, shuffle: bool = True) -> None:
82
+ """
83
+ Convert pandas DataFrame to JSON file format suitable for training CodonTransformer.
84
+
85
+ This function takes a preprocessed DataFrame and writes it to a JSON file
86
+ where each line is a JSON object representing a single record.
87
+
88
+ Args:
89
+ df (pd.DataFrame): The input DataFrame with 'codons' and 'organism' columns.
90
+ output_file (str): Path to the output JSON file.
91
+ shuffle (bool, optional): Whether to shuffle the dataset before saving.
92
+ Defaults to True.
93
+
94
+ Returns:
95
+ None
96
+
97
+ Raises:
98
+ ValueError: If the required columns are not present in the DataFrame.
99
+ """
100
+ required_columns = {"codons", "organism"}
101
+ if not required_columns.issubset(df.columns):
102
+ raise ValueError(f"DataFrame must contain columns: {required_columns}")
103
+
104
+ print(f"\nStarted writing to {output_file}...")
105
+
106
+ # Shuffle the DataFrame if requested
107
+ if shuffle:
108
+ df = sk_shuffle(df)
109
+
110
+ # Write the DataFrame to a JSON file
111
+ with open(output_file, "w") as f:
112
+ for idx, row in tqdm(
113
+ df.iterrows(), total=len(df), desc="Writing JSON...", unit=" records"
114
+ ):
115
+ doc = {"idx": idx, "codons": row["codons"], "organism": row["organism"]}
116
+ f.write(json.dumps(doc) + "\n")
117
+
118
+ print(f"\nTotal Entries Saved: {len(df)}, JSON data saved to {output_file}")
119
+
120
+
121
+ def process_organism(organism: Union[str, int], organism_to_id: Dict[str, int]) -> int:
122
+ """
123
+ Process and validate the organism input, converting it to a valid organism ID.
124
+
125
+ This function handles both string (organism name) and integer (organism ID) inputs.
126
+ It validates the input against a provided mapping of organism names to IDs.
127
+
128
+ Args:
129
+ organism (Union[str, int]): Input organism, either as a name (str) or ID (int).
130
+ organism_to_id (Dict[str, int]): Dictionary mapping organism names to their
131
+ corresponding IDs.
132
+
133
+ Returns:
134
+ int: The validated organism ID.
135
+
136
+ Raises:
137
+ ValueError: If the input is an invalid organism name or ID.
138
+ TypeError: If the input is neither a string nor an integer.
139
+ """
140
+ if isinstance(organism, str):
141
+ if organism not in organism_to_id:
142
+ raise ValueError(f"Invalid organism name: {organism}")
143
+ return organism_to_id[organism]
144
+
145
+ elif isinstance(organism, int):
146
+ if organism not in organism_to_id.values():
147
+ raise ValueError(f"Invalid organism ID: {organism}")
148
+ return organism
149
+
150
+ raise TypeError(
151
+ f"Organism must be a string or integer, not {type(organism).__name__}"
152
+ )
153
+
154
+
155
+ def preprocess_protein_sequence(protein: str) -> str:
156
+ """
157
+ Preprocess a protein sequence by cleaning, standardizing, and handling
158
+ ambiguous amino acids.
159
+
160
+ Args:
161
+ protein (str): The input protein sequence.
162
+
163
+ Returns:
164
+ str: The preprocessed protein sequence.
165
+
166
+ Raises:
167
+ ValueError: If the protein sequence is invalid or if the configuration is invalid.
168
+ """
169
+ if not protein:
170
+ raise ValueError("Protein sequence is empty.")
171
+
172
+ # Clean and standardize the protein sequence
173
+ protein = (
174
+ protein.upper().strip().replace("\n", "").replace(" ", "").replace("\t", "")
175
+ )
176
+
177
+ # Handle ambiguous amino acids based on the specified behavior
178
+ config = ProteinConfig()
179
+ ambiguous_aminoacid_map_override = config.get("ambiguous_aminoacid_map_override")
180
+ ambiguous_aminoacid_behavior = config.get("ambiguous_aminoacid_behavior")
181
+ ambiguous_aminoacid_map = AMBIGUOUS_AMINOACID_MAP.copy()
182
+
183
+ for aminoacid, standard_aminoacids in ambiguous_aminoacid_map_override.items():
184
+ ambiguous_aminoacid_map[aminoacid] = standard_aminoacids
185
+
186
+ if ambiguous_aminoacid_behavior == "raise_error":
187
+ if any(aminoacid in ambiguous_aminoacid_map for aminoacid in protein):
188
+ raise ValueError("Ambiguous amino acids found in protein sequence.")
189
+ elif ambiguous_aminoacid_behavior == "standardize_deterministic":
190
+ protein = "".join(
191
+ ambiguous_aminoacid_map.get(aminoacid, [aminoacid])[0]
192
+ for aminoacid in protein
193
+ )
194
+ elif ambiguous_aminoacid_behavior == "standardize_random":
195
+ protein = "".join(
196
+ random.choice(ambiguous_aminoacid_map.get(aminoacid, [aminoacid]))
197
+ for aminoacid in protein
198
+ )
199
+ else:
200
+ raise ValueError(
201
+ f"Invalid ambiguous_aminoacid_behavior: {ambiguous_aminoacid_behavior}."
202
+ )
203
+
204
+ # Check for sequence validity
205
+ if any(aminoacid not in AMINO_ACIDS + STOP_SYMBOLS for aminoacid in protein):
206
+ raise ValueError("Invalid characters in protein sequence.")
207
+
208
+ if protein[-1] not in AMINO_ACIDS + STOP_SYMBOLS:
209
+ raise ValueError(
210
+ "Protein sequence must end with `*`, or `_`, or an amino acid."
211
+ )
212
+
213
+ # Replace '*' at the end of protein with STOP_SYMBOL if present
214
+ if protein[-1] == "*":
215
+ protein = protein[:-1] + STOP_SYMBOL
216
+
217
+ # Add stop symbol to end of protein
218
+ if protein[-1] != STOP_SYMBOL:
219
+ protein += STOP_SYMBOL
220
+
221
+ return protein
222
+
223
+
224
+ def replace_ambiguous_codons(dna: str) -> str:
225
+ """
226
+ Replaces ambiguous codons in a DNA sequence with "UNK".
227
+
228
+ Args:
229
+ dna (str): The DNA sequence to process.
230
+
231
+ Returns:
232
+ str: The processed DNA sequence with ambiguous codons replaced by "UNK".
233
+ """
234
+ result = []
235
+ dna = dna.upper()
236
+
237
+ # Check codons in DNA sequence
238
+ for i in range(0, len(dna), 3):
239
+ codon = dna[i : i + 3]
240
+
241
+ if len(codon) == 3 and all(nucleotide in "ATCG" for nucleotide in codon):
242
+ result.append(codon)
243
+ else:
244
+ result.append("UNK")
245
+
246
+ return "".join(result)
247
+
248
+
249
+ def preprocess_dna_sequence(dna: str) -> str:
250
+ """
251
+ Cleans and preprocesses a DNA sequence by standardizing it and replacing
252
+ ambiguous codons.
253
+
254
+ Args:
255
+ dna (str): The DNA sequence to preprocess.
256
+
257
+ Returns:
258
+ str: The cleaned and preprocessed DNA sequence.
259
+ """
260
+ if not dna:
261
+ return ""
262
+
263
+ # Clean and standardize the DNA sequence
264
+ dna = dna.upper().strip().replace("\n", "").replace(" ", "").replace("\t", "")
265
+
266
+ # Replace codons with ambigous nucleotides with "UNK"
267
+ dna = replace_ambiguous_codons(dna)
268
+
269
+ # Add unkown stop codon to end of DNA sequence if not present
270
+ if dna[-3:] not in STOP_CODONS:
271
+ dna += "UNK"
272
+
273
+ return dna
274
+
275
+
276
+ def get_merged_seq(protein: str, dna: str = "", separator: str = "_") -> str:
277
+ """
278
+ Return the merged sequence of protein amino acids and DNA codons in the form
279
+ of tokens separated by space, where each token is composed of an amino acid +
280
+ separator + codon.
281
+
282
+ Args:
283
+ protein (str): Protein sequence.
284
+ dna (str): DNA sequence.
285
+ separator (str): Separator between amino acid and codon.
286
+
287
+ Returns:
288
+ str: Merged sequence.
289
+
290
+ Example:
291
+ >>> get_merged_seq(protein="MAV_", dna="ATGGCTGTGTAA", separator="_")
292
+ 'M_ATG A_GCT V_GTG __TAA'
293
+
294
+ >>> get_merged_seq(protein="QHH_", dna="", separator="_")
295
+ 'Q_UNK H_UNK H_UNK __UNK'
296
+ """
297
+ merged_seq = ""
298
+
299
+ # Prepare protein and dna sequences
300
+ dna = preprocess_dna_sequence(dna)
301
+ protein = preprocess_protein_sequence(protein)
302
+
303
+ # Check if the length of protein and dna sequences are equal
304
+ if len(dna) > 0 and len(protein) != len(dna) / 3:
305
+ raise ValueError(
306
+ 'Length of protein (including stop symbol such as "_") and '
307
+ "the number of codons in DNA sequence (including stop codon) "
308
+ "must be equal."
309
+ )
310
+
311
+ # Merge protein and DNA sequences into tokens
312
+ for i, aminoacid in enumerate(protein):
313
+ merged_seq += f'{aminoacid}{separator}{dna[i * 3:i * 3 + 3] if dna else "UNK"} '
314
+
315
+ return merged_seq.strip()
316
+
317
+
318
+ def is_correct_seq(dna: str, protein: str, stop_symbol: str = STOP_SYMBOL) -> bool:
319
+ """
320
+ Check if the given DNA and protein pair is correct, that is:
321
+ 1. The length of dna is divisible by 3
322
+ 2. There is an initiator codon in the beginning of dna
323
+ 3. There is only one stop codon in the sequence
324
+ 4. The only stop codon is the last codon
325
+
326
+ Note since in Codon Table 3, 'TGA' is interpreted as Triptophan (W),
327
+ there is a separate check to make sure those sequences are considered correct.
328
+
329
+ Args:
330
+ dna (str): DNA sequence.
331
+ protein (str): Protein sequence.
332
+ stop_symbol (str): Stop symbol.
333
+
334
+ Returns:
335
+ bool: True if the sequence is correct, False otherwise.
336
+ """
337
+ return (
338
+ len(dna) % 3 == 0 # Check if DNA length is divisible by 3
339
+ and dna[:3].upper() in START_CODONS # Check for initiator codon
340
+ and protein[-1]
341
+ == stop_symbol # Check if the last protein symbol is the stop symbol
342
+ and protein.count(stop_symbol) == 1 # Check if there is only one stop symbol
343
+ and len(set(dna))
344
+ == 4 # Check if DNA consists of 4 unique nucleotides (A, T, C, G)
345
+ )
346
+
347
+
348
+ def get_amino_acid_sequence(
349
+ dna: str,
350
+ stop_symbol: str = "_",
351
+ codon_table: int = 1,
352
+ return_correct_seq: bool = False,
353
+ ) -> Union[str, Tuple[str, bool]]:
354
+ """
355
+ Return the translated protein sequence given a DNA sequence and codon table.
356
+
357
+ Args:
358
+ dna (str): DNA sequence.
359
+ stop_symbol (str): Stop symbol.
360
+ codon_table (int): Codon table number.
361
+ return_correct_seq (bool): Whether to return if the sequence is correct.
362
+
363
+ Returns:
364
+ Union[str, Tuple[str, bool]]: Protein sequence and correctness flag if
365
+ return_correct_seq is True, otherwise just the protein sequence.
366
+ """
367
+ dna_seq = Seq(dna).strip()
368
+
369
+ # Translate the DNA sequence to a protein sequence
370
+ protein_seq = str(
371
+ dna_seq.translate(
372
+ stop_symbol=stop_symbol, # Symbol to use for stop codons
373
+ to_stop=False, # Translate the entire sequence, including any stop codons
374
+ cds=False, # Do not assume the input is a coding sequence
375
+ table=codon_table, # Codon table to use for translation
376
+ )
377
+ ).strip()
378
+
379
+ return (
380
+ protein_seq
381
+ if not return_correct_seq
382
+ else (protein_seq, is_correct_seq(dna_seq, protein_seq, stop_symbol))
383
+ )
384
+
385
+
386
+ def read_fasta_file(
387
+ input_file: str,
388
+ save_to_file: Optional[str] = None,
389
+ organism: str = "",
390
+ buffer_size: int = 50000,
391
+ ) -> pd.DataFrame:
392
+ """
393
+ Read a FASTA file of DNA sequences and convert it to a Pandas DataFrame.
394
+ Optionally, save the DataFrame to a CSV file.
395
+
396
+ Args:
397
+ input_file (str): Path to the input FASTA file.
398
+ save_to_file (Optional[str]): Path to save the output DataFrame. If None,
399
+ data is only returned.
400
+ organism (str): Name of the organism. If empty, it will be extracted from
401
+ the FASTA description.
402
+ buffer_size (int): Number of records to process before writing to file.
403
+
404
+ Returns:
405
+ pd.DataFrame: DataFrame containing the DNA sequences if return_dataframe
406
+ is True, else None.
407
+
408
+ Raises:
409
+ FileNotFoundError: If the input file does not exist.
410
+ """
411
+ if not os.path.exists(input_file):
412
+ raise FileNotFoundError(f"Input file not found: {input_file}")
413
+
414
+ buffer = []
415
+ columns = [
416
+ "dna",
417
+ "protein",
418
+ "correct_seq",
419
+ "organism",
420
+ "GeneID",
421
+ "description",
422
+ "tokenized",
423
+ ]
424
+
425
+ # Initialize DataFrame to store all data if return_dataframe is True
426
+ all_data = pd.DataFrame(columns=columns)
427
+
428
+ with open(input_file, "r") as fasta_file:
429
+ for record in tqdm(
430
+ SeqIO.parse(fasta_file, "fasta"),
431
+ desc=f"Processing {organism}",
432
+ unit=" Records",
433
+ ):
434
+ dna = str(record.seq).strip().upper() # Ensure uppercase DNA sequence
435
+
436
+ # Determine the organism from the record if not provided
437
+ current_organism = organism or find_pattern_in_fasta(
438
+ "organism", record.description
439
+ )
440
+ gene_id = find_pattern_in_fasta("GeneID", record.description)
441
+
442
+ # Get the appropriate codon table for the organism
443
+ codon_table = get_codon_table(current_organism)
444
+
445
+ # Translate DNA to protein sequence
446
+ protein, correct_seq = get_amino_acid_sequence(
447
+ dna,
448
+ stop_symbol=STOP_SYMBOL,
449
+ codon_table=codon_table,
450
+ return_correct_seq=True,
451
+ )
452
+ description = record.description.split("[", 1)[0].strip()
453
+ tokenized = get_merged_seq(protein, dna, separator=STOP_SYMBOL)
454
+
455
+ # Create a data row for the current sequence
456
+ data_row = {
457
+ "dna": dna,
458
+ "protein": protein,
459
+ "correct_seq": correct_seq,
460
+ "organism": current_organism,
461
+ "GeneID": gene_id,
462
+ "description": description,
463
+ "tokenized": tokenized,
464
+ }
465
+ buffer.append(data_row)
466
+
467
+ # Write buffer to CSV file when buffer size is reached
468
+ if save_to_file and len(buffer) >= buffer_size:
469
+ write_buffer_to_csv(buffer, save_to_file, columns)
470
+ buffer = []
471
+
472
+ all_data = pd.concat(
473
+ [all_data, pd.DataFrame([data_row])], ignore_index=True
474
+ )
475
+
476
+ # Write remaining buffer to CSV file
477
+ if save_to_file and buffer:
478
+ write_buffer_to_csv(buffer, save_to_file, columns)
479
+
480
+ return all_data
481
+
482
+
483
+ def write_buffer_to_csv(buffer: List[Dict], output_path: str, columns: List[str]):
484
+ """Helper function to write buffer to CSV file."""
485
+ buffer_df = pd.DataFrame(buffer, columns=columns)
486
+ buffer_df.to_csv(
487
+ output_path,
488
+ mode="a",
489
+ header=(not os.path.exists(output_path)),
490
+ index=True,
491
+ )
492
+
493
+
494
+ def download_codon_frequencies_from_kazusa(
495
+ taxonomy_id: Optional[int] = None,
496
+ organism: Optional[str] = None,
497
+ taxonomy_reference: Optional[str] = None,
498
+ return_original_format: bool = False,
499
+ ) -> AMINO2CODON_TYPE:
500
+ """
501
+ Return the codon table of the given taxonomy ID from the Kazusa Database.
502
+
503
+ Args:
504
+ taxonomy_id (Optional[int]): Taxonomy ID.
505
+ organism (Optional[str]): Name of the organism.
506
+ taxonomy_reference (Optional[str]): Taxonomy reference.
507
+ return_original_format (bool): Whether to return in the original format.
508
+
509
+ Returns:
510
+ AMINO2CODON_TYPE: Codon table.
511
+ """
512
+ if taxonomy_reference:
513
+ taxonomy_id = get_taxonomy_id(taxonomy_reference, organism=organism)
514
+
515
+ kazusa_amino2codon = pct.get_codons_table(table_name=taxonomy_id)
516
+
517
+ if return_original_format:
518
+ return kazusa_amino2codon
519
+
520
+ # Replace "*" with STOP_SYMBOL in the codon table
521
+ kazusa_amino2codon[STOP_SYMBOL] = kazusa_amino2codon.pop("*")
522
+
523
+ # Create amino2codon dictionary
524
+ amino2codon = {
525
+ aminoacid: (list(codon2freq.keys()), list(codon2freq.values()))
526
+ for aminoacid, codon2freq in kazusa_amino2codon.items()
527
+ }
528
+
529
+ return sort_amino2codon_skeleton(amino2codon)
530
+
531
+
532
+ def build_amino2codon_skeleton(organism: str) -> AMINO2CODON_TYPE:
533
+ """
534
+ Return the empty skeleton of the amino2codon dictionary, needed for
535
+ get_codon_frequencies.
536
+
537
+ Args:
538
+ organism (str): Name of the organism.
539
+
540
+ Returns:
541
+ AMINO2CODON_TYPE: Empty amino2codon dictionary.
542
+ """
543
+ amino2codon = {}
544
+ possible_codons = [f"{i}{j}{k}" for i in "ACGT" for j in "ACGT" for k in "ACGT"]
545
+ possible_aminoacids = get_amino_acid_sequence(
546
+ dna="".join(possible_codons),
547
+ codon_table=get_codon_table(organism),
548
+ return_correct_seq=False,
549
+ )
550
+
551
+ # Initialize the amino2codon skeleton with all possible codons and set their
552
+ # frequencies to 0
553
+ for i, (codon, amino) in enumerate(zip(possible_codons, possible_aminoacids)):
554
+ if amino not in amino2codon:
555
+ amino2codon[amino] = ([], [])
556
+
557
+ amino2codon[amino][0].append(codon)
558
+ amino2codon[amino][1].append(0)
559
+
560
+ # Sort the dictionary and each list of codon frequency alphabetically
561
+ amino2codon = sort_amino2codon_skeleton(amino2codon)
562
+
563
+ return amino2codon
564
+
565
+
566
+ def get_codon_frequencies(
567
+ dna_sequences: List[str],
568
+ protein_sequences: Optional[List[str]] = None,
569
+ organism: Optional[str] = None,
570
+ ) -> AMINO2CODON_TYPE:
571
+ """
572
+ Return a dictionary mapping each codon to its respective frequency based on
573
+ the collection of DNA sequences and protein sequences.
574
+
575
+ Args:
576
+ dna_sequences (List[str]): List of DNA sequences.
577
+ protein_sequences (Optional[List[str]]): List of protein sequences.
578
+ organism (Optional[str]): Name of the organism.
579
+
580
+ Returns:
581
+ AMINO2CODON_TYPE: Dictionary mapping each amino acid to a tuple of codons
582
+ and frequencies.
583
+ """
584
+ if organism:
585
+ codon_table = get_codon_table(organism)
586
+ protein_sequences = [
587
+ get_amino_acid_sequence(
588
+ dna, codon_table=codon_table, return_correct_seq=False
589
+ )
590
+ for dna in dna_sequences
591
+ ]
592
+
593
+ amino2codon = build_amino2codon_skeleton(organism)
594
+
595
+ # Count the frequencies of each codon for each amino acid
596
+ for dna, protein in zip(dna_sequences, protein_sequences):
597
+ for i, amino in enumerate(protein):
598
+ codon = dna[i * 3 : (i + 1) * 3]
599
+ codon_loc = amino2codon[amino][0].index(codon)
600
+ amino2codon[amino][1][codon_loc] += 1
601
+
602
+ # Normalize codon frequencies per amino acid so they sum to 1
603
+ amino2codon = {
604
+ amino: (codons, [freq / (sum(frequencies) + 1e-100) for freq in frequencies])
605
+ for amino, (codons, frequencies) in amino2codon.items()
606
+ }
607
+
608
+ return amino2codon
609
+
610
+
611
+ def get_organism_to_codon_frequencies(
612
+ dataset: pd.DataFrame, organisms: List[str]
613
+ ) -> Dict[str, AMINO2CODON_TYPE]:
614
+ """
615
+ Return a dictionary mapping each organism to their codon frequency distribution.
616
+
617
+ Args:
618
+ dataset (pd.DataFrame): DataFrame containing DNA sequences.
619
+ organisms (List[str]): List of organisms.
620
+
621
+ Returns:
622
+ Dict[str, AMINO2CODON_TYPE]: Dictionary mapping each organism to its codon
623
+ frequency distribution.
624
+ """
625
+ organism2frequencies = {}
626
+
627
+ # Calculate codon frequencies for each organism in the dataset
628
+ for organism in tqdm(
629
+ organisms, desc="Calculating Codon Frequencies: ", unit="Organism"
630
+ ):
631
+ organism_data = dataset.loc[dataset["organism"] == organism]
632
+
633
+ dna_sequences = organism_data["dna"].to_list()
634
+ protein_sequences = organism_data["protein"].to_list()
635
+
636
+ codon_frequencies = get_codon_frequencies(dna_sequences, protein_sequences)
637
+ organism2frequencies[organism] = codon_frequencies
638
+
639
+ return organism2frequencies
640
+
641
+
642
+ def get_codon_table(organism: str) -> int:
643
+ """
644
+ Return the appropriate NCBI codon table for a given organism.
645
+
646
+ Args:
647
+ organism (str): Name of the organism.
648
+
649
+ Returns:
650
+ int: Codon table number.
651
+ """
652
+ # Common codon table (Table 1) for many model organisms
653
+ if organism in [
654
+ "Arabidopsis thaliana",
655
+ "Caenorhabditis elegans",
656
+ "Chlamydomonas reinhardtii",
657
+ "Saccharomyces cerevisiae",
658
+ "Danio rerio",
659
+ "Drosophila melanogaster",
660
+ "Homo sapiens",
661
+ "Mus musculus",
662
+ "Nicotiana tabacum",
663
+ "Solanum tuberosum",
664
+ "Solanum lycopersicum",
665
+ "Oryza sativa",
666
+ "Glycine max",
667
+ "Zea mays",
668
+ ]:
669
+ codon_table = 1
670
+
671
+ # Chloroplast codon table (Table 11)
672
+ elif organism in [
673
+ "Chlamydomonas reinhardtii chloroplast",
674
+ "Nicotiana tabacum chloroplast",
675
+ ]:
676
+ codon_table = 11
677
+
678
+ # Default to Table 11 for other bacteria and archaea
679
+ else:
680
+ codon_table = 11
681
+
682
+ return codon_table
model/CodonTransformer/CodonEvaluation.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: CodonEvaluation.py
3
+ ---------------------------
4
+ Includes functions to calculate various evaluation metrics along with helper
5
+ functions.
6
+ """
7
+
8
+ from typing import Dict, List, Tuple
9
+
10
+ import pandas as pd
11
+ from CAI import CAI, relative_adaptiveness
12
+ from tqdm import tqdm
13
+
14
+
15
+ def get_CSI_weights(sequences: List[str]) -> Dict[str, float]:
16
+ """
17
+ Calculate the Codon Similarity Index (CSI) weights for a list of DNA sequences.
18
+
19
+ Args:
20
+ sequences (List[str]): List of DNA sequences.
21
+
22
+ Returns:
23
+ dict: The CSI weights.
24
+ """
25
+ return relative_adaptiveness(sequences=sequences)
26
+
27
+
28
+ def get_CSI_value(dna: str, weights: Dict[str, float]) -> float:
29
+ """
30
+ Calculate the Codon Similarity Index (CSI) for a DNA sequence.
31
+
32
+ Args:
33
+ dna (str): The DNA sequence.
34
+ weights (dict): The CSI weights from get_CSI_weights.
35
+
36
+ Returns:
37
+ float: The CSI value.
38
+ """
39
+ return CAI(dna, weights)
40
+
41
+
42
+ def get_organism_to_CSI_weights(
43
+ dataset: pd.DataFrame, organisms: List[str]
44
+ ) -> Dict[str, dict]:
45
+ """
46
+ Calculate the Codon Similarity Index (CSI) weights for a list of organisms.
47
+
48
+ Args:
49
+ dataset (pd.DataFrame): Dataset containing organism and DNA sequence info.
50
+ organisms (List[str]): List of organism names.
51
+
52
+ Returns:
53
+ Dict[str, dict]: A dictionary mapping each organism to its CSI weights.
54
+ """
55
+ organism2weights = {}
56
+
57
+ # Iterate through each organism to calculate its CSI weights
58
+ for organism in tqdm(organisms, desc="Calculating CSI Weights: ", unit="Organism"):
59
+ organism_data = dataset.loc[dataset["organism"] == organism]
60
+ sequences = organism_data["dna"].to_list()
61
+ weights = get_CSI_weights(sequences)
62
+ organism2weights[organism] = weights
63
+
64
+ return organism2weights
65
+
66
+
67
+ def get_GC_content(dna: str, lower: bool = False) -> float:
68
+ """
69
+ Calculate the GC content of a DNA sequence.
70
+
71
+ Args:
72
+ dna (str): The DNA sequence.
73
+ lower (bool): If True, converts DNA sequence to lowercase before calculation.
74
+
75
+ Returns:
76
+ float: The GC content as a percentage.
77
+ """
78
+ if lower:
79
+ dna = dna.lower()
80
+ return (dna.count("G") + dna.count("C")) / len(dna) * 100
81
+
82
+
83
+ def get_cfd(
84
+ dna: str,
85
+ codon_frequencies: Dict[str, Tuple[List[str], List[float]]],
86
+ threshold: float = 0.3,
87
+ ) -> float:
88
+ """
89
+ Calculate the codon frequency distribution (CFD) metric for a DNA sequence.
90
+
91
+ Args:
92
+ dna (str): The DNA sequence.
93
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
94
+ frequency distribution per amino acid.
95
+ threshold (float): Frequency threshold for counting rare codons.
96
+
97
+ Returns:
98
+ float: The CFD metric as a percentage.
99
+ """
100
+ # Get a dictionary mapping each codon to its normalized frequency
101
+ codon2frequency = {
102
+ codon: freq / max(frequencies)
103
+ for amino, (codons, frequencies) in codon_frequencies.items()
104
+ for codon, freq in zip(codons, frequencies)
105
+ }
106
+
107
+ cfd = 0
108
+
109
+ # Iterate through the DNA sequence in steps of 3 to process each codon
110
+ for i in range(0, len(dna), 3):
111
+ codon = dna[i : i + 3]
112
+ codon_frequency = codon2frequency[codon]
113
+
114
+ if codon_frequency < threshold:
115
+ cfd += 1
116
+
117
+ return cfd / (len(dna) / 3) * 100
118
+
119
+
120
+ def get_min_max_percentage(
121
+ dna: str,
122
+ codon_frequencies: Dict[str, Tuple[List[str], List[float]]],
123
+ window_size: int = 18,
124
+ ) -> List[float]:
125
+ """
126
+ Calculate the %MinMax metric for a DNA sequence.
127
+
128
+ Args:
129
+ dna (str): The DNA sequence.
130
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
131
+ frequency distribution per amino acid.
132
+ window_size (int): Size of the window to calculate %MinMax.
133
+
134
+ Returns:
135
+ List[float]: List of %MinMax values for the sequence.
136
+
137
+ Credit: https://github.com/chowington/minmax
138
+ """
139
+ # Get a dictionary mapping each codon to its respective amino acid
140
+ codon2amino = {
141
+ codon: amino
142
+ for amino, (codons, frequencies) in codon_frequencies.items()
143
+ for codon in codons
144
+ }
145
+
146
+ min_max_values = []
147
+ codons = [dna[i : i + 3] for i in range(0, len(dna), 3)] # Split DNA into codons
148
+
149
+ # Iterate through the DNA sequence using the specified window size
150
+ for i in range(len(codons) - window_size + 1):
151
+ codon_window = codons[i : i + window_size] # Codons in the current window
152
+
153
+ Actual = 0.0 # Average of the actual codon frequencies
154
+ Max = 0.0 # Average of the min codon frequencies
155
+ Min = 0.0 # Average of the max codon frequencies
156
+ Avg = 0.0 # Average of the averages of all frequencies for each amino acid
157
+
158
+ # Sum the frequencies for codons in the current window
159
+ for codon in codon_window:
160
+ aminoacid = codon2amino[codon]
161
+ frequencies = codon_frequencies[aminoacid][1]
162
+ codon_index = codon_frequencies[aminoacid][0].index(codon)
163
+ codon_frequency = codon_frequencies[aminoacid][1][codon_index]
164
+
165
+ Actual += codon_frequency
166
+ Max += max(frequencies)
167
+ Min += min(frequencies)
168
+ Avg += sum(frequencies) / len(frequencies)
169
+
170
+ # Divide by the window size to get the averages
171
+ Actual = Actual / window_size
172
+ Max = Max / window_size
173
+ Min = Min / window_size
174
+ Avg = Avg / window_size
175
+
176
+ # Calculate %MinMax
177
+ percentMax = ((Actual - Avg) / (Max - Avg)) * 100
178
+ percentMin = ((Avg - Actual) / (Avg - Min)) * 100
179
+
180
+ # Append the appropriate %MinMax value
181
+ if percentMax >= 0:
182
+ min_max_values.append(percentMax)
183
+ else:
184
+ min_max_values.append(-percentMin)
185
+
186
+ # Populate the last floor(window_size / 2) entries of min_max_values with None
187
+ for i in range(int(window_size / 2)):
188
+ min_max_values.append(None)
189
+
190
+ return min_max_values
191
+
192
+
193
+ def get_sequence_complexity(dna: str) -> float:
194
+ """
195
+ Calculate the sequence complexity score of a DNA sequence.
196
+
197
+ Args:
198
+ dna (str): The DNA sequence.
199
+
200
+ Returns:
201
+ float: The sequence complexity score.
202
+ """
203
+
204
+ def sum_up_to(x):
205
+ """Recursive function to calculate the sum of integers from 1 to x."""
206
+ if x <= 1:
207
+ return 1
208
+ else:
209
+ return x + sum_up_to(x - 1)
210
+
211
+ def f(x):
212
+ """Returns 4 if x is greater than or equal to 4, else returns x."""
213
+ if x >= 4:
214
+ return 4
215
+ elif x < 4:
216
+ return x
217
+
218
+ unique_subseq_length = []
219
+
220
+ # Calculate unique subsequences lengths
221
+ for i in range(1, len(dna) + 1):
222
+ unique_subseq = set()
223
+ for j in range(len(dna) - (i - 1)):
224
+ unique_subseq.add(dna[j : (j + i)])
225
+ unique_subseq_length.append(len(unique_subseq))
226
+
227
+ # Calculate complexity score
228
+ complexity_score = (
229
+ sum(unique_subseq_length) / (sum_up_to(len(dna) - 1) + f(len(dna)))
230
+ ) * 100
231
+
232
+ return complexity_score
233
+
234
+
235
+ def get_sequence_similarity(
236
+ original: str, predicted: str, truncate: bool = True, window_length: int = 1
237
+ ) -> float:
238
+ """
239
+ Calculate the sequence similarity between two sequences.
240
+
241
+ Args:
242
+ original (str): The original sequence.
243
+ predicted (str): The predicted sequence.
244
+ truncate (bool): If True, truncate the original sequence to match the length
245
+ of the predicted sequence.
246
+ window_length (int): Length of the window for comparison (1 for amino acids,
247
+ 3 for codons).
248
+
249
+ Returns:
250
+ float: The sequence similarity as a percentage.
251
+
252
+ Preconditions:
253
+ len(predicted) <= len(original).
254
+ """
255
+ if not truncate and len(original) != len(predicted):
256
+ raise ValueError(
257
+ "Set truncate to True if the length of sequences do not match."
258
+ )
259
+
260
+ identity = 0.0
261
+ original = original.strip()
262
+ predicted = predicted.strip()
263
+
264
+ if truncate:
265
+ original = original[: len(predicted)]
266
+
267
+ if window_length == 1:
268
+ # Simple comparison for amino acid
269
+ for i in range(len(predicted)):
270
+ if original[i] == predicted[i]:
271
+ identity += 1
272
+ else:
273
+ # Comparison for substrings based on window_length
274
+ for i in range(0, len(original) - window_length + 1, window_length):
275
+ if original[i : i + window_length] == predicted[i : i + window_length]:
276
+ identity += 1
277
+
278
+ return (identity / (len(predicted) / window_length)) * 100
model/CodonTransformer/CodonJupyter.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: CodonJupyter.py
3
+ ---------------------
4
+ Includes Jupyter-specific functions for displaying interactive widgets.
5
+ """
6
+
7
+ from typing import Dict, List, Tuple
8
+
9
+ import ipywidgets as widgets
10
+ from IPython.display import HTML, display
11
+
12
+ from CodonTransformer.CodonUtils import (
13
+ COMMON_ORGANISMS,
14
+ ID2ORGANISM,
15
+ ORGANISM2ID,
16
+ DNASequencePrediction,
17
+ )
18
+
19
+
20
+ class UserContainer:
21
+ """
22
+ A container class to store user inputs for organism and protein sequence.
23
+ Attributes:
24
+ organism (int): The selected organism id.
25
+ protein (str): The input protein sequence.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ self.organism: int = -1
30
+ self.protein: str = ""
31
+
32
+
33
+ def create_styled_options(
34
+ organisms: list, organism2id: Dict[str, int], is_fine_tuned: bool = False
35
+ ) -> list:
36
+ """
37
+ Create styled options for the dropdown widget.
38
+
39
+ Args:
40
+ organisms (list): List of organism names.
41
+ organism2id (Dict[str, int]): Dictionary mapping organism names to their IDs.
42
+ is_fine_tuned (bool): Whether these are fine-tuned organisms.
43
+
44
+ Returns:
45
+ list: Styled options for the dropdown widget.
46
+ """
47
+ styled_options = []
48
+ for organism in organisms:
49
+ organism_id = organism2id[organism]
50
+ if is_fine_tuned:
51
+ if organism_id < 10:
52
+ styled_options.append(f"\u200b{organism_id:>6}. {organism}")
53
+ elif organism_id < 100:
54
+ styled_options.append(f"\u200b{organism_id:>5}. {organism}")
55
+ else:
56
+ styled_options.append(f"\u200b{organism_id:>4}. {organism}")
57
+ else:
58
+ if organism_id < 10:
59
+ styled_options.append(f"{organism_id:>6}. {organism}")
60
+ elif organism_id < 100:
61
+ styled_options.append(f"{organism_id:>5}. {organism}")
62
+ else:
63
+ styled_options.append(f"{organism_id:>4}. {organism}")
64
+ return styled_options
65
+
66
+
67
+ def create_dropdown_options(organism2id: Dict[str, int]) -> list:
68
+ """
69
+ Create the full list of dropdown options, including section headers.
70
+
71
+ Args:
72
+ organism2id (Dict[str, int]): Dictionary mapping organism names to their IDs.
73
+
74
+ Returns:
75
+ list: Full list of dropdown options.
76
+ """
77
+ fine_tuned_organisms = sorted(
78
+ [org for org in organism2id.keys() if org in COMMON_ORGANISMS]
79
+ )
80
+ all_organisms = sorted(organism2id.keys())
81
+
82
+ fine_tuned_options = create_styled_options(
83
+ fine_tuned_organisms, organism2id, is_fine_tuned=True
84
+ )
85
+ all_organisms_options = create_styled_options(
86
+ all_organisms, organism2id, is_fine_tuned=False
87
+ )
88
+
89
+ return (
90
+ [""]
91
+ + ["Selected Organisms"]
92
+ + fine_tuned_options
93
+ + [""]
94
+ + ["All Organisms"]
95
+ + all_organisms_options
96
+ )
97
+
98
+
99
+ def create_organism_dropdown(container: UserContainer) -> widgets.Dropdown:
100
+ """
101
+ Create and configure the organism dropdown widget.
102
+
103
+ Args:
104
+ container (UserContainer): Container to store the selected organism.
105
+
106
+ Returns:
107
+ widgets.Dropdown: Configured dropdown widget.
108
+ """
109
+ dropdown = widgets.Dropdown(
110
+ options=create_dropdown_options(ORGANISM2ID),
111
+ description="",
112
+ layout=widgets.Layout(width="40%", margin="0 0 10px 0"),
113
+ style={"description_width": "initial"},
114
+ )
115
+
116
+ def show_organism(change: Dict[str, str]) -> None:
117
+ """
118
+ Update the container with the selected organism and print to terminal.
119
+
120
+ Args:
121
+ change (Dict[str, str]): Information about the change in dropdown value.
122
+ """
123
+ dropdown_choice = change["new"]
124
+ if dropdown_choice and dropdown_choice not in [
125
+ "Selected Organisms",
126
+ "All Organisms",
127
+ ]:
128
+ organism = "".join(filter(str.isdigit, dropdown_choice))
129
+ organism_id = ID2ORGANISM[int(organism)]
130
+ container.organism = organism_id
131
+ else:
132
+ container.organism = None
133
+
134
+ dropdown.observe(show_organism, names="value")
135
+ return dropdown
136
+
137
+
138
+ def get_dropdown_style() -> str:
139
+ """
140
+ Return the custom CSS style for the dropdown widget.
141
+
142
+ Returns:
143
+ str: CSS style string.
144
+ """
145
+ return """
146
+ <style>
147
+ .widget-dropdown > select {
148
+ font-size: 16px;
149
+ font-weight: normal;
150
+ background-color: #f0f0f0;
151
+ border-radius: 5px;
152
+ padding: 5px;
153
+ }
154
+ .widget-label {
155
+ font-size: 18px;
156
+ font-weight: bold;
157
+ }
158
+ .custom-container {
159
+ display: flex;
160
+ flex-direction: column;
161
+ align-items: flex-start;
162
+ }
163
+ .widget-dropdown option[value^="\u200b"] {
164
+ font-family: sans-serif;
165
+ font-weight: bold;
166
+ font-size: 18px;
167
+ padding: 510px;
168
+ }
169
+ .widget-dropdown option[value*="Selected Organisms"],
170
+ .widget-dropdown option[value*="All Organisms"] {
171
+ text-align: center;
172
+ font-family: Arial, sans-serif;
173
+ font-weight: bold;
174
+ font-size: 20px;
175
+ color: #6900A1;
176
+ background-color: #00D8A1;
177
+ }
178
+ </style>
179
+ """
180
+
181
+
182
+ def display_organism_dropdown(container: UserContainer) -> None:
183
+ """
184
+ Display the organism dropdown widget and apply custom styles.
185
+
186
+ Args:
187
+ container (UserContainer): Container to store the selected organism.
188
+ """
189
+ dropdown = create_organism_dropdown(container)
190
+ header = widgets.HTML(
191
+ '<b style="font-size:20px;">Select Organism:</b>'
192
+ '<div style="height:10px;"></div>'
193
+ )
194
+ container_widget = widgets.VBox(
195
+ [header, dropdown],
196
+ layout=widgets.Layout(padding="12px 0 12px 25px"),
197
+ )
198
+ display(container_widget)
199
+ display(HTML(get_dropdown_style()))
200
+
201
+
202
+ def display_protein_input(container: UserContainer) -> None:
203
+ """
204
+ Display a widget for entering a protein sequence and save it to the container.
205
+
206
+ Args:
207
+ container (UserContainer): A container to store the entered protein sequence.
208
+ """
209
+ protein_input = widgets.Textarea(
210
+ value="",
211
+ placeholder="Enter here...",
212
+ description="",
213
+ layout=widgets.Layout(width="100%", height="100px", margin="0 0 10px 0"),
214
+ style={"description_width": "initial"},
215
+ )
216
+
217
+ # Custom CSS for the input widget
218
+ input_style = """
219
+ <style>
220
+ .widget-textarea > textarea {
221
+ font-size: 12px;
222
+ font-family: Arial, sans-serif;
223
+ font-weight: normal;
224
+ background-color: #f0f0f0;
225
+ border-radius: 5px;
226
+ padding: 10px;
227
+ }
228
+ .widget-label {
229
+ font-size: 18px;
230
+ font-weight: bold;
231
+ }
232
+ .custom-container {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: flex-start;
236
+ }
237
+ </style>
238
+ """
239
+
240
+ # Function to save the input protein sequence to the container
241
+ def save_protein(change: Dict[str, str]) -> None:
242
+ """
243
+ Save the input protein sequence to the container.
244
+
245
+ Args:
246
+ change (Dict[str, str]): A dictionary containing information about
247
+ the change in textarea value.
248
+ """
249
+ container.protein = (
250
+ change["new"]
251
+ .upper()
252
+ .strip()
253
+ .replace("\n", "")
254
+ .replace(" ", "")
255
+ .replace("\t", "")
256
+ )
257
+
258
+ # Attach the function to the input widget
259
+ protein_input.observe(save_protein, names="value")
260
+
261
+ # Display the input widget
262
+ header = widgets.HTML(
263
+ '<b style="font-size:20px;">Enter Protein Sequence:</b>'
264
+ '<div style="height:18px;"></div>'
265
+ )
266
+ container_widget = widgets.VBox(
267
+ [header, protein_input], layout=widgets.Layout(padding="12px 12px 0 25px")
268
+ )
269
+
270
+ display(container_widget)
271
+ display(widgets.HTML(input_style))
272
+
273
+
274
+ def format_model_output(output: DNASequencePrediction) -> str:
275
+ """
276
+ Format DNA sequence prediction output in an appealing and easy-to-read manner.
277
+
278
+ This function takes the prediction output and formats it into
279
+ a structured string with clear section headers and separators.
280
+
281
+ Args:
282
+ output (DNASequencePrediction): Object containing the prediction output.
283
+ Expected attributes:
284
+ - organism (str): The organism name.
285
+ - protein (str): The input protein sequence.
286
+ - processed_input (str): The processed input sequence.
287
+ - predicted_dna (str): The predicted DNA sequence.
288
+
289
+ Returns:
290
+ str: A formatted string containing the organized output.
291
+ """
292
+
293
+ def format_section(title: str, content: str) -> str:
294
+ """Helper function to format individual sections."""
295
+ separator = "-" * 29
296
+ title_line = f"| {title.center(25)} |"
297
+ return f"{separator}\n{title_line}\n{separator}\n{content}\n\n"
298
+
299
+ sections: List[Tuple[str, str]] = [
300
+ ("Organism", output.organism),
301
+ ("Input Protein", output.protein),
302
+ ("Processed Input", output.processed_input),
303
+ ("Predicted DNA", output.predicted_dna),
304
+ ]
305
+
306
+ formatted_output = ""
307
+ for title, content in sections:
308
+ formatted_output += format_section(title, content)
309
+
310
+ # Remove the last newline to avoid extra space at the end
311
+ return formatted_output.rstrip()
model/CodonTransformer/CodonPrediction.py ADDED
@@ -0,0 +1,855 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: CodonPrediction.py
3
+ ---------------------------
4
+ Includes functions to tokenize input, load models, infer predicted dna sequences and
5
+ helper functions related to processing data for passing to the model.
6
+ """
7
+
8
+ import warnings
9
+ from typing import Any, Dict, List, Optional, Tuple, Union
10
+
11
+ import numpy as np
12
+ import onnxruntime as rt
13
+ import torch
14
+ import transformers
15
+ from transformers import (
16
+ AutoTokenizer,
17
+ BatchEncoding,
18
+ BigBirdConfig,
19
+ BigBirdForMaskedLM,
20
+ PreTrainedTokenizerFast,
21
+ )
22
+
23
+ from CodonTransformer.CodonData import get_merged_seq
24
+ from CodonTransformer.CodonUtils import (
25
+ AMINO_ACID_TO_INDEX,
26
+ INDEX2TOKEN,
27
+ NUM_ORGANISMS,
28
+ ORGANISM2ID,
29
+ TOKEN2INDEX,
30
+ DNASequencePrediction,
31
+ )
32
+
33
+
34
+ def predict_dna_sequence(
35
+ protein: str,
36
+ organism: Union[int, str],
37
+ device: torch.device,
38
+ tokenizer: Union[str, PreTrainedTokenizerFast] = None,
39
+ model: Union[str, torch.nn.Module] = None,
40
+ attention_type: str = "original_full",
41
+ deterministic: bool = True,
42
+ temperature: float = 0.2,
43
+ top_p: float = 0.95,
44
+ num_sequences: int = 1,
45
+ match_protein: bool = False,
46
+ ) -> Union[DNASequencePrediction, List[DNASequencePrediction]]:
47
+ """
48
+ Predict the DNA sequence(s) for a given protein using the CodonTransformer model.
49
+
50
+ This function takes a protein sequence and an organism (as ID or name) as input
51
+ and returns the predicted DNA sequence(s) using the CodonTransformer model. It can use
52
+ either provided tokenizer and model objects or load them from specified paths.
53
+
54
+ Args:
55
+ protein (str): The input protein sequence for which to predict the DNA sequence.
56
+ organism (Union[int, str]): Either the ID of the organism or its name (e.g.,
57
+ "Escherichia coli general"). If a string is provided, it will be converted
58
+ to the corresponding ID using ORGANISM2ID.
59
+ device (torch.device): The device (CPU or GPU) to run the model on.
60
+ tokenizer (Union[str, PreTrainedTokenizerFast, None], optional): Either a file
61
+ path to load the tokenizer from, a pre-loaded tokenizer object, or None. If
62
+ None, it will be loaded from HuggingFace. Defaults to None.
63
+ model (Union[str, torch.nn.Module, None], optional): Either a file path to load
64
+ the model from, a pre-loaded model object, or None. If None, it will be
65
+ loaded from HuggingFace. Defaults to None.
66
+ attention_type (str, optional): The type of attention mechanism to use in the
67
+ model. Can be either 'block_sparse' or 'original_full'. Defaults to
68
+ "original_full".
69
+ deterministic (bool, optional): Whether to use deterministic decoding (most
70
+ likely tokens). If False, samples tokens according to their probabilities
71
+ adjusted by the temperature. Defaults to True.
72
+ temperature (float, optional): A value controlling the randomness of predictions
73
+ during non-deterministic decoding. Lower values (e.g., 0.2) make the model
74
+ more conservative, while higher values (e.g., 0.8) increase randomness.
75
+ Using high temperatures may result in prediction of DNA sequences that
76
+ do not translate to the input protein.
77
+ Recommended values are:
78
+ - Low randomness: 0.2
79
+ - Medium randomness: 0.5
80
+ - High randomness: 0.8
81
+ The temperature must be a positive float. Defaults to 0.2.
82
+ top_p (float, optional): The cumulative probability threshold for nucleus sampling.
83
+ Tokens with cumulative probability up to top_p are considered for sampling.
84
+ This parameter helps balance diversity and coherence in the predicted DNA sequences.
85
+ The value must be a float between 0 and 1. Defaults to 0.95.
86
+ num_sequences (int, optional): The number of DNA sequences to generate. Only applicable
87
+ when deterministic is False. Defaults to 1.
88
+ match_protein (bool, optional): Ensures the predicted DNA sequence is translated
89
+ to the input protein sequence by sampling from only the respective codons of
90
+ given amino acids. Defaults to False.
91
+
92
+ Returns:
93
+ Union[DNASequencePrediction, List[DNASequencePrediction]]: An object or list of objects
94
+ containing the prediction results:
95
+ - organism (str): Name of the organism used for prediction.
96
+ - protein (str): Input protein sequence for which DNA sequence is predicted.
97
+ - processed_input (str): Processed input sequence (merged protein and DNA).
98
+ - predicted_dna (str): Predicted DNA sequence.
99
+
100
+ Raises:
101
+ ValueError: If the protein sequence is empty, if the organism is invalid,
102
+ if the temperature is not a positive float, if top_p is not between 0 and 1,
103
+ or if num_sequences is less than 1 or used with deterministic mode.
104
+
105
+ Note:
106
+ This function uses ORGANISM2ID, INDEX2TOKEN, and AMINO_ACID_TO_INDEX dictionaries
107
+ imported from CodonTransformer.CodonUtils. ORGANISM2ID maps organism names to their
108
+ corresponding IDs. INDEX2TOKEN maps model output indices (token IDs) to
109
+ respective codons. AMINO_ACID_TO_INDEX maps each amino acid and stop symbol to indices
110
+ of codon tokens that translate to it.
111
+
112
+ Example:
113
+ >>> import torch
114
+ >>> from transformers import AutoTokenizer, BigBirdForMaskedLM
115
+ >>> from CodonTransformer.CodonPrediction import predict_dna_sequence
116
+ >>> from CodonTransformer.CodonJupyter import format_model_output
117
+ >>>
118
+ >>> # Set up device
119
+ >>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
120
+ >>>
121
+ >>> # Load tokenizer and model
122
+ >>> tokenizer = AutoTokenizer.from_pretrained("adibvafa/CodonTransformer")
123
+ >>> model = BigBirdForMaskedLM.from_pretrained("adibvafa/CodonTransformer")
124
+ >>> model = model.to(device)
125
+ >>>
126
+ >>> # Define protein sequence and organism
127
+ >>> protein = "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLA"
128
+ >>> organism = "Escherichia coli general"
129
+ >>>
130
+ >>> # Predict DNA sequence with deterministic decoding (single sequence)
131
+ >>> output = predict_dna_sequence(
132
+ ... protein=protein,
133
+ ... organism=organism,
134
+ ... device=device,
135
+ ... tokenizer=tokenizer,
136
+ ... model=model,
137
+ ... attention_type="original_full",
138
+ ... deterministic=True
139
+ ... )
140
+ >>>
141
+ >>> # Predict multiple DNA sequences with low randomness and top_p sampling
142
+ >>> output_random = predict_dna_sequence(
143
+ ... protein=protein,
144
+ ... organism=organism,
145
+ ... device=device,
146
+ ... tokenizer=tokenizer,
147
+ ... model=model,
148
+ ... attention_type="original_full",
149
+ ... deterministic=False,
150
+ ... temperature=0.2,
151
+ ... top_p=0.95,
152
+ ... num_sequences=3
153
+ ... )
154
+ >>>
155
+ >>> print(format_model_output(output))
156
+ >>> for i, seq in enumerate(output_random, 1):
157
+ ... print(f"Sequence {i}:")
158
+ ... print(format_model_output(seq))
159
+ ... print()
160
+ """
161
+ if not protein:
162
+ raise ValueError("Protein sequence cannot be empty.")
163
+
164
+ if not isinstance(temperature, (float, int)) or temperature <= 0:
165
+ raise ValueError("Temperature must be a positive float.")
166
+
167
+ if not isinstance(top_p, (float, int)) or not 0 < top_p <= 1.0:
168
+ raise ValueError("top_p must be a float between 0 and 1.")
169
+
170
+ if not isinstance(num_sequences, int) or num_sequences < 1:
171
+ raise ValueError("num_sequences must be a positive integer.")
172
+
173
+ if deterministic and num_sequences > 1:
174
+ raise ValueError(
175
+ "Multiple sequences can only be generated in non-deterministic mode."
176
+ )
177
+
178
+ # Load tokenizer
179
+ if not isinstance(tokenizer, PreTrainedTokenizerFast):
180
+ tokenizer = load_tokenizer(tokenizer)
181
+
182
+ # Load model
183
+ if not isinstance(model, torch.nn.Module):
184
+ model = load_model(model, device=device, attention_type=attention_type)
185
+ else:
186
+ model.eval()
187
+ model.bert.set_attention_type(attention_type)
188
+ model.to(device)
189
+
190
+ # Validate organism and convert to organism_id and organism_name
191
+ organism_id, organism_name = validate_and_convert_organism(organism)
192
+
193
+ # Inference loop
194
+ with torch.no_grad():
195
+ # Tokenize the input sequence
196
+ merged_seq = get_merged_seq(protein=protein, dna="")
197
+ input_dict = {
198
+ "idx": 0, # sample index
199
+ "codons": merged_seq,
200
+ "organism": organism_id,
201
+ }
202
+ tokenized_input = tokenize([input_dict], tokenizer=tokenizer).to(device)
203
+
204
+ # Get the model predictions
205
+ output_dict = model(**tokenized_input, return_dict=True)
206
+ logits = output_dict.logits.detach().cpu()
207
+ logits = logits[:, 1:-1, :] # Remove [CLS] and [SEP] tokens
208
+
209
+ # Mask the logits of codons that do not correspond to the input protein sequence
210
+ if match_protein:
211
+ possible_tokens_per_position = [
212
+ AMINO_ACID_TO_INDEX[token[0]] for token in merged_seq.split(" ")
213
+ ]
214
+ mask = torch.full_like(logits, float("-inf"))
215
+
216
+ for pos, possible_tokens in enumerate(possible_tokens_per_position):
217
+ mask[:, pos, possible_tokens] = 0
218
+
219
+ logits = mask + logits
220
+
221
+ predictions = []
222
+ for _ in range(num_sequences):
223
+ # Decode the predicted DNA sequence from the model output
224
+ if deterministic:
225
+ predicted_indices = logits.argmax(dim=-1).squeeze().tolist()
226
+ else:
227
+ predicted_indices = sample_non_deterministic(
228
+ logits=logits, temperature=temperature, top_p=top_p
229
+ )
230
+
231
+ predicted_dna = list(map(INDEX2TOKEN.__getitem__, predicted_indices))
232
+ predicted_dna = (
233
+ "".join([token[-3:] for token in predicted_dna]).strip().upper()
234
+ )
235
+
236
+ predictions.append(
237
+ DNASequencePrediction(
238
+ organism=organism_name,
239
+ protein=protein,
240
+ processed_input=merged_seq,
241
+ predicted_dna=predicted_dna,
242
+ )
243
+ )
244
+
245
+ return predictions[0] if num_sequences == 1 else predictions
246
+
247
+
248
+ def sample_non_deterministic(
249
+ logits: torch.Tensor,
250
+ temperature: float = 0.2,
251
+ top_p: float = 0.95,
252
+ ) -> List[int]:
253
+ """
254
+ Sample token indices from logits using temperature scaling and nucleus (top-p) sampling.
255
+
256
+ This function applies temperature scaling to the logits, computes probabilities,
257
+ and then performs nucleus sampling to select token indices. It is used for
258
+ non-deterministic decoding in language models to introduce randomness while
259
+ maintaining coherence in the generated sequences.
260
+
261
+ Args:
262
+ logits (torch.Tensor): The logits output from the model of shape
263
+ [seq_len, vocab_size] or [batch_size, seq_len, vocab_size].
264
+ temperature (float, optional): Temperature value for scaling logits.
265
+ Must be a positive float. Defaults to 1.0.
266
+ top_p (float, optional): Cumulative probability threshold for nucleus sampling.
267
+ Must be a float between 0 and 1. Tokens with cumulative probability up to
268
+ `top_p` are considered for sampling. Defaults to 0.95.
269
+
270
+ Returns:
271
+ List[int]: A list of sampled token indices corresponding to the predicted tokens.
272
+
273
+ Raises:
274
+ ValueError: If `temperature` is not a positive float or if `top_p` is not between 0 and 1.
275
+
276
+ Example:
277
+ >>> logits = model_output.logits # Assume logits is a tensor of shape [seq_len, vocab_size]
278
+ >>> predicted_indices = sample_non_deterministic(logits, temperature=0.7, top_p=0.9)
279
+ """
280
+ if not isinstance(temperature, (float, int)) or temperature <= 0:
281
+ raise ValueError("Temperature must be a positive float.")
282
+
283
+ if not isinstance(top_p, (float, int)) or not 0 < top_p <= 1.0:
284
+ raise ValueError("top_p must be a float between 0 and 1.")
285
+
286
+ # Compute probabilities using temperature scaling
287
+ probs = torch.softmax(logits / temperature, dim=-1)
288
+
289
+
290
+ # Remove batch dimension if present
291
+ if probs.dim() == 3:
292
+ probs = probs.squeeze(0) # Shape: [seq_len, vocab_size]
293
+
294
+ # Sort probabilities in descending order
295
+ probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
296
+ probs_sum = torch.cumsum(probs_sort, dim=-1)
297
+ mask = probs_sum - probs_sort > top_p
298
+
299
+ # Zero out probabilities for tokens beyond the top-p threshold
300
+ probs_sort[mask] = 0.0
301
+
302
+ # Renormalize the probabilities
303
+ probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
304
+ next_token = torch.multinomial(probs_sort, num_samples=1)
305
+ predicted_indices = torch.gather(probs_idx, -1, next_token).squeeze(-1)
306
+
307
+ return predicted_indices.tolist()
308
+
309
+
310
+ def load_model(
311
+ model_path: Optional[str] = None,
312
+ device: torch.device = None,
313
+ attention_type: str = "original_full",
314
+ num_organisms: int = None,
315
+ remove_prefix: bool = True,
316
+ ) -> torch.nn.Module:
317
+ """
318
+ Load a BigBirdForMaskedLM model from a model file, checkpoint, or HuggingFace.
319
+
320
+ Args:
321
+ model_path (Optional[str]): Path to the model file or checkpoint. If None,
322
+ load from HuggingFace.
323
+ device (torch.device, optional): The device to load the model onto.
324
+ attention_type (str, optional): The type of attention, 'block_sparse'
325
+ or 'original_full'.
326
+ num_organisms (int, optional): Number of organisms, needed if loading from a
327
+ checkpoint that requires this.
328
+ remove_prefix (bool, optional): Whether to remove the "model." prefix from the
329
+ keys in the state dict.
330
+
331
+ Returns:
332
+ torch.nn.Module: The loaded model.
333
+ """
334
+ if not model_path:
335
+ warnings.warn("Model path not provided. Loading from HuggingFace.", UserWarning)
336
+ model = BigBirdForMaskedLM.from_pretrained("adibvafa/CodonTransformer")
337
+
338
+ elif model_path.endswith(".ckpt"):
339
+ checkpoint = torch.load(model_path)
340
+ state_dict = checkpoint["state_dict"]
341
+
342
+ # Remove the "model." prefix from the keys
343
+ if remove_prefix:
344
+ state_dict = {
345
+ key.replace("model.", ""): value for key, value in state_dict.items()
346
+ }
347
+
348
+ if num_organisms is None:
349
+ num_organisms = NUM_ORGANISMS
350
+
351
+ # Load model configuration and instantiate the model
352
+ config = load_bigbird_config(num_organisms)
353
+ model = BigBirdForMaskedLM(config=config)
354
+ model.load_state_dict(state_dict)
355
+
356
+ elif model_path.endswith(".pt"):
357
+ state_dict = torch.load(model_path)
358
+ config = state_dict.pop("self.config")
359
+ model = BigBirdForMaskedLM(config=config)
360
+ model.load_state_dict(state_dict)
361
+
362
+ else:
363
+ raise ValueError(
364
+ "Unsupported file type. Please provide a .ckpt or .pt file, "
365
+ "or None to load from HuggingFace."
366
+ )
367
+
368
+ # Prepare model for evaluation
369
+ model.bert.set_attention_type(attention_type)
370
+ model.eval()
371
+ if device:
372
+ model.to(device)
373
+
374
+ return model
375
+
376
+
377
+ def load_bigbird_config(num_organisms: int) -> BigBirdConfig:
378
+ """
379
+ Load the config object used to train the BigBird transformer.
380
+
381
+ Args:
382
+ num_organisms (int): The number of organisms.
383
+
384
+ Returns:
385
+ BigBirdConfig: The configuration object for BigBird.
386
+ """
387
+ config = transformers.BigBirdConfig(
388
+ vocab_size=len(TOKEN2INDEX), # Equal to len(tokenizer)
389
+ type_vocab_size=num_organisms,
390
+ sep_token_id=2,
391
+ )
392
+ return config
393
+
394
+
395
+ def create_model_from_checkpoint(
396
+ checkpoint_dir: str, output_model_dir: str, num_organisms: int
397
+ ) -> None:
398
+ """
399
+ Save a model to disk using a previous checkpoint.
400
+
401
+ Args:
402
+ checkpoint_dir (str): Directory where the checkpoint is stored.
403
+ output_model_dir (str): Directory where the model will be saved.
404
+ num_organisms (int): Number of organisms.
405
+ """
406
+ checkpoint = load_model(model_path=checkpoint_dir, num_organisms=num_organisms)
407
+ state_dict = checkpoint.state_dict()
408
+ state_dict["self.config"] = load_bigbird_config(num_organisms=num_organisms)
409
+
410
+ # Save the model state dict to the output directory
411
+ torch.save(state_dict, output_model_dir)
412
+
413
+
414
+ def load_tokenizer(tokenizer_path: Optional[str] = None) -> PreTrainedTokenizerFast:
415
+ """
416
+ Create and return a tokenizer object from tokenizer path or HuggingFace.
417
+
418
+ Args:
419
+ tokenizer_path (Optional[str]): Path to the tokenizer file. If None,
420
+ load from HuggingFace.
421
+
422
+ Returns:
423
+ PreTrainedTokenizerFast: The tokenizer object.
424
+ """
425
+ if not tokenizer_path:
426
+ warnings.warn(
427
+ "Tokenizer path not provided. Loading from HuggingFace.", UserWarning
428
+ )
429
+ return AutoTokenizer.from_pretrained("adibvafa/CodonTransformer")
430
+
431
+ return transformers.PreTrainedTokenizerFast(
432
+ tokenizer_file=tokenizer_path,
433
+ bos_token="[CLS]",
434
+ eos_token="[SEP]",
435
+ unk_token="[UNK]",
436
+ sep_token="[SEP]",
437
+ pad_token="[PAD]",
438
+ cls_token="[CLS]",
439
+ mask_token="[MASK]",
440
+ )
441
+
442
+
443
+ def tokenize(
444
+ batch: List[Dict[str, Any]],
445
+ tokenizer: Union[PreTrainedTokenizerFast, str] = None,
446
+ max_len: int = 2048,
447
+ ) -> BatchEncoding:
448
+ """
449
+ Return the tokenized sequences given a batch of input data.
450
+ Each data in the batch is expected to be a dictionary with "codons" and
451
+ "organism" keys.
452
+
453
+ Args:
454
+ batch (List[Dict[str, Any]]): A list of dictionaries with "codons" and
455
+ "organism" keys.
456
+ tokenizer (PreTrainedTokenizerFast, str, optional): The tokenizer object or
457
+ path to the tokenizer file.
458
+ max_len (int, optional): Maximum length of the tokenized sequence.
459
+
460
+ Returns:
461
+ BatchEncoding: The tokenized batch.
462
+ """
463
+ if not isinstance(tokenizer, PreTrainedTokenizerFast):
464
+ tokenizer = load_tokenizer(tokenizer)
465
+
466
+ tokenized = tokenizer(
467
+ [data["codons"] for data in batch],
468
+ return_attention_mask=True,
469
+ return_token_type_ids=True,
470
+ truncation=True,
471
+ padding=True,
472
+ max_length=max_len,
473
+ return_tensors="pt",
474
+ )
475
+
476
+ # Add token type IDs for species
477
+ seq_len = tokenized["input_ids"].shape[-1]
478
+ species_index = torch.tensor([[data["organism"]] for data in batch])
479
+ tokenized["token_type_ids"] = species_index.repeat(1, seq_len)
480
+
481
+ return tokenized
482
+
483
+
484
+ def validate_and_convert_organism(organism: Union[int, str]) -> Tuple[int, str]:
485
+ """
486
+ Validate and convert the organism input to both ID and name.
487
+
488
+ This function takes either an organism ID or name as input and returns both
489
+ the ID and name. It performs validation to ensure the input corresponds to
490
+ a valid organism in the ORGANISM2ID dictionary.
491
+
492
+ Args:
493
+ organism (Union[int, str]): Either the ID of the organism (int) or its
494
+ name (str).
495
+
496
+ Returns:
497
+ Tuple[int, str]: A tuple containing the organism ID (int) and name (str).
498
+
499
+ Raises:
500
+ ValueError: If the input is neither a string nor an integer, if the
501
+ organism name is not found in ORGANISM2ID, if the organism ID is not a
502
+ value in ORGANISM2ID, or if no name is found for a given ID.
503
+
504
+ Note:
505
+ This function relies on the ORGANISM2ID dictionary imported from
506
+ CodonTransformer.CodonUtils, which maps organism names to their
507
+ corresponding IDs.
508
+ """
509
+ if isinstance(organism, str):
510
+ if organism not in ORGANISM2ID:
511
+ raise ValueError(
512
+ f"Invalid organism name: {organism}. "
513
+ "Please use a valid organism name or ID."
514
+ )
515
+ organism_id = ORGANISM2ID[organism]
516
+ organism_name = organism
517
+
518
+ elif isinstance(organism, int):
519
+ if organism not in ORGANISM2ID.values():
520
+ raise ValueError(
521
+ f"Invalid organism ID: {organism}. "
522
+ "Please use a valid organism name or ID."
523
+ )
524
+
525
+ organism_id = organism
526
+ organism_name = next(
527
+ (name for name, id in ORGANISM2ID.items() if id == organism), None
528
+ )
529
+ if organism_name is None:
530
+ raise ValueError(f"No organism name found for ID: {organism}")
531
+
532
+ return organism_id, organism_name
533
+
534
+
535
+ def get_high_frequency_choice_sequence(
536
+ protein: str, codon_frequencies: Dict[str, Tuple[List[str], List[float]]]
537
+ ) -> str:
538
+ """
539
+ Return the DNA sequence optimized using High Frequency Choice (HFC) approach
540
+ in which the most frequent codon for a given amino acid is always chosen.
541
+
542
+ Args:
543
+ protein (str): The protein sequence.
544
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
545
+ frequencies for each amino acid.
546
+
547
+ Returns:
548
+ str: The optimized DNA sequence.
549
+ """
550
+ # Select the most frequent codon for each amino acid in the protein sequence
551
+ dna_codons = [
552
+ codon_frequencies[aminoacid][0][np.argmax(codon_frequencies[aminoacid][1])]
553
+ for aminoacid in protein
554
+ ]
555
+ return "".join(dna_codons)
556
+
557
+
558
+ def precompute_most_frequent_codons(
559
+ codon_frequencies: Dict[str, Tuple[List[str], List[float]]],
560
+ ) -> Dict[str, str]:
561
+ """
562
+ Precompute the most frequent codon for each amino acid.
563
+
564
+ Args:
565
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
566
+ frequencies for each amino acid.
567
+
568
+ Returns:
569
+ Dict[str, str]: The most frequent codon for each amino acid.
570
+ """
571
+ # Create a dictionary mapping each amino acid to its most frequent codon
572
+ return {
573
+ aminoacid: codons[np.argmax(frequencies)]
574
+ for aminoacid, (codons, frequencies) in codon_frequencies.items()
575
+ }
576
+
577
+
578
+ def get_high_frequency_choice_sequence_optimized(
579
+ protein: str, codon_frequencies: Dict[str, Tuple[List[str], List[float]]]
580
+ ) -> str:
581
+ """
582
+ Efficient implementation of get_high_frequency_choice_sequence that uses
583
+ vectorized operations and helper functions, achieving up to x10 faster speed.
584
+
585
+ Args:
586
+ protein (str): The protein sequence.
587
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
588
+ frequencies for each amino acid.
589
+
590
+ Returns:
591
+ str: The optimized DNA sequence.
592
+ """
593
+ # Precompute the most frequent codons for each amino acid
594
+ most_frequent_codons = precompute_most_frequent_codons(codon_frequencies)
595
+
596
+ return "".join(most_frequent_codons[aminoacid] for aminoacid in protein)
597
+
598
+
599
+ def get_background_frequency_choice_sequence(
600
+ protein: str, codon_frequencies: Dict[str, Tuple[List[str], List[float]]]
601
+ ) -> str:
602
+ """
603
+ Return the DNA sequence optimized using Background Frequency Choice (BFC)
604
+ approach in which a random codon for a given amino acid is chosen using
605
+ the codon frequencies probability distribution.
606
+
607
+ Args:
608
+ protein (str): The protein sequence.
609
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
610
+ frequencies for each amino acid.
611
+
612
+ Returns:
613
+ str: The optimized DNA sequence.
614
+ """
615
+ # Select a random codon for each amino acid based on the codon frequencies
616
+ # probability distribution
617
+ dna_codons = [
618
+ np.random.choice(
619
+ codon_frequencies[aminoacid][0], p=codon_frequencies[aminoacid][1]
620
+ )
621
+ for aminoacid in protein
622
+ ]
623
+ return "".join(dna_codons)
624
+
625
+
626
+ def precompute_cdf(
627
+ codon_frequencies: Dict[str, Tuple[List[str], List[float]]],
628
+ ) -> Dict[str, Tuple[List[str], Any]]:
629
+ """
630
+ Precompute the cumulative distribution function (CDF) for each amino acid.
631
+
632
+ Args:
633
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
634
+ frequencies for each amino acid.
635
+
636
+ Returns:
637
+ Dict[str, Tuple[List[str], Any]]: CDFs for each amino acid.
638
+ """
639
+ cdf = {}
640
+
641
+ # Calculate the cumulative distribution function for each amino acid
642
+ for aminoacid, (codons, frequencies) in codon_frequencies.items():
643
+ cdf[aminoacid] = (codons, np.cumsum(frequencies))
644
+
645
+ return cdf
646
+
647
+
648
+ def get_background_frequency_choice_sequence_optimized(
649
+ protein: str, codon_frequencies: Dict[str, Tuple[List[str], List[float]]]
650
+ ) -> str:
651
+ """
652
+ Efficient implementation of get_background_frequency_choice_sequence that uses
653
+ vectorized operations and helper functions, achieving up to x8 faster speed.
654
+
655
+ Args:
656
+ protein (str): The protein sequence.
657
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
658
+ frequencies for each amino acid.
659
+
660
+ Returns:
661
+ str: The optimized DNA sequence.
662
+ """
663
+ dna_codons = []
664
+ cdf = precompute_cdf(codon_frequencies)
665
+
666
+ # Select a random codon for each amino acid using the precomputed CDFs
667
+ for aminoacid in protein:
668
+ codons, cumulative_prob = cdf[aminoacid]
669
+ selected_codon_index = np.searchsorted(cumulative_prob, np.random.rand())
670
+ dna_codons.append(codons[selected_codon_index])
671
+
672
+ return "".join(dna_codons)
673
+
674
+
675
+ def get_uniform_random_choice_sequence(
676
+ protein: str, codon_frequencies: Dict[str, Tuple[List[str], List[float]]]
677
+ ) -> str:
678
+ """
679
+ Return the DNA sequence optimized using Uniform Random Choice (URC) approach
680
+ in which a random codon for a given amino acid is chosen using a uniform
681
+ prior.
682
+
683
+ Args:
684
+ protein (str): The protein sequence.
685
+ codon_frequencies (Dict[str, Tuple[List[str], List[float]]]): Codon
686
+ frequencies for each amino acid.
687
+
688
+ Returns:
689
+ str: The optimized DNA sequence.
690
+ """
691
+ # Select a random codon for each amino acid using a uniform prior distribution
692
+ dna_codons = []
693
+ for aminoacid in protein:
694
+ codons = codon_frequencies[aminoacid][0]
695
+ random_index = np.random.randint(0, len(codons))
696
+ dna_codons.append(codons[random_index])
697
+ return "".join(dna_codons)
698
+
699
+
700
+ def get_icor_prediction(input_seq: str, model_path: str, stop_symbol: str) -> str:
701
+ """
702
+ Return the optimized codon sequence for the given protein sequence using ICOR.
703
+
704
+ Credit: ICOR: improving codon optimization with recurrent neural networks
705
+ Rishab Jain, Aditya Jain, Elizabeth Mauro, Kevin LeShane, Douglas
706
+ Densmore
707
+
708
+ Args:
709
+ input_seq (str): The input protein sequence.
710
+ model_path (str): The path to the ICOR model.
711
+ stop_symbol (str): The symbol representing stop codons in the sequence.
712
+
713
+ Returns:
714
+ str: The optimized DNA sequence.
715
+ """
716
+ input_seq = input_seq.strip().upper()
717
+ input_seq = input_seq.replace(stop_symbol, "*")
718
+
719
+ # Define categorical labels from when model was trained.
720
+ labels = [
721
+ "AAA",
722
+ "AAC",
723
+ "AAG",
724
+ "AAT",
725
+ "ACA",
726
+ "ACG",
727
+ "ACT",
728
+ "AGC",
729
+ "ATA",
730
+ "ATC",
731
+ "ATG",
732
+ "ATT",
733
+ "CAA",
734
+ "CAC",
735
+ "CAG",
736
+ "CCG",
737
+ "CCT",
738
+ "CTA",
739
+ "CTC",
740
+ "CTG",
741
+ "CTT",
742
+ "GAA",
743
+ "GAT",
744
+ "GCA",
745
+ "GCC",
746
+ "GCG",
747
+ "GCT",
748
+ "GGA",
749
+ "GGC",
750
+ "GTC",
751
+ "GTG",
752
+ "GTT",
753
+ "TAA",
754
+ "TAT",
755
+ "TCA",
756
+ "TCG",
757
+ "TCT",
758
+ "TGG",
759
+ "TGT",
760
+ "TTA",
761
+ "TTC",
762
+ "TTG",
763
+ "TTT",
764
+ "ACC",
765
+ "CAT",
766
+ "CCA",
767
+ "CGG",
768
+ "CGT",
769
+ "GAC",
770
+ "GAG",
771
+ "GGT",
772
+ "AGT",
773
+ "GGG",
774
+ "GTA",
775
+ "TGC",
776
+ "CCC",
777
+ "CGA",
778
+ "CGC",
779
+ "TAC",
780
+ "TAG",
781
+ "TCC",
782
+ "AGA",
783
+ "AGG",
784
+ "TGA",
785
+ ]
786
+
787
+ # Define aa to integer table
788
+ def aa2int(seq: str) -> List[int]:
789
+ _aa2int = {
790
+ "A": 1,
791
+ "R": 2,
792
+ "N": 3,
793
+ "D": 4,
794
+ "C": 5,
795
+ "Q": 6,
796
+ "E": 7,
797
+ "G": 8,
798
+ "H": 9,
799
+ "I": 10,
800
+ "L": 11,
801
+ "K": 12,
802
+ "M": 13,
803
+ "F": 14,
804
+ "P": 15,
805
+ "S": 16,
806
+ "T": 17,
807
+ "W": 18,
808
+ "Y": 19,
809
+ "V": 20,
810
+ "B": 21,
811
+ "Z": 22,
812
+ "X": 23,
813
+ "*": 24,
814
+ "-": 25,
815
+ "?": 26,
816
+ }
817
+ return [_aa2int[i] for i in seq]
818
+
819
+ # Create empty array to fill
820
+ oh_array = np.zeros(shape=(26, len(input_seq)))
821
+
822
+ # Load placements from aa2int
823
+ aa_placement = aa2int(input_seq)
824
+
825
+ # One-hot encode the amino acid sequence:
826
+
827
+ # style nit: more pythonic to write for i in range(0, len(aa_placement)):
828
+ for i in range(0, len(aa_placement)):
829
+ oh_array[aa_placement[i], i] = 1
830
+ i += 1
831
+
832
+ oh_array = [oh_array]
833
+ x = np.array(np.transpose(oh_array))
834
+
835
+ y = x.astype(np.float32)
836
+
837
+ y = np.reshape(y, (y.shape[0], 1, 26))
838
+
839
+ # Start ICOR session using model.
840
+ sess = rt.InferenceSession(model_path)
841
+ input_name = sess.get_inputs()[0].name
842
+
843
+ # Get prediction:
844
+ pred_onx = sess.run(None, {input_name: y})
845
+
846
+ # Get the index of the highest probability from softmax output:
847
+ pred_indices = []
848
+ for pred in pred_onx[0]:
849
+ pred_indices.append(np.argmax(pred))
850
+
851
+ out_str = ""
852
+ for index in pred_indices:
853
+ out_str += labels[index]
854
+
855
+ return out_str
model/CodonTransformer/CodonUtils.py ADDED
@@ -0,0 +1,830 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: CodonUtils.py
3
+ ---------------------
4
+ Includes constants and helper functions used by other Python scripts.
5
+ """
6
+
7
+ import itertools
8
+ import gzip
9
+ import json
10
+ import os
11
+ import pickle
12
+ import re
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass
15
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
16
+
17
+ import pandas as pd
18
+ import requests
19
+ import torch
20
+
21
+ # List of all amino acids
22
+ AMINO_ACIDS: List[str] = [
23
+ "A", # Alanine
24
+ "C", # Cysteine
25
+ "D", # Aspartic acid
26
+ "E", # Glutamic acid
27
+ "F", # Phenylalanine
28
+ "G", # Glycine
29
+ "H", # Histidine
30
+ "I", # Isoleucine
31
+ "K", # Lysine
32
+ "L", # Leucine
33
+ "M", # Methionine
34
+ "N", # Asparagine
35
+ "P", # Proline
36
+ "Q", # Glutamine
37
+ "R", # Arginine
38
+ "S", # Serine
39
+ "T", # Threonine
40
+ "V", # Valine
41
+ "W", # Tryptophan
42
+ "Y", # Tyrosine
43
+ ]
44
+ STOP_SYMBOLS = ["_", "*"] # Stop codon symbols
45
+
46
+ # Dictionary ambiguous amino acids to standard amino acids
47
+ AMBIGUOUS_AMINOACID_MAP: Dict[str, list[str]] = {
48
+ "B": ["N", "D"], # Asparagine (N) or Aspartic acid (D)
49
+ "Z": ["Q", "E"], # Glutamine (Q) or Glutamic acid (E)
50
+ "X": ["A"], # Any amino acid (typically replaced with Alanine)
51
+ "J": ["L", "I"], # Leucine (L) or Isoleucine (I)
52
+ "U": ["C"], # Selenocysteine (typically replaced with Cysteine)
53
+ "O": ["K"], # Pyrrolysine (typically replaced with Lysine)
54
+ }
55
+
56
+ # List of all possible start and stop codons
57
+ START_CODONS: List[str] = ["ATG", "TTG", "CTG", "GTG"]
58
+ STOP_CODONS: List[str] = ["TAA", "TAG", "TGA"]
59
+
60
+ # Token-to-index mapping for amino acids and special tokens
61
+ TOKEN2INDEX: Dict[str, int] = {
62
+ "[UNK]": 0,
63
+ "[CLS]": 1,
64
+ "[SEP]": 2,
65
+ "[PAD]": 3,
66
+ "[MASK]": 4,
67
+ "a_unk": 5,
68
+ "c_unk": 6,
69
+ "d_unk": 7,
70
+ "e_unk": 8,
71
+ "f_unk": 9,
72
+ "g_unk": 10,
73
+ "h_unk": 11,
74
+ "i_unk": 12,
75
+ "k_unk": 13,
76
+ "l_unk": 14,
77
+ "m_unk": 15,
78
+ "n_unk": 16,
79
+ "p_unk": 17,
80
+ "q_unk": 18,
81
+ "r_unk": 19,
82
+ "s_unk": 20,
83
+ "t_unk": 21,
84
+ "v_unk": 22,
85
+ "w_unk": 23,
86
+ "y_unk": 24,
87
+ "__unk": 25,
88
+ "k_aaa": 26,
89
+ "n_aac": 27,
90
+ "k_aag": 28,
91
+ "n_aat": 29,
92
+ "t_aca": 30,
93
+ "t_acc": 31,
94
+ "t_acg": 32,
95
+ "t_act": 33,
96
+ "r_aga": 34,
97
+ "s_agc": 35,
98
+ "r_agg": 36,
99
+ "s_agt": 37,
100
+ "i_ata": 38,
101
+ "i_atc": 39,
102
+ "m_atg": 40,
103
+ "i_att": 41,
104
+ "q_caa": 42,
105
+ "h_cac": 43,
106
+ "q_cag": 44,
107
+ "h_cat": 45,
108
+ "p_cca": 46,
109
+ "p_ccc": 47,
110
+ "p_ccg": 48,
111
+ "p_cct": 49,
112
+ "r_cga": 50,
113
+ "r_cgc": 51,
114
+ "r_cgg": 52,
115
+ "r_cgt": 53,
116
+ "l_cta": 54,
117
+ "l_ctc": 55,
118
+ "l_ctg": 56,
119
+ "l_ctt": 57,
120
+ "e_gaa": 58,
121
+ "d_gac": 59,
122
+ "e_gag": 60,
123
+ "d_gat": 61,
124
+ "a_gca": 62,
125
+ "a_gcc": 63,
126
+ "a_gcg": 64,
127
+ "a_gct": 65,
128
+ "g_gga": 66,
129
+ "g_ggc": 67,
130
+ "g_ggg": 68,
131
+ "g_ggt": 69,
132
+ "v_gta": 70,
133
+ "v_gtc": 71,
134
+ "v_gtg": 72,
135
+ "v_gtt": 73,
136
+ "__taa": 74,
137
+ "y_tac": 75,
138
+ "__tag": 76,
139
+ "y_tat": 77,
140
+ "s_tca": 78,
141
+ "s_tcc": 79,
142
+ "s_tcg": 80,
143
+ "s_tct": 81,
144
+ "__tga": 82,
145
+ "c_tgc": 83,
146
+ "w_tgg": 84,
147
+ "c_tgt": 85,
148
+ "l_tta": 86,
149
+ "f_ttc": 87,
150
+ "l_ttg": 88,
151
+ "f_ttt": 89,
152
+ }
153
+
154
+ # Index-to-token mapping, reverse of TOKEN2INDEX
155
+ INDEX2TOKEN: Dict[int, str] = {i: c for c, i in TOKEN2INDEX.items()}
156
+
157
+ # Dictionary mapping each amino acid and stop symbol to indices of codon tokens that translate to it
158
+ AMINO_ACID_TO_INDEX = {
159
+ aa: sorted(
160
+ [i for t, i in TOKEN2INDEX.items() if t[0].upper() == aa and t[-3:] != "unk"]
161
+ )
162
+ for aa in (AMINO_ACIDS + STOP_SYMBOLS)
163
+ }
164
+
165
+
166
+ # Mask token mapping
167
+ TOKEN2MASK: Dict[int, int] = {
168
+ 0: 0,
169
+ 1: 1,
170
+ 2: 2,
171
+ 3: 3,
172
+ 4: 4,
173
+ 5: 5,
174
+ 6: 6,
175
+ 7: 7,
176
+ 8: 8,
177
+ 9: 9,
178
+ 10: 10,
179
+ 11: 11,
180
+ 12: 12,
181
+ 13: 13,
182
+ 14: 14,
183
+ 15: 15,
184
+ 16: 16,
185
+ 17: 17,
186
+ 18: 18,
187
+ 19: 19,
188
+ 20: 20,
189
+ 21: 21,
190
+ 22: 22,
191
+ 23: 23,
192
+ 24: 24,
193
+ 25: 25,
194
+ 26: 13,
195
+ 27: 16,
196
+ 28: 13,
197
+ 29: 16,
198
+ 30: 21,
199
+ 31: 21,
200
+ 32: 21,
201
+ 33: 21,
202
+ 34: 19,
203
+ 35: 20,
204
+ 36: 19,
205
+ 37: 20,
206
+ 38: 12,
207
+ 39: 12,
208
+ 40: 15,
209
+ 41: 12,
210
+ 42: 18,
211
+ 43: 11,
212
+ 44: 18,
213
+ 45: 11,
214
+ 46: 17,
215
+ 47: 17,
216
+ 48: 17,
217
+ 49: 17,
218
+ 50: 19,
219
+ 51: 19,
220
+ 52: 19,
221
+ 53: 19,
222
+ 54: 14,
223
+ 55: 14,
224
+ 56: 14,
225
+ 57: 14,
226
+ 58: 8,
227
+ 59: 7,
228
+ 60: 8,
229
+ 61: 7,
230
+ 62: 5,
231
+ 63: 5,
232
+ 64: 5,
233
+ 65: 5,
234
+ 66: 10,
235
+ 67: 10,
236
+ 68: 10,
237
+ 69: 10,
238
+ 70: 22,
239
+ 71: 22,
240
+ 72: 22,
241
+ 73: 22,
242
+ 74: 25,
243
+ 75: 24,
244
+ 76: 25,
245
+ 77: 24,
246
+ 78: 20,
247
+ 79: 20,
248
+ 80: 20,
249
+ 81: 20,
250
+ 82: 25,
251
+ 83: 6,
252
+ 84: 23,
253
+ 85: 6,
254
+ 86: 14,
255
+ 87: 9,
256
+ 88: 14,
257
+ 89: 9,
258
+ }
259
+
260
+ # List of organisms used for fine-tuning
261
+ FINE_TUNE_ORGANISMS: List[str] = [
262
+ "Arabidopsis thaliana",
263
+ "Bacillus subtilis",
264
+ "Caenorhabditis elegans",
265
+ "Chlamydomonas reinhardtii",
266
+ "Chlamydomonas reinhardtii chloroplast",
267
+ "Danio rerio",
268
+ "Drosophila melanogaster",
269
+ "Homo sapiens",
270
+ "Mus musculus",
271
+ "Nicotiana tabacum",
272
+ "Nicotiana tabacum chloroplast",
273
+ "Pseudomonas putida",
274
+ "Saccharomyces cerevisiae",
275
+ "Escherichia coli O157-H7 str. Sakai",
276
+ "Escherichia coli general",
277
+ "Escherichia coli str. K-12 substr. MG1655",
278
+ "Thermococcus barophilus MPT",
279
+ ]
280
+
281
+ # List of organisms most commonly used for coodn optimization
282
+ COMMON_ORGANISMS: List[str] = [
283
+ "Arabidopsis thaliana",
284
+ "Bacillus subtilis",
285
+ "Caenorhabditis elegans",
286
+ "Chlamydomonas reinhardtii",
287
+ "Danio rerio",
288
+ "Drosophila melanogaster",
289
+ "Homo sapiens",
290
+ "Mus musculus",
291
+ "Nicotiana tabacum",
292
+ "Pseudomonas putida",
293
+ "Saccharomyces cerevisiae",
294
+ "Escherichia coli general",
295
+ ]
296
+
297
+ # Dictionary mapping each organism name to respective organism id
298
+ ORGANISM2ID: Dict[str, int] = {
299
+ "Arabidopsis thaliana": 0,
300
+ "Atlantibacter hermannii": 1,
301
+ "Bacillus subtilis": 2,
302
+ "Brenneria goodwinii": 3,
303
+ "Buchnera aphidicola (Schizaphis graminum)": 4,
304
+ "Caenorhabditis elegans": 5,
305
+ "Candidatus Erwinia haradaeae": 6,
306
+ "Candidatus Hamiltonella defensa 5AT (Acyrthosiphon pisum)": 7,
307
+ "Chlamydomonas reinhardtii": 8,
308
+ "Chlamydomonas reinhardtii chloroplast": 9,
309
+ "Citrobacter amalonaticus": 10,
310
+ "Citrobacter braakii": 11,
311
+ "Citrobacter cronae": 12,
312
+ "Citrobacter europaeus": 13,
313
+ "Citrobacter farmeri": 14,
314
+ "Citrobacter freundii": 15,
315
+ "Citrobacter koseri ATCC BAA-895": 16,
316
+ "Citrobacter portucalensis": 17,
317
+ "Citrobacter werkmanii": 18,
318
+ "Citrobacter youngae": 19,
319
+ "Cronobacter dublinensis subsp. dublinensis LMG 23823": 20,
320
+ "Cronobacter malonaticus LMG 23826": 21,
321
+ "Cronobacter sakazakii": 22,
322
+ "Cronobacter turicensis": 23,
323
+ "Danio rerio": 24,
324
+ "Dickeya dadantii 3937": 25,
325
+ "Dickeya dianthicola": 26,
326
+ "Dickeya fangzhongdai": 27,
327
+ "Dickeya solani": 28,
328
+ "Dickeya zeae": 29,
329
+ "Drosophila melanogaster": 30,
330
+ "Edwardsiella anguillarum ET080813": 31,
331
+ "Edwardsiella ictaluri": 32,
332
+ "Edwardsiella piscicida": 33,
333
+ "Edwardsiella tarda": 34,
334
+ "Enterobacter asburiae": 35,
335
+ "Enterobacter bugandensis": 36,
336
+ "Enterobacter cancerogenus": 37,
337
+ "Enterobacter chengduensis": 38,
338
+ "Enterobacter cloacae": 39,
339
+ "Enterobacter hormaechei": 40,
340
+ "Enterobacter kobei": 41,
341
+ "Enterobacter ludwigii": 42,
342
+ "Enterobacter mori": 43,
343
+ "Enterobacter quasiroggenkampii": 44,
344
+ "Enterobacter roggenkampii": 45,
345
+ "Enterobacter sichuanensis": 46,
346
+ "Erwinia amylovora CFBP1430": 47,
347
+ "Erwinia persicina": 48,
348
+ "Escherichia albertii": 49,
349
+ "Escherichia coli O157-H7 str. Sakai": 50,
350
+ "Escherichia coli general": 51,
351
+ "Escherichia coli str. K-12 substr. MG1655": 52,
352
+ "Escherichia fergusonii": 53,
353
+ "Escherichia marmotae": 54,
354
+ "Escherichia ruysiae": 55,
355
+ "Ewingella americana": 56,
356
+ "Hafnia alvei": 57,
357
+ "Hafnia paralvei": 58,
358
+ "Homo sapiens": 59,
359
+ "Kalamiella piersonii": 60,
360
+ "Klebsiella aerogenes": 61,
361
+ "Klebsiella grimontii": 62,
362
+ "Klebsiella michiganensis": 63,
363
+ "Klebsiella oxytoca": 64,
364
+ "Klebsiella pasteurii": 65,
365
+ "Klebsiella pneumoniae subsp. pneumoniae HS11286": 66,
366
+ "Klebsiella quasipneumoniae": 67,
367
+ "Klebsiella quasivariicola": 68,
368
+ "Klebsiella variicola": 69,
369
+ "Kosakonia cowanii": 70,
370
+ "Kosakonia radicincitans": 71,
371
+ "Leclercia adecarboxylata": 72,
372
+ "Lelliottia amnigena": 73,
373
+ "Lonsdalea populi": 74,
374
+ "Moellerella wisconsensis": 75,
375
+ "Morganella morganii": 76,
376
+ "Mus musculus": 77,
377
+ "Nicotiana tabacum": 78,
378
+ "Nicotiana tabacum chloroplast": 79,
379
+ "Obesumbacterium proteus": 80,
380
+ "Pantoea agglomerans": 81,
381
+ "Pantoea allii": 82,
382
+ "Pantoea ananatis PA13": 83,
383
+ "Pantoea dispersa": 84,
384
+ "Pantoea stewartii": 85,
385
+ "Pantoea vagans": 86,
386
+ "Pectobacterium aroidearum": 87,
387
+ "Pectobacterium atrosepticum": 88,
388
+ "Pectobacterium brasiliense": 89,
389
+ "Pectobacterium carotovorum": 90,
390
+ "Pectobacterium odoriferum": 91,
391
+ "Pectobacterium parmentieri": 92,
392
+ "Pectobacterium polaris": 93,
393
+ "Pectobacterium versatile": 94,
394
+ "Photorhabdus laumondii subsp. laumondii TTO1": 95,
395
+ "Plesiomonas shigelloides": 96,
396
+ "Pluralibacter gergoviae": 97,
397
+ "Proteus faecis": 98,
398
+ "Proteus mirabilis HI4320": 99,
399
+ "Proteus penneri": 100,
400
+ "Proteus terrae subsp. cibarius": 101,
401
+ "Proteus vulgaris": 102,
402
+ "Providencia alcalifaciens": 103,
403
+ "Providencia heimbachae": 104,
404
+ "Providencia rettgeri": 105,
405
+ "Providencia rustigianii": 106,
406
+ "Providencia stuartii": 107,
407
+ "Providencia thailandensis": 108,
408
+ "Pseudomonas putida": 109,
409
+ "Pyrococcus furiosus": 110,
410
+ "Pyrococcus horikoshii": 111,
411
+ "Pyrococcus yayanosii": 112,
412
+ "Rahnella aquatilis CIP 78.65 = ATCC 33071": 113,
413
+ "Raoultella ornithinolytica": 114,
414
+ "Raoultella planticola": 115,
415
+ "Raoultella terrigena": 116,
416
+ "Rosenbergiella epipactidis": 117,
417
+ "Rouxiella badensis": 118,
418
+ "Saccharolobus solfataricus": 119,
419
+ "Saccharomyces cerevisiae": 120,
420
+ "Salmonella bongori N268-08": 121,
421
+ "Salmonella enterica subsp. enterica serovar Typhimurium str. LT2": 122,
422
+ "Serratia bockelmannii": 123,
423
+ "Serratia entomophila": 124,
424
+ "Serratia ficaria": 125,
425
+ "Serratia fonticola": 126,
426
+ "Serratia grimesii": 127,
427
+ "Serratia liquefaciens": 128,
428
+ "Serratia marcescens": 129,
429
+ "Serratia nevei": 130,
430
+ "Serratia plymuthica AS9": 131,
431
+ "Serratia proteamaculans": 132,
432
+ "Serratia quinivorans": 133,
433
+ "Serratia rubidaea": 134,
434
+ "Serratia ureilytica": 135,
435
+ "Shigella boydii": 136,
436
+ "Shigella dysenteriae": 137,
437
+ "Shigella flexneri 2a str. 301": 138,
438
+ "Shigella sonnei": 139,
439
+ "Thermoccoccus kodakarensis": 140,
440
+ "Thermococcus barophilus MPT": 141,
441
+ "Thermococcus chitonophagus": 142,
442
+ "Thermococcus gammatolerans": 143,
443
+ "Thermococcus litoralis": 144,
444
+ "Thermococcus onnurineus": 145,
445
+ "Thermococcus sibiricus": 146,
446
+ "Xenorhabdus bovienii str. feltiae Florida": 147,
447
+ "Yersinia aldovae 670-83": 148,
448
+ "Yersinia aleksiciae": 149,
449
+ "Yersinia alsatica": 150,
450
+ "Yersinia enterocolitica": 151,
451
+ "Yersinia frederiksenii ATCC 33641": 152,
452
+ "Yersinia intermedia": 153,
453
+ "Yersinia kristensenii": 154,
454
+ "Yersinia massiliensis CCUG 53443": 155,
455
+ "Yersinia mollaretii ATCC 43969": 156,
456
+ "Yersinia pestis A1122": 157,
457
+ "Yersinia proxima": 158,
458
+ "Yersinia pseudotuberculosis IP 32953": 159,
459
+ "Yersinia rochesterensis": 160,
460
+ "Yersinia rohdei": 161,
461
+ "Yersinia ruckeri": 162,
462
+ "Yokenella regensburgei": 163,
463
+ }
464
+
465
+ # Dictionary mapping each organism id to respective organism name
466
+ ID2ORGANISM = {v: k for k, v in ORGANISM2ID.items()}
467
+
468
+ # Type alias for amino acid to codon mapping
469
+ AMINO2CODON_TYPE = Dict[str, Tuple[List[str], List[float]]]
470
+
471
+ # Constants for the number of organisms and sequence lengths
472
+ NUM_ORGANISMS = 164
473
+ MAX_LEN = 2048
474
+ MAX_AMINO_ACIDS = MAX_LEN - 2 # Without special tokens [CLS] and [SEP]
475
+ STOP_SYMBOL = "_"
476
+
477
+
478
+ @dataclass
479
+ class DNASequencePrediction:
480
+ """
481
+ A class to hold the output of the DNA sequence prediction.
482
+
483
+ Attributes:
484
+ organism (str): Name of the organism used for prediction.
485
+ protein (str): Input protein sequence for which DNA sequence is predicted.
486
+ processed_input (str): Processed input sequence (merged protein and DNA).
487
+ predicted_dna (str): Predicted DNA sequence.
488
+ """
489
+
490
+ organism: str
491
+ protein: str
492
+ processed_input: str
493
+ predicted_dna: str
494
+
495
+
496
+ class IterableData(torch.utils.data.IterableDataset):
497
+ """
498
+ Defines the logic for iterable datasets (working over streams of
499
+ data) in parallel multi-processing environments, e.g., multi-GPU.
500
+
501
+ Args:
502
+ dist_env (Optional[str]): The distribution environment identifier
503
+ (e.g., "slurm").
504
+
505
+ Credit: Guillaume Filion
506
+ """
507
+
508
+ def __init__(self, dist_env: Optional[str] = None):
509
+ super().__init__()
510
+ self.world_size_handle, self.rank_handle = {
511
+ "slurm": ("SLURM_NTASKS", "SLURM_PROCID")
512
+ }.get(dist_env, ("WORLD_SIZE", "LOCAL_RANK"))
513
+
514
+ @property
515
+ def iterator(self) -> Iterator:
516
+ """Define the stream logic for the dataset. Implement in subclasses."""
517
+ raise NotImplementedError
518
+
519
+ def __iter__(self) -> Iterator:
520
+ """
521
+ Create an iterator for the dataset, handling multi-processing contexts.
522
+
523
+ Returns:
524
+ Iterator: The iterator for the dataset.
525
+ """
526
+ worker_info = torch.utils.data.get_worker_info()
527
+ if worker_info is None:
528
+ return self.iterator
529
+
530
+ # In multi-processing context, use 'os.environ' to
531
+ # find global worker rank. Then use 'islice' to allocate
532
+ # the items of the stream to the workers.
533
+ world_size = int(os.environ.get(self.world_size_handle))
534
+ global_rank = int(os.environ.get(self.rank_handle))
535
+ local_rank = worker_info.id
536
+ local_num_workers = worker_info.num_workers
537
+
538
+ # Assume that each process has the same number of local workers.
539
+ worker_rk = global_rank * local_num_workers + local_rank
540
+ worker_nb = world_size * local_num_workers
541
+ return itertools.islice(self.iterator, worker_rk, None, worker_nb)
542
+
543
+
544
+ class IterableJSONData(IterableData):
545
+ """
546
+ Iterate over the lines of a JSON file and uncompress if needed.
547
+
548
+ Args:
549
+ data_path (str): The path to the JSON data file.
550
+ train (bool): Flag indicating if the dataset is for training.
551
+ **kwargs: Additional keyword arguments for the base class.
552
+ """
553
+
554
+ def __init__(self, data_path: str, train: bool = True, **kwargs):
555
+ super().__init__(**kwargs)
556
+ self.data_path = data_path
557
+ self.train = train
558
+
559
+ @property
560
+ def iterator(self) -> Iterator:
561
+ open_fn = gzip.open if self.data_path.endswith(".gz") else open
562
+ with open_fn(self.data_path, "rt") as file:
563
+ for line in file:
564
+ line = line.strip()
565
+ if line:
566
+ yield json.loads(line)
567
+
568
+
569
+ class ConfigManager(ABC):
570
+ """
571
+ Abstract base class for managing configuration settings.
572
+ """
573
+
574
+ def __enter__(self):
575
+ return self
576
+
577
+ def __exit__(self, exc_type, exc_value, traceback):
578
+ if exc_type is not None:
579
+ print(f"Exception occurred: {exc_type}, {exc_value}, {traceback}")
580
+ self.reset_config()
581
+
582
+ @abstractmethod
583
+ def reset_config(self) -> None:
584
+ """Reset the configuration to default values."""
585
+ pass
586
+
587
+ def get(self, key: str) -> Any:
588
+ """
589
+ Get the value of a configuration key.
590
+
591
+ Args:
592
+ key (str): The key to retrieve the value for.
593
+
594
+ Returns:
595
+ Any: The value of the configuration key.
596
+ """
597
+ return self._config.get(key)
598
+
599
+ def set(self, key: str, value: Any) -> None:
600
+ """
601
+ Set the value of a configuration key.
602
+
603
+ Args:
604
+ key (str): The key to set the value for.
605
+ value (Any): The value to set for the key.
606
+ """
607
+ self.validate_inputs(key, value)
608
+ self._config[key] = value
609
+
610
+ def update(self, config_dict: dict) -> None:
611
+ """
612
+ Update the configuration with a dictionary of key-value pairs after validating them.
613
+
614
+ Args:
615
+ config_dict (dict): A dictionary of key-value pairs to update the configuration.
616
+ """
617
+ for key, value in config_dict.items():
618
+ self.validate_inputs(key, value)
619
+ self._config.update(config_dict)
620
+
621
+ @abstractmethod
622
+ def validate_inputs(self, key: str, value: Any) -> None:
623
+ """Validate the inputs for the configuration."""
624
+ pass
625
+
626
+
627
+ class ProteinConfig(ConfigManager):
628
+ """
629
+ A class to manage configuration settings for protein sequences.
630
+
631
+ This class ensures that the configuration is a singleton.
632
+ It provides methods to get, set, and update configuration values.
633
+
634
+ Attributes:
635
+ _instance (Optional[ConfigManager]): The singleton instance of the ConfigManager.
636
+ _config (Dict[str, Any]): The configuration dictionary.
637
+ """
638
+
639
+ _instance = None
640
+
641
+ def __new__(cls):
642
+ """
643
+ Create a new instance of the ProteinConfig class.
644
+
645
+ Returns:
646
+ ProteinConfig: The singleton instance of the ProteinConfig.
647
+ """
648
+ if cls._instance is None:
649
+ cls._instance = super(ProteinConfig, cls).__new__(cls)
650
+ cls._instance.reset_config()
651
+ return cls._instance
652
+
653
+ def validate_inputs(self, key: str, value: Any) -> None:
654
+ """
655
+ Validate the inputs for the configuration.
656
+
657
+ Args:
658
+ key (str): The key to validate.
659
+ value (Any): The value to validate.
660
+
661
+ Raises:
662
+ ValueError: If the value is invalid.
663
+ TypeError: If the value is of the wrong type.
664
+ """
665
+ if key == "ambiguous_aminoacid_behavior":
666
+ if value not in [
667
+ "raise_error",
668
+ "standardize_deterministic",
669
+ "standardize_random",
670
+ ]:
671
+ raise ValueError(
672
+ f"Invalid value for ambiguous_aminoacid_behavior: {value}."
673
+ )
674
+ elif key == "ambiguous_aminoacid_map_override":
675
+ if not isinstance(value, dict):
676
+ raise TypeError(
677
+ f"Invalid type for ambiguous_aminoacid_map_override: {value}."
678
+ )
679
+ for ambiguous_aminoacid, aminoacids in value.items():
680
+ if not isinstance(aminoacids, list):
681
+ raise TypeError(f"Invalid type for aminoacids: {aminoacids}.")
682
+ if not aminoacids:
683
+ raise ValueError(
684
+ f"Override for aminoacid '{ambiguous_aminoacid}' cannot be empty list."
685
+ )
686
+ if ambiguous_aminoacid not in AMBIGUOUS_AMINOACID_MAP:
687
+ raise ValueError(
688
+ f"Invalid amino acid in ambiguous_aminoacid_map_override: {ambiguous_aminoacid}"
689
+ )
690
+ else:
691
+ raise ValueError(f"Invalid configuration key: {key}")
692
+
693
+ def reset_config(self) -> None:
694
+ """
695
+ Reset the configuration to the default values.
696
+ """
697
+ self._config = {
698
+ "ambiguous_aminoacid_behavior": "standardize_random",
699
+ "ambiguous_aminoacid_map_override": {},
700
+ }
701
+
702
+
703
+ def load_python_object_from_disk(file_path: str) -> Any:
704
+ """
705
+ Load a Pickle object from disk and return it as a Python object.
706
+
707
+ Args:
708
+ file_path (str): The path to the Pickle file.
709
+
710
+ Returns:
711
+ Any: The loaded Python object.
712
+ """
713
+ with open(file_path, "rb") as file:
714
+ return pickle.load(file)
715
+
716
+
717
+ def save_python_object_to_disk(input_object: Any, file_path: str) -> None:
718
+ """
719
+ Save a Python object to disk using Pickle.
720
+
721
+ Args:
722
+ input_object (Any): The Python object to save.
723
+ file_path (str): The path where the object will be saved.
724
+ """
725
+ with open(file_path, "wb") as file:
726
+ pickle.dump(input_object, file)
727
+
728
+
729
+ def find_pattern_in_fasta(keyword: str, text: str) -> str:
730
+ """
731
+ Find a specific keyword pattern in text. Helpful for identifying parts
732
+ of a FASTA sequence.
733
+
734
+ Args:
735
+ keyword (str): The keyword pattern to search for.
736
+ text (str): The text to search within.
737
+
738
+ Returns:
739
+ str: The found pattern or an empty string if not found.
740
+ """
741
+ # Search for the keyword pattern in the text using regex
742
+ result = re.search(keyword + r"=(.*?)]", text)
743
+ return result.group(1) if result else ""
744
+
745
+
746
+ def get_organism2id_dict(organism_reference: str) -> Dict[str, int]:
747
+ """
748
+ Return a dictionary mapping each organism in training data to an index
749
+ used for training.
750
+
751
+ Args:
752
+ organism_reference (str): Path to a CSV file containing a list of
753
+ all organisms. The format of the CSV file should be as follows:
754
+
755
+ 0,Escherichia coli
756
+ 1,Homo sapiens
757
+ 2,Mus musculus
758
+
759
+ Returns:
760
+ Dict[str, int]: Dictionary mapping organism names to their respective indices.
761
+ """
762
+ # Read the CSV file and create a dictionary mapping organisms to their indices
763
+ organisms = pd.read_csv(organism_reference, index_col=0, header=None)
764
+ organism2id = {organisms.iloc[i].values[0]: i for i in organisms.index}
765
+
766
+ return organism2id
767
+
768
+
769
+ def get_taxonomy_id(
770
+ taxonomy_reference: str, organism: Optional[str] = None, return_dict: bool = False
771
+ ) -> Any:
772
+ """
773
+ Return the taxonomy id of a given organism using a reference file.
774
+ Optionally, return the whole dictionary instead if return_dict is True.
775
+
776
+ Args:
777
+ taxonomy_reference (str): Path to the taxonomy reference file.
778
+ organism (Optional[str]): The name of the organism to look up.
779
+ return_dict (bool): Whether to return the entire dictionary.
780
+
781
+ Returns:
782
+ Any: The taxonomy id of the organism or the entire dictionary.
783
+ """
784
+ # Load the organism-to-taxonomy mapping from a Pickle file
785
+ organism2taxonomy = load_python_object_from_disk(taxonomy_reference)
786
+
787
+ if return_dict:
788
+ return dict(sorted(organism2taxonomy.items()))
789
+
790
+ return organism2taxonomy[organism]
791
+
792
+
793
+ def sort_amino2codon_skeleton(amino2codon: Dict[str, Any]) -> Dict[str, Any]:
794
+ """
795
+ Sort the amino2codon dictionary alphabetically by amino acid and by codon name.
796
+
797
+ Args:
798
+ amino2codon (Dict[str, Any]): The amino2codon dictionary to sort.
799
+
800
+ Returns:
801
+ Dict[str, Any]: The sorted amino2codon dictionary.
802
+ """
803
+ # Sort the dictionary by amino acid and then by codon name
804
+ amino2codon = dict(sorted(amino2codon.items()))
805
+ amino2codon = {
806
+ amino: (
807
+ [codon for codon, _ in sorted(zip(codons, frequencies))],
808
+ [freq for _, freq in sorted(zip(codons, frequencies))],
809
+ )
810
+ for amino, (codons, frequencies) in amino2codon.items()
811
+ }
812
+
813
+ return amino2codon
814
+
815
+
816
+ def load_pkl_from_url(url: str) -> Any:
817
+ """
818
+ Download a Pickle file from a URL and return the loaded object.
819
+
820
+ Args:
821
+ url (str): The URL to download the Pickle file from.
822
+
823
+ Returns:
824
+ Any: The loaded Python object from the Pickle file.
825
+ """
826
+ response = requests.get(url)
827
+ response.raise_for_status() # Ensure the request was successful
828
+
829
+ # Load the Pickle object from the response content
830
+ return pickle.loads(response.content)
model/CodonTransformer/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """CodonTransformer package."""
model/src/CodonTransformerTokenizer.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version": "1.0", "truncation": null, "padding": null, "added_tokens": [{"id": 0, "special": true, "content": "[UNK]", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false}, {"id": 1, "special": true, "content": "[CLS]", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false}, {"id": 2, "special": true, "content": "[SEP]", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false}, {"id": 3, "special": true, "content": "[PAD]", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false}, {"id": 4, "special": true, "content": "[MASK]", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false}], "normalizer": {"type": "Sequence", "normalizers": [{"type": "Lowercase"}]}, "pre_tokenizer": {"type": "Sequence", "pretokenizers": [{"type": "Split", "pattern": {"String": " "}, "behavior": "Isolated", "invert": false}, {"type": "Whitespace"}]}, "post_processor": {"type": "TemplateProcessing", "single": [{"SpecialToken": {"id": "[CLS]", "type_id": 0}}, {"Sequence": {"id": "A", "type_id": 0}}, {"SpecialToken": {"id": "[SEP]", "type_id": 0}}], "pair": [{"SpecialToken": {"id": "[CLS]", "type_id": 0}}, {"Sequence": {"id": "A", "type_id": 0}}, {"SpecialToken": {"id": "[SEP]", "type_id": 0}}, {"Sequence": {"id": "B", "type_id": 1}}, {"SpecialToken": {"id": "[SEP]", "type_id": 1}}], "special_tokens": {"[CLS]": {"id": "[CLS]", "ids": [1], "tokens": ["[CLS]"]}, "[SEP]": {"id": "[SEP]", "ids": [2], "tokens": ["[SEP]"]}}}, "decoder": null, "model": {"type": "WordPiece", "unk_token": "[UNK]", "continuing_subword_prefix": "##", "max_input_chars_per_word": 100, "vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2, "[PAD]": 3, "[MASK]": 4, "a_unk": 5, "c_unk": 6, "d_unk": 7, "e_unk": 8, "f_unk": 9, "g_unk": 10, "h_unk": 11, "i_unk": 12, "k_unk": 13, "l_unk": 14, "m_unk": 15, "n_unk": 16, "p_unk": 17, "q_unk": 18, "r_unk": 19, "s_unk": 20, "t_unk": 21, "v_unk": 22, "w_unk": 23, "y_unk": 24, "__unk": 25, "k_aaa": 26, "n_aac": 27, "k_aag": 28, "n_aat": 29, "t_aca": 30, "t_acc": 31, "t_acg": 32, "t_act": 33, "r_aga": 34, "s_agc": 35, "r_agg": 36, "s_agt": 37, "i_ata": 38, "i_atc": 39, "m_atg": 40, "i_att": 41, "q_caa": 42, "h_cac": 43, "q_cag": 44, "h_cat": 45, "p_cca": 46, "p_ccc": 47, "p_ccg": 48, "p_cct": 49, "r_cga": 50, "r_cgc": 51, "r_cgg": 52, "r_cgt": 53, "l_cta": 54, "l_ctc": 55, "l_ctg": 56, "l_ctt": 57, "e_gaa": 58, "d_gac": 59, "e_gag": 60, "d_gat": 61, "a_gca": 62, "a_gcc": 63, "a_gcg": 64, "a_gct": 65, "g_gga": 66, "g_ggc": 67, "g_ggg": 68, "g_ggt": 69, "v_gta": 70, "v_gtc": 71, "v_gtg": 72, "v_gtt": 73, "__taa": 74, "y_tac": 75, "__tag": 76, "y_tat": 77, "s_tca": 78, "s_tcc": 79, "s_tcg": 80, "s_tct": 81, "__tga": 82, "c_tgc": 83, "w_tgg": 84, "c_tgt": 85, "l_tta": 86, "f_ttc": 87, "l_ttg": 88, "f_ttt": 89}}}
model/src/CodonTransformer_inference_template.xlsx ADDED
Binary file (17.4 kB). View file
 
model/src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Model weights, tokenizer, and other resources."""
model/src/banner_final.png ADDED

Git LFS Details

  • SHA256: 6aa745d1f362190e7ae0b8940154446e68426bfb16ef6be9336fb6f98168a205
  • Pointer size: 131 Bytes
  • Size of remote file: 468 kB
model/src/organism2id.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:44f7b73bbb3c6ea82bf864e886b57b219cbd5f14fe79a8aa47d2befab5d40ad0
3
+ size 4605
pyproject.toml ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "CodonTransformer"
3
+ version = "1.6.7"
4
+ description = "The ultimate tool for codon optimization, transforming protein sequences into optimized DNA sequences specific for your target organisms."
5
+ authors = ["Adibvafa Fallahpour <Adibvafa.fallahpour@mail.utoronto.ca>"]
6
+ license = "Apache-2.0"
7
+ readme = "README.md"
8
+ homepage = "https://github.com/adibvafa/CodonTransformer"
9
+ repository = "https://github.com/adibvafa/CodonTransformer"
10
+ packages = [{ include = "CodonTransformer", from = "model" }]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: Apache Software License",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+
17
+ [tool.poetry.dependencies]
18
+ python = "^3.9"
19
+ biopython = "^1.83"
20
+ ipywidgets = "^7.0.0"
21
+ numpy = "<2.0.0"
22
+ onnxruntime = "^1.16.3"
23
+ pandas = "^2.0.0"
24
+ python_codon_tables = "^0.1.12"
25
+ pytorch_lightning = "^2.2.1"
26
+ scikit-learn = "^1.2.2"
27
+ scipy = "^1.13.1"
28
+ setuptools = "^70.0.0"
29
+ torch = "^2.0.0"
30
+ tqdm = "^4.66.2"
31
+ transformers = "^4.40.0"
32
+ CAI-PyPI = "^2.0.1"
33
+
34
+ [tool.poetry.dev-dependencies]
35
+ coverage = {version = "^7.0", extras = ["toml"]}
36
+
37
+ [build-system]
38
+ requires = ["poetry-core>=1.0.0"]
39
+ build-backend = "poetry.core.masonry.api"
40
+
41
+ [tool.ruff]
42
+ line-length = 88
43
+ indent-width = 4
44
+ target-version = "py310"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I"]
48
+ ignore = []
49
+
50
+ [tool.ruff.format]
51
+ quote-style = "double"
52
+ indent-style = "space"
53
+ skip-magic-trailing-comma = false
54
+ line-ending = "auto"
55
+
56
+ [tool.coverage.run]
57
+ omit = [
58
+ # omit pytorch-generated files in /tmp
59
+ "/tmp/*",
60
+ ]
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ biopython>=1.83,<2.0
2
+ CAI-PyPI>=2.0.1,<3.0
3
+ ipywidgets>=7.0.0,<10.0
4
+ numpy==1.26.3
5
+ onnxruntime>=1.16.3,<3.0
6
+ pandas>=2.0.0,<3.0
7
+ python_codon_tables>=0.1.12,<1.0
8
+ pytorch_lightning>=2.2.1,<3.0
9
+ scikit-learn>=1.2.2,<2.0
10
+ scipy>=1.13.1,<3.0
11
+ setuptools>=70.0.0
12
+ torch>=2.0.0,<3.0
13
+ tqdm>=4.66.2,<5.0
14
+ transformers>=4.40.0,<5.0
scripts/demo/sample_dataset.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ ,protein_sequence,organism,predicted_dna
2
+ 0,MSEKYIVTWDMLQIHARKLASRLMPSEQWKGIIAVSRGGLVPGALLARELGIRHVDTVCISSYDHDNQRELKVLKRAEGDGEGFIVIDDLVDTGGTAVAIREMYPKAHFVTIFAKPAGRPLVDDYVVDIPQDTWIEQPWDMGVVFVPPISGR_,Escherichia coli general,
3
+ 1,MKNIIRTPETHPLTWRLRDDKQPVWLDEYRSKNGYEGARKALTGLSPDEIVNQVKDAGLKGRGGAGFSTGLKWSLMPKDESMNIRYLLCNADEMEPGTYKDRLLMEQLPHLLVEGMLISAFALKAYRGYIFLRGEYIEAAVNLRRAIAEATEAGLLGKNIMGTGFDFELFVHTGAGRYICGEETALINSLEGRRANPRSKPPFPATSGAWGKPTCVNNVETLCNVPAILANGVEWYQNISKSKDAGTKLMGFSGRVKNPGLWELPFGTTAREILEDYAGGMRDGLKFKAWQPGGAGTDFLTEAHLDLPMEFESIGKAGSRLGTALAMAVDHEINMVSLVRNLEEFFARESCGWCTPCRDGLPWSVKILRALERGEGQPGDIETLEQLCRFLGPGKTFCAHAPGAVEPLQSAIKYFREEFEAGIKQPFSNTHLINGIQPNLLKERW_,Escherichia coli general,
4
+ 2,MDALQIAEDTLQTLVPHCPVPSGPRRIFLDANVKESYCPLVPHTMYCLPLWQGINLVLLTRSPSAPLALVLSQLMDGFSMLEKKLKEGPEPGASLRSQPLVGDLRQRMDKFVKNRGAQEIQSTWLEFKAKAFSKSEPGSSWELLQACGKLKRQLCAIYRLNFLTTAPSRGGPHLPQHLQDQVQRLMREKLTDWKDFLLVKSRRNITMVSYLEDFPGLVHFIYVDRTTGQMVAPSLNCSQKTSSELGKGPLAAFVKTKVWSLIQLARRYLQKGYTTLLFQEGDFYCSYFLWFENDMGYKLQMIEVPVLSDDSVPIGMLGGDYYRKLLRYYSKNRPTEAVRCYELLALHLSVIPTDLLVQQAGQLARRLWEASRIPLL_,Homo sapiens,
5
+ 3,MAFANFRRILRLSTFEKRKSREYEHVRRDLDPNEVWEIVGELGDGAFGKVYKAKNKETGALAAAKVIETKSEEELEDYIVEIEILATCDHPYIVKLLGAYYHDGKLWIMIEFCPGGAVDAIMLELDRGLTEPQIQVVCRQMLEALNFLHSKRIIHRDLKAGNVLMTLEGDIRLADFGVSAKNLKTLQKRDSFIGTPYWMAPEVVMCETMKDTPYDYKADIWSLGITLIEMAQIEPPHHELNPMRVLLKIAKSDPPTLLTPSKWSVEFRDFLKIALDKNPETRPSAAQLLEHPFVSSITSNKALRELVAEAKAEVMEEIEDGRDEGEEEDAVDAASDPKLYKKTLKRTRKFVVDGVEVSITTSKIISEDEKKDEEMRFLRRQELRELRLLQKEEHRNQTQLSNKHELQLEQMHKRFEQEINAKKKFFDTELENLERQQKQQVEKMEQDHAVRRREEARRIRLEQDRDYTRFQEQLKLMKKEVKNEVEKLPRQQRKESMKQKMEEHTQKKQLLDRDFVAKQKEDLELAMKRLTTDNRREICDKERECLMKKQELLRDREAALWEMEEHQLQERHQLVKQQLKDQYFLQRHELLRKHEKEREQMQRYNQRMIEQLKVRQQQEKARLPKIQRSEGKTRMAMYKKSLHINGGGSAAEQREKIKQFSQQEEKRQKSERLQQQQKHENQMRDMLAQCESNMSELQQLQNEKCHLLVEHETQKLKALDESHNQNLKEWRDKLRPRKKALEEDLNQKKREQEMFFKLSEEAECPNPSTPSKAAKFFPYSSADAS_,Homo sapiens,
6
+ 4,MTEKDAGGFNMSTFMNRKFQEPIQQIKTFSWMGFSWTCRKRRKHYQSYLRNGVRISVNDFVYVLAEQHKRLVAYIEDLYEDSKGKKMVVVRWFHKTEEVGSVLSDDDNDREIFFSLNRQDISIECIDYLATVLSPQHYEKFLKVPMHVQTVAFFCQKLYGDDGLKPYDITQLEGYWRQEMLRYLNVSILKSFEGAQAPGTDPGLKAPLVGCVGIRSRKRRRPSPVGTLNVSYAGDMKGDCKSSPDSVLAVTDASIFKGDEDGSSHHIKKGSLIEVLSEDSGIRGCWFKALVLKKHKDKVKVQYQDIQDADDESKKLEEWILTSRVAAGDHLGDLRIKGRKVVRPMLKPSKENDVCVIGVGMPVDVWWCDGWWEGIVVQEVSEEKFEVYLPGEKKMSAFHRNDLRQSREWLDDEWLNIRSRSDIVSSVLSLTKKKEMEVKHDEKSSDVGVCNGRMSPKTEAKRTISLPVATTKKSLPKRPIPDLLKDVLVTSDLKWKKSSRKRNRVVSCCPHDPSLNDGFSSERSLDCENCKFMEDTFGSSDGQHLTGLLMSR_,Arabidopsis thaliana,
scripts/demo/sample_predictions.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ ,protein_sequence,organism,predicted_dna
2
+ 0,MSEKYIVTWDMLQIHARKLASRLMPSEQWKGIIAVSRGGLVPGALLARELGIRHVDTVCISSYDHDNQRELKVLKRAEGDGEGFIVIDDLVDTGGTAVAIREMYPKAHFVTIFAKPAGRPLVDDYVVDIPQDTWIEQPWDMGVVFVPPISGR_,Escherichia coli general,ATGAGCGAAAAATATATTGTCACCTGGGACATGCTGCAGATCCATGCCCGCAAACTGGCCAGCCGCCTGATGCCGTCAGAACAGTGGAAAGGCATTATTGCCGTCAGCCGCGGCGGCCTGGTGCCGGGTGCGCTGCTGGCGCGTGAGCTGGGTATTCGCCACGTCGACACCGTGTGCATCAGCAGCTATGACCACGACAACCAGCGCGAGCTGAAAGTGCTGAAACGTGCGGAAGGCGATGGCGAAGGCTTTATCGTCATTGATGATCTGGTTGATACCGGCGGCACCGCGGTGGCGATCCGTGAAATGTACCCGAAAGCGCACTTTGTCACCATCTTTGCGAAACCGGCAGGCCGTCCGCTGGTTGATGATTATGTGGTTGATATTCCGCAGGACACCTGGATCGAACAGCCGTGGGACATGGGCGTGGTGTTTGTTCCGCCGATCAGCGGCCGCTAA
3
+ 1,MKNIIRTPETHPLTWRLRDDKQPVWLDEYRSKNGYEGARKALTGLSPDEIVNQVKDAGLKGRGGAGFSTGLKWSLMPKDESMNIRYLLCNADEMEPGTYKDRLLMEQLPHLLVEGMLISAFALKAYRGYIFLRGEYIEAAVNLRRAIAEATEAGLLGKNIMGTGFDFELFVHTGAGRYICGEETALINSLEGRRANPRSKPPFPATSGAWGKPTCVNNVETLCNVPAILANGVEWYQNISKSKDAGTKLMGFSGRVKNPGLWELPFGTTAREILEDYAGGMRDGLKFKAWQPGGAGTDFLTEAHLDLPMEFESIGKAGSRLGTALAMAVDHEINMVSLVRNLEEFFARESCGWCTPCRDGLPWSVKILRALERGEGQPGDIETLEQLCRFLGPGKTFCAHAPGAVEPLQSAIKYFREEFEAGIKQPFSNTHLINGIQPNLLKERW_,Escherichia coli general,ATGAAAAATATTATTAGAACACCTGAAACCCATCCGCTGACCTGGCGTCTGCGCGATGACAAACAGCCGGTGTGGCTGGATGAGTACCGCAGCAAAAACGGCTATGAAGGTGCGCGTAAAGCGCTGACCGGTCTGTCTCCGGATGAGATTGTCAATCAGGTCAAAGATGCCGGCCTGAAAGGCCGTGGCGGTGCGGGTTTCTCCACCGGCCTGAAGTGGTCTCTGATGCCGAAAGATGAGAGCATGAACATCCGCTATCTGCTGTGCAATGCCGATGAGATGGAGCCGGGCACCTATAAAGACCGCCTGCTGATGGAGCAGCTGCCGCACCTGCTGGTTGAAGGTATGCTGATCTCTGCATTTGCGCTGAAAGCCTACCGTGGCTACATCTTCCTGCGTGGCGAGTACATCGAAGCGGCGGTGAACCTGCGCCGTGCGATTGCTGAAGCCACTGAAGCAGGTCTGCTGGGTAAAAACATCATGGGTACCGGTTTTGACTTTGAACTGTTCGTCCACACCGGTGCAGGGCGCTACATCTGCGGTGAAGAAACCGCGCTGATCAACAGCCTGGAAGGCCGTCGTGCGAACCCGCGCAGCAAACCGCCGTTCCCGGCAACCTCTGGTGCGTGGGGTAAACCGACCTGTGTTAATAACGTTGAAACCCTGTGCAACGTTCCGGCGATTCTGGCGAACGGTGTGGAATGGTATCAGAACATCTCCAAAAGCAAAGATGCTGGTACCAAGCTGATGGGTTTCTCCGGCCGTGTGAAAAACCCGGGCCTGTGGGAACTGCCGTTTGGTACCACCGCGCGTGAAATCCTGGAAGATTATGCCGGTGGCATGCGTGACGGCCTGAAGTTCAAAGCGTGGCAGCCGGGTGGTGCAGGTACCGATTTCCTGACTGAAGCGCACCTGGATCTGCCGATGGAGTTTGAGTCCATTGGTAAAGCGGGCAGCCGTCTGGGTACAGCGCTGGCGATGGCGGTTGACCATGAGATCAACATGGTGTCGCTGGTGCGTAACCTGGAAGAGTTCTTTGCCCGTGAAAGCTGCGGCTGGTGCACGCCGTGCCGCGACGGGCTGCCGTGGTCAGTGAAAATCCTGCGTGCGCTGGAGCGCGGTGAAGGCCAGCCGGGTGACATCGAAACGCTGGAACAGCTGTGCCGCTTCCTGGGTCCGGGTAAAACCTTCTGTGCACATGCACCCGGTGCGGTGGAACCGCTGCAGTCTGCGATCAAATATTTCCGTGAAGAGTTTGAAGCGGGGATCAAACAGCCGTTCTCCAACACCCATCTGATTAACGGGATTCAGCCGAACCTGCTGAAAGAGCGCTGGTAA
4
+ 2,MDALQIAEDTLQTLVPHCPVPSGPRRIFLDANVKESYCPLVPHTMYCLPLWQGINLVLLTRSPSAPLALVLSQLMDGFSMLEKKLKEGPEPGASLRSQPLVGDLRQRMDKFVKNRGAQEIQSTWLEFKAKAFSKSEPGSSWELLQACGKLKRQLCAIYRLNFLTTAPSRGGPHLPQHLQDQVQRLMREKLTDWKDFLLVKSRRNITMVSYLEDFPGLVHFIYVDRTTGQMVAPSLNCSQKTSSELGKGPLAAFVKTKVWSLIQLARRYLQKGYTTLLFQEGDFYCSYFLWFENDMGYKLQMIEVPVLSDDSVPIGMLGGDYYRKLLRYYSKNRPTEAVRCYELLALHLSVIPTDLLVQQAGQLARRLWEASRIPLL_,Homo sapiens,ATGGATGCCCTGCAGATTGCTGAGGACACCCTGCAGACCCTGGTGCCCCACTGCCCTGTGCCCTCTGGGCCCAGGAGGATCTTCCTGGATGCCAATGTGAAGGAGAGCTACTGCCCCCTGGTGCCCCACACCATGTACTGCCTGCCCCTGTGGCAGGGCATCAACCTGGTCCTGCTGACCAGGTCTCCCTCTGCCCCCCTGGCCCTGGTGCTGTCCCAGCTGATGGATGGCTTTTCCATGCTGGAGAAGAAGCTGAAGGAGGGGCCCGAGCCTGGAGCCTCTCTGAGGAGCCAGCCCCTGGTGGGGGACCTGCGGCAGAGAATGGACAAATTTGTGAAGAACCGAGGGGCCCAGGAGATCCAGAGCACCTGGCTGGAATTCAAGGCCAAGGCCTTCTCCAAATCTGAGCCTGGCAGCAGCTGGGAGCTGCTGCAGGCCTGTGGGAAGCTGAAGAGACAGCTGTGTGCCATCTACAGGCTGAACTTCCTGACCACAGCCCCCTCCAGAGGAGGGCCCCACCTGCCCCAGCACCTGCAGGACCAGGTGCAGCGGCTGATGCGGGAGAAGCTGACTGACTGGAAGGACTTCCTGCTGGTGAAGAGCAGGAGGAACATCACCATGGTGTCCTACCTGGAGGACTTCCCTGGCCTGGTGCACTTCATCTATGTGGACAGGACCACTGGGCAGATGGTGGCCCCCAGCCTGAACTGCAGCCAGAAGACCAGCTCTGAGCTGGGCAAGGGGCCCCTGGCTGCCTTTGTGAAGACCAAAGTGTGGAGCCTGATCCAGCTGGCCCGGAGATACCTGCAGAAGGGCTATACCACCCTGCTGTTCCAGGAAGGAGACTTCTACTGCTCCTACTTCCTGTGGTTTGAGAATGATATGGGCTACAAGCTGCAGATGATTGAGGTGCCTGTGCTGTCTGATGACTCTGTCCCCATTGGCATGCTGGGAGGAGACTACTACCGGAAGCTGCTGCGCTATTACAGCAAGAACCGGCCCACTGAGGCTGTGCGCTGCTATGAGCTGCTGGCCCTGCACCTGTCTGTGATCCCCACTGACCTGCTGGTGCAGCAGGCTGGGCAGCTGGCCAGGAGGCTGTGGGAGGCCTCCAGGATCCCCCTGCTGTGA
5
+ 3,MAFANFRRILRLSTFEKRKSREYEHVRRDLDPNEVWEIVGELGDGAFGKVYKAKNKETGALAAAKVIETKSEEELEDYIVEIEILATCDHPYIVKLLGAYYHDGKLWIMIEFCPGGAVDAIMLELDRGLTEPQIQVVCRQMLEALNFLHSKRIIHRDLKAGNVLMTLEGDIRLADFGVSAKNLKTLQKRDSFIGTPYWMAPEVVMCETMKDTPYDYKADIWSLGITLIEMAQIEPPHHELNPMRVLLKIAKSDPPTLLTPSKWSVEFRDFLKIALDKNPETRPSAAQLLEHPFVSSITSNKALRELVAEAKAEVMEEIEDGRDEGEEEDAVDAASDPKLYKKTLKRTRKFVVDGVEVSITTSKIISEDEKKDEEMRFLRRQELRELRLLQKEEHRNQTQLSNKHELQLEQMHKRFEQEINAKKKFFDTELENLERQQKQQVEKMEQDHAVRRREEARRIRLEQDRDYTRFQEQLKLMKKEVKNEVEKLPRQQRKESMKQKMEEHTQKKQLLDRDFVAKQKEDLELAMKRLTTDNRREICDKERECLMKKQELLRDREAALWEMEEHQLQERHQLVKQQLKDQYFLQRHELLRKHEKEREQMQRYNQRMIEQLKVRQQQEKARLPKIQRSEGKTRMAMYKKSLHINGGGSAAEQREKIKQFSQQEEKRQKSERLQQQQKHENQMRDMLAQCESNMSELQQLQNEKCHLLVEHETQKLKALDESHNQNLKEWRDKLRPRKKALEEDLNQKKREQEMFFKLSEEAECPNPSTPSKAAKFFPYSSADAS_,Homo sapiens,ATGGCCTTTGCCAACTTCCGGAGAATCCTGCGGCTGTCCACCTTTGAGAAGAGGAAGAGCCGGGAATATGAGCACGTGCGCAGGGACCTGGACCCCAACGAGGTGTGGGAGATTGTGGGGGAGCTGGGGGATGGTGCCTTTGGGAAGGTCTACAAGGCCAAGAACAAGGAGACTGGGGCCTTGGCTGCGGCCAAGGTAATTGAGACCAAATCTGAGGAGGAGCTGGAAGACTACATTGTGGAGATTGAGATTCTGGCCACCTGTGACCACCCCTACATTGTGAAGCTGCTGGGGGCCTACTACCATGATGGCAAGCTGTGGATCATGATCGAGTTCTGCCCTGGAGGGGCTGTGGATGCCATTATGCTGGAGCTGGACCGAGGCCTGACTGAGCCACAGATCCAGGTGGTGTGCAGGCAGATGCTGGAGGCCCTGAACTTCCTGCACAGCAAGAGAATCATTCACAGAGACCTGAAGGCTGGGAACGTGTTGATGACCCTGGAAGGAGACATCCGCTTGGCTGACTTTGGTGTCTCTGCCAAGAACCTGAAGACCTTACAGAAGAGGGACAGCTTCATTGGCACACCCTACTGGATGGCCCCTGAGGTGGTCATGTGTGAGACCATGAAGGACACACCCTATGACTACAAGGCTGACATTTGGAGCCTGGGCATCACCTTGATTGAGATGGCCCAGATTGAGCCACCACACCACGAGCTGAACCCAATGCGCGTGCTGCTGAAGATTGCCAAGTCAGACCCACCCACCTTGTTGACACCAAGCAAGTGGTCTGTGGAGTTCCGGGACTTCCTGAAGATTGCCCTGGACAAGAACCCTGAAACCAGGCCCTCTGCTGCCCAGCTGCTGGAGCACCCCTTTGTCTCCTCCATCACCTCCAACAAGGCCCTGCGGGAGCTGGTGGCTGAGGCCAAGGCTGAGGTGATGGAGGAGATTGAGGATGGCAGAGATGAAGGAGAGGAGGAGGATGCTGTGGATGCCGCCTCTGACCCCAAGCTGTACAAGAAGACCCTGAAGAGGACCCGCAAGTTTGTGGTGGACGGGGTGGAGGTGTCTATCACCACCTCCAAGATCATCTCTGAGGATGAGAAGAAGGATGAAGAGATGAGGTTCCTGCGGCGCCAGGAGCTGCGGGAGCTGCGGCTGCTGCAGAAGGAGGAGCACAGGAACCAGACCCAGCTGTCCAACAAGCATGAGCTGCAGCTGGAGCAGATGCACAAGAGGTTTGAGCAGGAGATCAATGCCAAGAAGAAGTTCTTTGACACTGAGCTGGAGAACCTGGAGAGGCAGCAGAAGCAGCAGGTGGAGAAGATGGAGCAGGACCATGCTGTGCGGAGGCGGGAGGAGGCCCGGAGGATCCGCCTGGAGCAGGACCGGGACTACACCCGCTTCCAGGAGCAGCTGAAGCTGATGAAGAAGGAGGTCAAGAACGAGGTGGAGAAGCTGCCCCGGCAGCAGAGGAAGGAAAGCATGAAGCAGAAGATGGAGGAGCACACCCAGAAGAAGCAGCTGCTGGACAGAGACTTTGTGGCCAAGCAGAAGGAGGACCTGGAGCTGGCTATGAAGAGACTGACCACTGATAACAGGAGGGAGATCTGTGACAAGGAGCGGGAGTGCCTGATGAAGAAGCAGGAGCTGCTGCGGGACCGGGAGGCTGCCCTGTGGGAGATGGAGGAGCACCAGCTGCAGGAGAGGCACCAGCTGGTCAAGCAGCAGCTGAAGGACCAGTACTTCCTGCAGAGGCACGAGCTGCTGAGGAAGCATGAGAAGGAGCGGGAGCAGATGCAGAGGTACAACCAGAGGATGATTGAGCAGCTGAAGGTGCGGCAGCAGCAGGAGAAGGCCAGACTGCCCAAGATCCAGAGATCTGAGGGGAAGACAAGGATGGCCATGTACAAGAAGTCTCTGCACATCAATGGGGGGGGCTCTGCCGCTGAGCAGCGGGAGAAGATCAAGCAGTTCTCCCAGCAGGAGGAGAAGAGGCAGAAGAGTGAGAGGCTGCAGCAGCAGCAGAAGCACGAGAACCAGATGAGGGACATGCTGGCCCAGTGTGAGTCCAACATGTCTGAGCTGCAGCAGCTGCAGAACGAGAAGTGTCACCTGCTGGTGGAGCATGAGACCCAGAAGCTGAAGGCCCTGGATGAGAGCCACAACCAGAATCTGAAGGAGTGGAGGGACAAGCTGAGGCCCAGGAAGAAGGCCCTGGAGGAGGACCTGAACCAGAAGAAGCGGGAGCAGGAGATGTTCTTCAAGCTGTCTGAGGAGGCCGAGTGCCCAAACCCAAGCACTCCATCCAAGGCTGCCAAGTTCTTCCCCTACAGCTCTGCCGACGCCAGCTAA
6
+ 4,MTEKDAGGFNMSTFMNRKFQEPIQQIKTFSWMGFSWTCRKRRKHYQSYLRNGVRISVNDFVYVLAEQHKRLVAYIEDLYEDSKGKKMVVVRWFHKTEEVGSVLSDDDNDREIFFSLNRQDISIECIDYLATVLSPQHYEKFLKVPMHVQTVAFFCQKLYGDDGLKPYDITQLEGYWRQEMLRYLNVSILKSFEGAQAPGTDPGLKAPLVGCVGIRSRKRRRPSPVGTLNVSYAGDMKGDCKSSPDSVLAVTDASIFKGDEDGSSHHIKKGSLIEVLSEDSGIRGCWFKALVLKKHKDKVKVQYQDIQDADDESKKLEEWILTSRVAAGDHLGDLRIKGRKVVRPMLKPSKENDVCVIGVGMPVDVWWCDGWWEGIVVQEVSEEKFEVYLPGEKKMSAFHRNDLRQSREWLDDEWLNIRSRSDIVSSVLSLTKKKEMEVKHDEKSSDVGVCNGRMSPKTEAKRTISLPVATTKKSLPKRPIPDLLKDVLVTSDLKWKKSSRKRNRVVSCCPHDPSLNDGFSSERSLDCENCKFMEDTFGSSDGQHLTGLLMSR_,Arabidopsis thaliana,ATGACGGAGAAAGATGCTGGAGGTTTTAATATGTCAACTTTCATGAACAGGAAGTTTCAAGAACCAATTCAACAGATCAAAACTTTCTCCTGGATGGGTTTCTCATGGACTTGTAGGAAGAGGAGGAAACATTATCAATCTTACCTTAGGAATGGAGTGAGGATCTCTGTCAATGATTTTGTTTATGTTCTTGCTGAGCAACACAAGAGGCTTGTTGCTTACATTGAAGATCTTTATGAGGATAGCAAAGGGAAGAAGATGGTTGTTGTTAGGTGGTTCCACAAGACTGAAGAGGTTGGATCTGTTCTTAGCGATGATGACAACGACAGGGAGATCTTCTTCTCTCTCAACAGACAAGACATCAGCATTGAGTGCATTGATTACCTTGCCACTGTTCTCTCTCCTCAACATTACGAGAAGTTTCTCAAGGTTCCTATGCATGTTCAAACTGTTGCTTTCTTCTGCCAGAAGCTCTATGGAGATGATGGTTTGAAACCTTATGACATCACTCAGCTTGAAGGTTACTGGAGACAAGAAATGCTCAGATACCTCAATGTCTCCATTCTCAAGAGCTTTGAAGGAGCTCAAGCTCCTGGAACTGATCCTGGTTTGAAGGCTCCTTTGGTTGGTTGTGTTGGTATCAGAAGCAGGAAGAGGAGGAGACCATCACCGGTTGGAACTCTCAACGTCAGCTACGCTGGAGACATGAAAGGAGACTGCAAAAGCTCTCCTGATTCTGTTTTGGCTGTCACTGATGCTTCGATCTTCAAAGGAGATGAAGATGGATCTTCTCACCACATCAAGAAAGGAAGCTTGATTGAGGTTCTCAGCGAGGACTCTGGGATCCGTGGTTGCTGGTTCAAAGCTTTGGTGTTGAAGAAACACAAGGACAAGGTGAAGGTGCAGTACCAAGACATTCAAGATGCTGATGATGAGAGCAAGAAGCTTGAGGAGTGGATTCTCACTAGCCGTGTTGCTGCTGGAGATCATCTTGGTGATTTGAGGATCAAAGGAAGGAAAGTTGTGAGACCAATGCTCAAACCTTCCAAGGAGAACGATGTTTGTGTGATTGGTGTTGGAATGCCGGTTGATGTTTGGTGGTGTGATGGATGGTGGGAAGGGATTGTGGTTCAAGAGGTTTCTGAGGAGAAGTTTGAGGTTTATCTTCCTGGAGAGAAGAAAATGTCAGCTTTCCACAGAAATGATTTGAGACAAAGCAGAGAGTGGCTTGATGATGAGTGGCTCAACATTAGAAGCAGAAGTGACATTGTTTCTTCTGTTCTTTCTTTGACCAAGAAGAAAGAGATGGAGGTGAAGCATGATGAGAAAAGCAGCGATGTTGGTGTCTGCAATGGAAGAATGTCTCCAAAAACAGAAGCTAAGAGAACAATCTCTCTTCCTGTTGCTACAACCAAGAAATCTCTTCCTAAGAGACCAATTCCTGATCTTCTCAAGGATGTGTTGGTCACTTCTGATTTGAAGTGGAAGAAAAGCTCAAGGAAAAGAAACAGAGTTGTTTCCTGCTGTCCTCATGATCCATCTCTCAATGATGGTTTCTCCTCTGAGAGATCTCTTGATTGTGAGAACTGCAAGTTCATGGAAGACACTTTTGGTTCTTCTGATGGACAACATCTCACTGGTCTTCTTATGTCCAGATAA
scripts/finetune.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: finetune.py
3
+ -------------------
4
+ Finetune the CodonTransformer model.
5
+
6
+ The pretrained model is loaded directly from Hugging Face.
7
+ The dataset is a JSON file. You can use prepare_training_data from CodonData to
8
+ prepare the dataset. The repository README has a guide on how to prepare the
9
+ dataset and use this script.
10
+ """
11
+
12
+ import argparse
13
+ import gzip
14
+ import math
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
20
+ MODEL_DIR = PROJECT_ROOT / "model"
21
+ if str(MODEL_DIR) not in sys.path:
22
+ sys.path.insert(0, str(MODEL_DIR))
23
+
24
+ import pytorch_lightning as pl
25
+ import torch
26
+ from torch.utils.data import DataLoader
27
+ from transformers import AutoTokenizer, BigBirdForMaskedLM
28
+
29
+ from CodonTransformer.CodonUtils import (
30
+ MAX_LEN,
31
+ TOKEN2MASK,
32
+ IterableJSONData,
33
+ )
34
+
35
+
36
+ class MaskedTokenizerCollator:
37
+ def __init__(self, tokenizer):
38
+ self.tokenizer = tokenizer
39
+
40
+ def __call__(self, examples):
41
+ tokenized = self.tokenizer(
42
+ [ex["codons"] for ex in examples],
43
+ return_attention_mask=True,
44
+ return_token_type_ids=True,
45
+ truncation=True,
46
+ padding=True,
47
+ max_length=MAX_LEN,
48
+ return_tensors="pt",
49
+ )
50
+
51
+ seq_len = tokenized["input_ids"].shape[-1]
52
+ species_index = torch.tensor([[ex["organism"]] for ex in examples])
53
+ tokenized["token_type_ids"] = species_index.repeat(1, seq_len)
54
+
55
+ inputs = tokenized["input_ids"]
56
+ targets = tokenized["input_ids"].clone()
57
+
58
+ prob_matrix = torch.full(inputs.shape, 0.15)
59
+ prob_matrix[torch.where(inputs < 5)] = 0.0
60
+ selected = torch.bernoulli(prob_matrix).bool()
61
+
62
+ # 80% of the time, replace masked input tokens with respective mask tokens
63
+ replaced = torch.bernoulli(torch.full(selected.shape, 0.8)).bool() & selected
64
+ inputs[replaced] = torch.tensor(
65
+ list((map(TOKEN2MASK.__getitem__, inputs[replaced].numpy())))
66
+ )
67
+
68
+ # 10% of the time, we replace masked input tokens with random vector.
69
+ randomized = (
70
+ torch.bernoulli(torch.full(selected.shape, 0.1)).bool()
71
+ & selected
72
+ & ~replaced
73
+ )
74
+ random_idx = torch.randint(26, 90, prob_matrix.shape, dtype=torch.long)
75
+ inputs[randomized] = random_idx[randomized]
76
+
77
+ tokenized["input_ids"] = inputs
78
+ tokenized["labels"] = torch.where(selected, targets, -100)
79
+
80
+ return tokenized
81
+
82
+
83
+ class plTrainHarness(pl.LightningModule):
84
+ def __init__(self, model, learning_rate, warmup_fraction, total_training_steps):
85
+ super().__init__()
86
+ self.model = model
87
+ self.learning_rate = learning_rate
88
+ self.warmup_fraction = warmup_fraction
89
+ self.total_training_steps = total_training_steps
90
+
91
+ def configure_optimizers(self):
92
+ optimizer = torch.optim.AdamW(
93
+ self.model.parameters(),
94
+ lr=self.learning_rate,
95
+ )
96
+ total_steps = self.total_training_steps or self.trainer.estimated_stepping_batches
97
+ if total_steps <= 0:
98
+ raise ValueError(f"Expected positive integer total_steps, but got {total_steps}")
99
+ lr_scheduler = {
100
+ "scheduler": torch.optim.lr_scheduler.OneCycleLR(
101
+ optimizer,
102
+ max_lr=self.learning_rate,
103
+ total_steps=total_steps,
104
+ pct_start=self.warmup_fraction,
105
+ ),
106
+ "interval": "step",
107
+ "frequency": 1,
108
+ }
109
+ return [optimizer], [lr_scheduler]
110
+
111
+ def training_step(self, batch, batch_idx):
112
+ self.model.bert.set_attention_type("block_sparse")
113
+ outputs = self.model(**batch)
114
+ self.log_dict(
115
+ dictionary={
116
+ "loss": outputs.loss,
117
+ "lr": self.trainer.optimizers[0].param_groups[0]["lr"],
118
+ },
119
+ on_step=True,
120
+ prog_bar=True,
121
+ )
122
+ return outputs.loss
123
+
124
+
125
+ class DumpStateDict(pl.callbacks.ModelCheckpoint):
126
+ def __init__(self, checkpoint_dir, checkpoint_filename, every_n_train_steps):
127
+ super().__init__(
128
+ dirpath=checkpoint_dir, every_n_train_steps=every_n_train_steps
129
+ )
130
+ self.checkpoint_filename = checkpoint_filename
131
+
132
+ def on_save_checkpoint(self, trainer, pl_module, checkpoint):
133
+ model = pl_module.model
134
+ torch.save(
135
+ model.state_dict(), os.path.join(self.dirpath, self.checkpoint_filename)
136
+ )
137
+
138
+
139
+ def count_jsonl_records(path):
140
+ open_fn = gzip.open if path.endswith(".gz") else open
141
+ with open_fn(path, "rt") as file:
142
+ return sum(1 for line in file if line.strip())
143
+
144
+
145
+ def estimate_training_steps(args):
146
+ num_records = count_jsonl_records(args.dataset_dir)
147
+ num_devices = 1 if args.debug else args.num_gpus
148
+ samples_per_step = max(1, args.batch_size * num_devices)
149
+ batches_per_epoch = math.ceil(num_records / samples_per_step)
150
+ optimizer_steps_per_epoch = math.ceil(
151
+ batches_per_epoch / max(1, args.accumulate_grad_batches)
152
+ )
153
+ total_steps = max(1, optimizer_steps_per_epoch * args.max_epochs)
154
+ print(
155
+ "Estimated training steps: "
156
+ f"{total_steps} "
157
+ f"({num_records} records, batch_size={args.batch_size}, "
158
+ f"devices={num_devices}, max_epochs={args.max_epochs}, "
159
+ f"accumulate_grad_batches={args.accumulate_grad_batches})"
160
+ )
161
+ return total_steps
162
+
163
+
164
+ def main(args):
165
+ """Finetune the CodonTransformer model."""
166
+ pl.seed_everything(args.seed)
167
+ torch.set_float32_matmul_precision("medium")
168
+ total_training_steps = estimate_training_steps(args)
169
+
170
+ # Load the tokenizer and model
171
+ tokenizer = AutoTokenizer.from_pretrained("adibvafa/CodonTransformer")
172
+ model = BigBirdForMaskedLM.from_pretrained("adibvafa/CodonTransformer-base")
173
+ harnessed_model = plTrainHarness(
174
+ model,
175
+ args.learning_rate,
176
+ args.warmup_fraction,
177
+ total_training_steps,
178
+ )
179
+
180
+ # Load the training data
181
+ train_data = IterableJSONData(args.dataset_dir, dist_env="slurm")
182
+ data_loader = DataLoader(
183
+ dataset=train_data,
184
+ collate_fn=MaskedTokenizerCollator(tokenizer),
185
+ batch_size=args.batch_size,
186
+ num_workers=0 if args.debug else args.num_workers,
187
+ persistent_workers=False if args.debug else True,
188
+ )
189
+
190
+ # Setup trainer and callbacks
191
+ save_checkpoint = DumpStateDict(
192
+ checkpoint_dir=args.checkpoint_dir,
193
+ checkpoint_filename=args.checkpoint_filename,
194
+ every_n_train_steps=args.save_every_n_steps,
195
+ )
196
+ trainer = pl.Trainer(
197
+ default_root_dir=args.checkpoint_dir,
198
+ strategy="ddp_find_unused_parameters_true",
199
+ accelerator="gpu",
200
+ devices=1 if args.debug else args.num_gpus,
201
+ precision="16-mixed",
202
+ max_epochs=args.max_epochs,
203
+ deterministic=False,
204
+ enable_checkpointing=True,
205
+ callbacks=[save_checkpoint],
206
+ accumulate_grad_batches=args.accumulate_grad_batches,
207
+ )
208
+
209
+ # Finetune the model
210
+ trainer.fit(harnessed_model, data_loader)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ parser = argparse.ArgumentParser(description="Finetune the CodonTransformer model.")
215
+ parser.add_argument(
216
+ "--dataset_dir",
217
+ type=str,
218
+ required=True,
219
+ help="Directory containing the dataset",
220
+ )
221
+ parser.add_argument(
222
+ "--checkpoint_dir",
223
+ type=str,
224
+ required=True,
225
+ help="Directory where checkpoints will be saved",
226
+ )
227
+ parser.add_argument(
228
+ "--checkpoint_filename",
229
+ type=str,
230
+ default="finetune.ckpt",
231
+ help="Filename for the saved checkpoint",
232
+ )
233
+ parser.add_argument(
234
+ "--batch_size", type=int, default=6, help="Batch size for training"
235
+ )
236
+ parser.add_argument(
237
+ "--max_epochs", type=int, default=15, help="Maximum number of epochs to train"
238
+ )
239
+ parser.add_argument(
240
+ "--num_workers", type=int, default=5, help="Number of workers for data loading"
241
+ )
242
+ parser.add_argument(
243
+ "--accumulate_grad_batches",
244
+ type=int,
245
+ default=1,
246
+ help="Number of batches to accumulate gradients",
247
+ )
248
+ parser.add_argument(
249
+ "--num_gpus", type=int, default=4, help="Number of GPUs to use for training"
250
+ )
251
+ parser.add_argument(
252
+ "--learning_rate",
253
+ type=float,
254
+ default=5e-5,
255
+ help="Learning rate for the optimizer",
256
+ )
257
+ parser.add_argument(
258
+ "--warmup_fraction",
259
+ type=float,
260
+ default=0.1,
261
+ help="Fraction of total steps to use for warmup",
262
+ )
263
+ parser.add_argument(
264
+ "--save_every_n_steps",
265
+ type=int,
266
+ default=512,
267
+ help="Save checkpoint every N steps",
268
+ )
269
+ parser.add_argument(
270
+ "--seed", type=int, default=123, help="Random seed for reproducibility"
271
+ )
272
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode")
273
+ args = parser.parse_args()
274
+ main(args)
scripts/notebooks/CodonTransformerDemo.ipynb ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "import warnings\n",
10
+ "from tqdm import tqdm\n",
11
+ "\n",
12
+ "import pandas as pd\n",
13
+ "import torch\n",
14
+ "from transformers import AutoTokenizer, BigBirdForMaskedLM\n",
15
+ "\n",
16
+ "from CodonTransformer.CodonJupyter import (\n",
17
+ " UserContainer,\n",
18
+ " display_organism_dropdown,\n",
19
+ " display_protein_input,\n",
20
+ " format_model_output,\n",
21
+ ")\n",
22
+ "from CodonTransformer.CodonPrediction import predict_dna_sequence\n",
23
+ "\n",
24
+ "warnings.filterwarnings(\"ignore\")\n",
25
+ "\n",
26
+ "DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "# Load model and tokenizer\n",
36
+ "tokenizer = AutoTokenizer.from_pretrained(\"adibvafa/CodonTransformer\")\n",
37
+ "model = BigBirdForMaskedLM.from_pretrained(\"adibvafa/CodonTransformer\").to(DEVICE)"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "markdown",
42
+ "metadata": {},
43
+ "source": [
44
+ "**Optimizing a Single Sequence**\n",
45
+ "-------------------------------------\n",
46
+ "1. Run the next code cell and input only your protein sequence and organism\n",
47
+ "\n",
48
+ "2. Run the code cell after it to optimize the sequence and display it.\n",
49
+ "\n",
50
+ "Protein sequences should end with \"*\" or \"_\" or an amino acid."
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "metadata": {},
57
+ "outputs": [],
58
+ "source": [
59
+ "# Sample: MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAEDLQVGQVELGG, Homo sapiens\n",
60
+ "user = UserContainer()\n",
61
+ "display_protein_input(user)\n",
62
+ "display_organism_dropdown(user)"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": null,
68
+ "metadata": {},
69
+ "outputs": [],
70
+ "source": [
71
+ "output = predict_dna_sequence(\n",
72
+ " protein=user.protein,\n",
73
+ " organism=user.organism,\n",
74
+ " device=DEVICE,\n",
75
+ " tokenizer=tokenizer,\n",
76
+ " model=model,\n",
77
+ " attention_type=\"original_full\",\n",
78
+ " deterministic=True,\n",
79
+ " # Can set temperature for non deterministic prediction\n",
80
+ ")\n",
81
+ "\n",
82
+ "print(format_model_output(output))"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "markdown",
87
+ "metadata": {},
88
+ "source": [
89
+ "**Optimizing Multiple Sequences**\n",
90
+ "-------------------------------------\n",
91
+ "1. Create a CSV file that has columns 'protein_sequence' and 'organism'.\n",
92
+ " You can have other columns in any order.\n",
93
+ "\n",
94
+ "2. Replace the _dataset_path_ below with the actual path to your CSV file.\n",
95
+ "\n",
96
+ "3. Run the next code cells to optimize and save the predicted DNA sequences."
97
+ ]
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": null,
102
+ "metadata": {},
103
+ "outputs": [],
104
+ "source": [
105
+ "# Update with the actual path to your dataset\n",
106
+ "dataset_path = \"scripts/demo/sample_dataset.csv\"\n",
107
+ "output_path = \"scripts/demo/sample_predictions.csv\"\n",
108
+ "\n",
109
+ "dataset = pd.read_csv(dataset_path, index_col=0)\n",
110
+ "dataset[\"predicted_dna\"] = None\n",
111
+ "dataset.head()"
112
+ ]
113
+ },
114
+ {
115
+ "cell_type": "code",
116
+ "execution_count": null,
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "for index, data in tqdm(\n",
121
+ " dataset.iterrows(),\n",
122
+ " desc=f\"CodonTransformer Predicting\",\n",
123
+ " unit=\" Sequences\",\n",
124
+ " total=dataset.shape[0],\n",
125
+ "):\n",
126
+ "\n",
127
+ " outputs = predict_dna_sequence(\n",
128
+ " protein=data[\"protein_sequence\"],\n",
129
+ " organism=data[\"organism\"],\n",
130
+ " device=DEVICE,\n",
131
+ " tokenizer_object=tokenizer,\n",
132
+ " model_object=model,\n",
133
+ " )\n",
134
+ " dataset.loc[index, \"predicted_dna\"] = outputs.predicted_dna\n",
135
+ "\n",
136
+ "dataset.to_csv(output_path)\n",
137
+ "dataset.head()"
138
+ ]
139
+ }
140
+ ],
141
+ "metadata": {
142
+ "kernelspec": {
143
+ "display_name": "light",
144
+ "language": "python",
145
+ "name": "python3"
146
+ },
147
+ "language_info": {
148
+ "codemirror_mode": {
149
+ "name": "ipython",
150
+ "version": 3
151
+ },
152
+ "file_extension": ".py",
153
+ "mimetype": "text/x-python",
154
+ "name": "python",
155
+ "nbconvert_exporter": "python",
156
+ "pygments_lexer": "ipython3",
157
+ "version": "3.12.2"
158
+ }
159
+ },
160
+ "nbformat": 4,
161
+ "nbformat_minor": 2
162
+ }
scripts/pretrain.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File: pretrain.py
3
+ -------------------
4
+ Pretrain the CodonTransformer model.
5
+
6
+ The dataset is a JSON file. You can use prepare_training_data from CodonData to
7
+ prepare the dataset. The repository README has a guide on how to prepare the
8
+ dataset and use this script.
9
+ """
10
+
11
+ import argparse
12
+ import gzip
13
+ import math
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
19
+ MODEL_DIR = PROJECT_ROOT / "model"
20
+ if str(MODEL_DIR) not in sys.path:
21
+ sys.path.insert(0, str(MODEL_DIR))
22
+
23
+ import pytorch_lightning as pl
24
+ import torch
25
+ from torch.utils.data import DataLoader
26
+ from transformers import BigBirdConfig, BigBirdForMaskedLM, PreTrainedTokenizerFast
27
+
28
+ from CodonTransformer.CodonUtils import (
29
+ MAX_LEN,
30
+ NUM_ORGANISMS,
31
+ TOKEN2MASK,
32
+ IterableJSONData,
33
+ )
34
+
35
+
36
+ class MaskedTokenizerCollator:
37
+ def __init__(self, tokenizer):
38
+ self.tokenizer = tokenizer
39
+
40
+ def __call__(self, examples):
41
+ tokenized = self.tokenizer(
42
+ [ex["codons"] for ex in examples],
43
+ return_attention_mask=True,
44
+ return_token_type_ids=True,
45
+ truncation=True,
46
+ padding=True,
47
+ max_length=MAX_LEN,
48
+ return_tensors="pt",
49
+ )
50
+
51
+ seq_len = tokenized["input_ids"].shape[-1]
52
+ species_index = torch.tensor([[ex["organism"]] for ex in examples])
53
+ tokenized["token_type_ids"] = species_index.repeat(1, seq_len)
54
+
55
+ inputs = tokenized["input_ids"]
56
+ targets = inputs.clone()
57
+
58
+ prob_matrix = torch.full(inputs.shape, 0.15)
59
+ prob_matrix[inputs < 5] = 0.0
60
+ selected = torch.bernoulli(prob_matrix).bool()
61
+
62
+ # 80% of the time, replace masked input tokens with respective mask tokens
63
+ replaced = torch.bernoulli(torch.full(selected.shape, 0.8)).bool() & selected
64
+ inputs[replaced] = torch.tensor(
65
+ list((map(TOKEN2MASK.__getitem__, inputs[replaced].numpy())))
66
+ )
67
+
68
+ # 10% of the time, we replace masked input tokens with random vector.
69
+ randomized = (
70
+ torch.bernoulli(torch.full(selected.shape, 0.1)).bool()
71
+ & selected
72
+ & ~replaced
73
+ )
74
+ random_idx = torch.randint(26, 90, inputs.shape, dtype=torch.long)
75
+ inputs[randomized] = random_idx[randomized]
76
+
77
+ tokenized["input_ids"] = inputs
78
+ tokenized["labels"] = torch.where(selected, targets, -100)
79
+
80
+ return tokenized
81
+
82
+
83
+ class plTrainHarness(pl.LightningModule):
84
+ def __init__(self, model, learning_rate, warmup_fraction, total_training_steps):
85
+ super().__init__()
86
+ self.model = model
87
+ self.learning_rate = learning_rate
88
+ self.warmup_fraction = warmup_fraction
89
+ self.total_training_steps = total_training_steps
90
+
91
+ def configure_optimizers(self):
92
+ optimizer = torch.optim.AdamW(
93
+ self.model.parameters(),
94
+ lr=self.learning_rate,
95
+ )
96
+ total_steps = self.total_training_steps or self.trainer.estimated_stepping_batches
97
+ if total_steps <= 0:
98
+ raise ValueError(f"Expected positive integer total_steps, but got {total_steps}")
99
+ lr_scheduler = {
100
+ "scheduler": torch.optim.lr_scheduler.OneCycleLR(
101
+ optimizer,
102
+ max_lr=self.learning_rate,
103
+ total_steps=total_steps,
104
+ pct_start=self.warmup_fraction,
105
+ ),
106
+ "interval": "step",
107
+ "frequency": 1,
108
+ }
109
+ return [optimizer], [lr_scheduler]
110
+
111
+ def training_step(self, batch, batch_idx):
112
+ self.model.bert.set_attention_type("block_sparse")
113
+ outputs = self.model(**batch)
114
+ self.log_dict(
115
+ dictionary={
116
+ "loss": outputs.loss,
117
+ "lr": self.trainer.optimizers[0].param_groups[0]["lr"],
118
+ },
119
+ on_step=True,
120
+ prog_bar=True,
121
+ )
122
+ return outputs.loss
123
+
124
+
125
+ class EpochCheckpoint(pl.Callback):
126
+ def __init__(self, checkpoint_dir, save_interval):
127
+ super().__init__()
128
+ self.checkpoint_dir = checkpoint_dir
129
+ self.save_interval = save_interval
130
+
131
+ def on_train_epoch_end(self, trainer, pl_module):
132
+ current_epoch = trainer.current_epoch
133
+ if current_epoch % self.save_interval == 0 or current_epoch == 0:
134
+ checkpoint_path = os.path.join(
135
+ self.checkpoint_dir, f"epoch_{current_epoch}.ckpt"
136
+ )
137
+ trainer.save_checkpoint(checkpoint_path)
138
+ print(f"\nCheckpoint saved at {checkpoint_path}\n")
139
+
140
+
141
+ def count_jsonl_records(path):
142
+ open_fn = gzip.open if path.endswith(".gz") else open
143
+ with open_fn(path, "rt") as file:
144
+ return sum(1 for line in file if line.strip())
145
+
146
+
147
+ def estimate_training_steps(args):
148
+ num_records = count_jsonl_records(args.train_data_path)
149
+ num_devices = 1 if args.debug else args.num_gpus
150
+ samples_per_step = max(1, args.batch_size * num_devices)
151
+ batches_per_epoch = math.ceil(num_records / samples_per_step)
152
+ optimizer_steps_per_epoch = math.ceil(
153
+ batches_per_epoch / max(1, args.accumulate_grad_batches)
154
+ )
155
+ total_steps = max(1, optimizer_steps_per_epoch * args.max_epochs)
156
+ print(
157
+ "Estimated training steps: "
158
+ f"{total_steps} "
159
+ f"({num_records} records, batch_size={args.batch_size}, "
160
+ f"devices={num_devices}, max_epochs={args.max_epochs}, "
161
+ f"accumulate_grad_batches={args.accumulate_grad_batches})"
162
+ )
163
+ return total_steps
164
+
165
+
166
+ def main(args):
167
+ """Pretrain the CodonTransformer model."""
168
+ pl.seed_everything(args.seed)
169
+ torch.set_float32_matmul_precision("medium")
170
+ total_training_steps = estimate_training_steps(args)
171
+
172
+ # Load the tokenizer and model
173
+ tokenizer = PreTrainedTokenizerFast(
174
+ tokenizer_file=args.tokenizer_path,
175
+ bos_token="[CLS]",
176
+ eos_token="[SEP]",
177
+ unk_token="[UNK]",
178
+ sep_token="[SEP]",
179
+ pad_token="[PAD]",
180
+ cls_token="[CLS]",
181
+ mask_token="[MASK]",
182
+ )
183
+ config = BigBirdConfig(
184
+ vocab_size=len(tokenizer),
185
+ type_vocab_size=NUM_ORGANISMS,
186
+ sep_token_id=2,
187
+ )
188
+ model = BigBirdForMaskedLM(config=config)
189
+ harnessed_model = plTrainHarness(
190
+ model,
191
+ args.learning_rate,
192
+ args.warmup_fraction,
193
+ total_training_steps,
194
+ )
195
+
196
+ # Load the training data
197
+ train_data = IterableJSONData(args.train_data_path, dist_env="slurm")
198
+ data_loader = DataLoader(
199
+ dataset=train_data,
200
+ collate_fn=MaskedTokenizerCollator(tokenizer),
201
+ batch_size=args.batch_size,
202
+ num_workers=0 if args.debug else args.num_workers,
203
+ persistent_workers=False if args.debug else True,
204
+ )
205
+
206
+ # Setup trainer and callbacks
207
+ save_checkpoint = EpochCheckpoint(args.checkpoint_dir, args.save_interval)
208
+ trainer = pl.Trainer(
209
+ default_root_dir=args.checkpoint_dir,
210
+ strategy="ddp_find_unused_parameters_true",
211
+ accelerator="gpu",
212
+ devices=1 if args.debug else args.num_gpus,
213
+ precision="16-mixed",
214
+ max_epochs=args.max_epochs,
215
+ deterministic=False,
216
+ enable_checkpointing=True,
217
+ callbacks=[save_checkpoint],
218
+ accumulate_grad_batches=args.accumulate_grad_batches,
219
+ )
220
+
221
+ # Pretrain the model
222
+ trainer.fit(harnessed_model, data_loader)
223
+
224
+
225
+ if __name__ == "__main__":
226
+ parser = argparse.ArgumentParser(description="Pretrain the CodonTransformer model.")
227
+ parser.add_argument(
228
+ "--tokenizer_path",
229
+ type=str,
230
+ required=True,
231
+ help="Path to the tokenizer model file",
232
+ )
233
+ parser.add_argument(
234
+ "--train_data_path",
235
+ type=str,
236
+ required=True,
237
+ help="Path to the training data JSON file",
238
+ )
239
+ parser.add_argument(
240
+ "--checkpoint_dir",
241
+ type=str,
242
+ required=True,
243
+ help="Directory where checkpoints will be saved",
244
+ )
245
+ parser.add_argument(
246
+ "--batch_size", type=int, default=6, help="Batch size for training"
247
+ )
248
+ parser.add_argument(
249
+ "--max_epochs", type=int, default=5, help="Maximum number of epochs to train"
250
+ )
251
+ parser.add_argument(
252
+ "--num_workers", type=int, default=5, help="Number of workers for data loading"
253
+ )
254
+ parser.add_argument(
255
+ "--accumulate_grad_batches",
256
+ type=int,
257
+ default=1,
258
+ help="Number of batches to accumulate gradients",
259
+ )
260
+ parser.add_argument(
261
+ "--num_gpus", type=int, default=16, help="Number of GPUs to use for training"
262
+ )
263
+ parser.add_argument(
264
+ "--learning_rate",
265
+ type=float,
266
+ default=5e-5,
267
+ help="Learning rate for the optimizer",
268
+ )
269
+ parser.add_argument(
270
+ "--warmup_fraction",
271
+ type=float,
272
+ default=0.1,
273
+ help="Fraction of total steps to use for warmup",
274
+ )
275
+ parser.add_argument(
276
+ "--save_interval", type=int, default=5, help="Save checkpoint every N epochs"
277
+ )
278
+ parser.add_argument(
279
+ "--seed", type=int, default=123, help="Random seed for reproducibility"
280
+ )
281
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode")
282
+ args = parser.parse_args()
283
+ main(args)
scripts/slurm/export_finetuned_model.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Export a Lightning .ckpt checkpoint to a plain .pt model file for inference.
5
+ # Run this after scripts/slurm/run_finetune.sh.
6
+ # This script does not request SLURM resources.
7
+
8
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ CHECKPOINT_DIR="${CHECKPOINT_DIR:-${PROJECT_DIR}/weight/checkpoints/finetune}"
12
+ CHECKPOINT_PATH="${CHECKPOINT_PATH:-}"
13
+ OUTPUT_MODEL_PATH="${OUTPUT_MODEL_PATH:-${CHECKPOINT_DIR}/finetuned_model.pt}"
14
+ NUM_ORGANISMS="${NUM_ORGANISMS:-164}"
15
+
16
+ cd "${PROJECT_DIR}"
17
+
18
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
19
+ export CHECKPOINT_DIR
20
+ export CHECKPOINT_PATH
21
+ export OUTPUT_MODEL_PATH
22
+ export NUM_ORGANISMS
23
+ export PYTHONFAULTHANDLER=1
24
+
25
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
26
+ # shellcheck disable=SC1091
27
+ source "$(conda info --base)/etc/profile.d/conda.sh"
28
+ conda activate "${CONDA_ENV}"
29
+ fi
30
+
31
+ if [[ -z "${CHECKPOINT_PATH}" ]]; then
32
+ CHECKPOINT_PATH="$(find "${CHECKPOINT_DIR}" -maxdepth 1 -type f -name 'epoch=*-step=*.ckpt' | sort | tail -n 1)"
33
+ fi
34
+
35
+ if [[ -z "${CHECKPOINT_PATH}" ]] || [[ ! -f "${CHECKPOINT_PATH}" ]]; then
36
+ echo "Missing CHECKPOINT_PATH. Set it explicitly or check CHECKPOINT_DIR: ${CHECKPOINT_DIR}" >&2
37
+ exit 1
38
+ fi
39
+
40
+ mkdir -p "$(dirname "${OUTPUT_MODEL_PATH}")"
41
+ export CHECKPOINT_PATH
42
+
43
+ python - <<'PY'
44
+ import os
45
+
46
+ from CodonTransformer.CodonPrediction import create_model_from_checkpoint
47
+
48
+ checkpoint_path = os.environ["CHECKPOINT_PATH"]
49
+ output_model_path = os.environ["OUTPUT_MODEL_PATH"]
50
+ num_organisms = int(os.environ["NUM_ORGANISMS"])
51
+
52
+ print(f"Checkpoint: {checkpoint_path}")
53
+ print(f"Output model: {output_model_path}")
54
+ print(f"Num organisms: {num_organisms}")
55
+
56
+ create_model_from_checkpoint(
57
+ checkpoint_dir=checkpoint_path,
58
+ output_model_dir=output_model_path,
59
+ num_organisms=num_organisms,
60
+ )
61
+ PY
scripts/slurm/finetune.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=finetune
3
+ #SBATCH --output=your_output_directory/output_%j.out
4
+ #SBATCH --error=your_error_directory/error_%j.err
5
+ #SBATCH --nodes=1
6
+ #SBATCH --ntasks-per-node=4
7
+ #SBATCH --gpus-per-node=4
8
+ #SBATCH --cpus-per-task=6
9
+ #SBATCH --time=15:00:00
10
+ #SBATCH --partition=compute_full_node
11
+
12
+ # Load required modules
13
+ module --ignore_cache load cuda/11.4.4
14
+ module --ignore_cache load anaconda3
15
+ source activate your_environment
16
+
17
+ # Change to the working directory
18
+ cd your_working_directory
19
+
20
+ # Set environment variables
21
+ export CUBLAS_WORKSPACE_CONFIG=:4096:2
22
+ export PYTHONPATH="${PWD}/model:${PYTHONPATH:-}"
23
+ export NCCL_DEBUG=INFO
24
+ export PYTHONFAULTHANDLER=1
25
+
26
+ # Run the Python script with arguments
27
+ stdbuf -oL -eL srun python scripts/finetune.py \
28
+ --dataset_dir your_dataset_directory \
29
+ --checkpoint_dir your_checkpoint_directory \
30
+ --checkpoint_filename finetune.ckpt \
31
+ --batch_size 6 \
32
+ --max_epochs 15 \
33
+ --num_workers 5 \
34
+ --accumulate_grad_batches 1 \
35
+ --num_gpus 4 \
36
+ --learning_rate 0.00005 \
37
+ --warmup_fraction 0.1 \
38
+ --save_every_n_steps 512 \
39
+ --seed 123
scripts/slurm/prepare_finetune_data.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Convert a user-provided finetuning CSV into the JSONL format required by finetuning.
5
+ # Run this before scripts/slurm/run_finetune.sh.
6
+ # This script does not request SLURM resources.
7
+
8
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ INPUT_CSV="${INPUT_CSV:-${PROJECT_DIR}/scripts/data/raw/your_data.csv}"
12
+ OUTPUT_JSON="${OUTPUT_JSON:-${PROJECT_DIR}/scripts/data/processed/finetune_data.json}"
13
+
14
+ cd "${PROJECT_DIR}"
15
+ mkdir -p "$(dirname "${OUTPUT_JSON}")"
16
+
17
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
18
+ export INPUT_CSV
19
+ export OUTPUT_JSON
20
+ export PYTHONFAULTHANDLER=1
21
+
22
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
23
+ # shellcheck disable=SC1091
24
+ source "$(conda info --base)/etc/profile.d/conda.sh"
25
+ conda activate "${CONDA_ENV}"
26
+ fi
27
+
28
+ if [[ ! -f "${INPUT_CSV}" ]]; then
29
+ echo "Missing INPUT_CSV: ${INPUT_CSV}" >&2
30
+ exit 1
31
+ fi
32
+
33
+ python - <<'PY'
34
+ import os
35
+
36
+ from CodonTransformer.CodonData import prepare_training_data
37
+
38
+ input_csv = os.environ["INPUT_CSV"]
39
+ output_json = os.environ["OUTPUT_JSON"]
40
+
41
+ print(f"Input CSV: {input_csv}")
42
+ print(f"Output JSONL: {output_json}")
43
+
44
+ prepare_training_data(input_csv, output_json)
45
+ PY
scripts/slurm/prepare_pretrain_data.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Convert the full CSV dataset into the JSONL format required by pretraining.
5
+ # Run this before scripts/slurm/run_pretrain.sh.
6
+ # This script does not request SLURM resources.
7
+
8
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ INPUT_CSV="${INPUT_CSV:-${PROJECT_DIR}/scripts/data/raw/dataset.csv}"
12
+ OUTPUT_JSON="${OUTPUT_JSON:-${PROJECT_DIR}/scripts/data/processed/pretrain_data.json}"
13
+
14
+ cd "${PROJECT_DIR}"
15
+ mkdir -p "$(dirname "${OUTPUT_JSON}")"
16
+
17
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
18
+ export INPUT_CSV
19
+ export OUTPUT_JSON
20
+ export PYTHONFAULTHANDLER=1
21
+
22
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
23
+ # shellcheck disable=SC1091
24
+ source "$(conda info --base)/etc/profile.d/conda.sh"
25
+ conda activate "${CONDA_ENV}"
26
+ fi
27
+
28
+ if [[ ! -f "${INPUT_CSV}" ]]; then
29
+ echo "Missing INPUT_CSV: ${INPUT_CSV}" >&2
30
+ exit 1
31
+ fi
32
+
33
+ python - <<'PY'
34
+ import os
35
+
36
+ from CodonTransformer.CodonData import prepare_training_data
37
+
38
+ input_csv = os.environ["INPUT_CSV"]
39
+ output_json = os.environ["OUTPUT_JSON"]
40
+
41
+ print(f"Input CSV: {input_csv}")
42
+ print(f"Output JSONL: {output_json}")
43
+
44
+ prepare_training_data(input_csv, output_json)
45
+ PY
scripts/slurm/pretrain.sh ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=pretrain
3
+ #SBATCH --output=your_output_directory/output_%j.out
4
+ #SBATCH --error=your_error_directory/error_%j.err
5
+ #SBATCH --nodes=4
6
+ #SBATCH --gpus-per-node=4
7
+ #SBATCH --ntasks-per-node=4
8
+ #SBATCH --time=23:59:00
9
+ #SBATCH -p compute_full_node
10
+
11
+ # Load required modules
12
+ module --ignore_cache load cuda/11.4.4
13
+ module --ignore_cache load anaconda3
14
+ source activate your_environment
15
+
16
+ # Change to the working directory
17
+ cd your_working_directory
18
+
19
+ # Set environment variables
20
+ export CUBLAS_WORKSPACE_CONFIG=:4096:2
21
+ export PYTHONPATH="${PWD}/model:${PYTHONPATH:-}"
22
+ export NCCL_DEBUG=INFO
23
+ export PYTHONFAULTHANDLER=1
24
+
25
+ # Run the Python script with arguments
26
+ stdbuf -oL -eL srun python scripts/pretrain.py \
27
+ --tokenizer_path model/src/CodonTransformerTokenizer.json \
28
+ --train_data_path your_data_directory/pretrain_dataset.json \
29
+ --checkpoint_dir your_checkpoint_directory \
30
+ --batch_size 6 \
31
+ --max_epochs 5 \
32
+ --num_workers 5 \
33
+ --accumulate_grad_batches 1 \
34
+ --num_gpus 16 \
35
+ --learning_rate 0.00005 \
36
+ --warmup_fraction 0.1 \
37
+ --save_interval 5 \
38
+ --seed 123
scripts/slurm/run_finetune.sh ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Run this script inside an allocated/interactive GPU session.
5
+ # It does not request SLURM resources.
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
8
+ HF_HOME="${HF_HOME:-/public/home/scnb9biwet/.cache/huggingface}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ DATASET_JSON="${DATASET_JSON:-${PROJECT_DIR}/scripts/data/processed/finetune_data.json}"
12
+ CHECKPOINT_DIR="${CHECKPOINT_DIR:-${PROJECT_DIR}/weight/checkpoints/finetune}"
13
+ CHECKPOINT_FILENAME="${CHECKPOINT_FILENAME:-finetune.ckpt}"
14
+
15
+ BATCH_SIZE="${BATCH_SIZE:-6}"
16
+ MAX_EPOCHS="${MAX_EPOCHS:-15}"
17
+ NUM_WORKERS="${NUM_WORKERS:-5}"
18
+ ACCUMULATE_GRAD_BATCHES="${ACCUMULATE_GRAD_BATCHES:-1}"
19
+ NUM_GPUS="${NUM_GPUS:-4}"
20
+ LEARNING_RATE="${LEARNING_RATE:-0.00005}"
21
+ WARMUP_FRACTION="${WARMUP_FRACTION:-0.1}"
22
+ SAVE_EVERY_N_STEPS="${SAVE_EVERY_N_STEPS:-512}"
23
+ SEED="${SEED:-123}"
24
+ DEBUG="${DEBUG:-0}"
25
+ OFFLINE="${OFFLINE:-1}"
26
+
27
+ cd "${PROJECT_DIR}"
28
+ mkdir -p "${CHECKPOINT_DIR}"
29
+
30
+ export HF_HOME
31
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
32
+ export CUBLAS_WORKSPACE_CONFIG=:4096:2
33
+ export NCCL_DEBUG="${NCCL_DEBUG:-INFO}"
34
+ export PYTHONFAULTHANDLER=1
35
+
36
+ if [[ "${OFFLINE}" == "1" ]]; then
37
+ export HF_HUB_OFFLINE=1
38
+ export TRANSFORMERS_OFFLINE=1
39
+ fi
40
+
41
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
42
+ # shellcheck disable=SC1091
43
+ source "$(conda info --base)/etc/profile.d/conda.sh"
44
+ conda activate "${CONDA_ENV}"
45
+ fi
46
+
47
+ if [[ ! -f "${DATASET_JSON}" ]]; then
48
+ echo "Missing DATASET_JSON: ${DATASET_JSON}" >&2
49
+ echo "Create it first with CodonTransformer.CodonData.prepare_training_data." >&2
50
+ exit 1
51
+ fi
52
+
53
+ args=(
54
+ "${PROJECT_DIR}/scripts/finetune.py"
55
+ --dataset_dir "${DATASET_JSON}"
56
+ --checkpoint_dir "${CHECKPOINT_DIR}"
57
+ --checkpoint_filename "${CHECKPOINT_FILENAME}"
58
+ --batch_size "${BATCH_SIZE}"
59
+ --max_epochs "${MAX_EPOCHS}"
60
+ --num_workers "${NUM_WORKERS}"
61
+ --accumulate_grad_batches "${ACCUMULATE_GRAD_BATCHES}"
62
+ --num_gpus "${NUM_GPUS}"
63
+ --learning_rate "${LEARNING_RATE}"
64
+ --warmup_fraction "${WARMUP_FRACTION}"
65
+ --save_every_n_steps "${SAVE_EVERY_N_STEPS}"
66
+ --seed "${SEED}"
67
+ )
68
+
69
+ if [[ "${DEBUG}" == "1" ]]; then
70
+ args+=(--debug)
71
+ fi
72
+
73
+ python "${args[@]}"
scripts/slurm/run_inference_batch.sh ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Batch deterministic inference from a CSV file.
5
+ # Run this inside an allocated/interactive GPU session. No SLURM resources are requested here.
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
8
+ HF_HOME="${HF_HOME:-/public/home/scnb9biwet/.cache/huggingface}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ INPUT_CSV="${INPUT_CSV:-${PROJECT_DIR}/scripts/demo/sample_dataset.csv}"
12
+ OUTPUT_CSV="${OUTPUT_CSV:-${PROJECT_DIR}/outputs/sample_predictions.csv}"
13
+ OFFLINE="${OFFLINE:-1}"
14
+
15
+ cd "${PROJECT_DIR}"
16
+ mkdir -p "$(dirname "${OUTPUT_CSV}")"
17
+
18
+ export HF_HOME
19
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
20
+ export INPUT_CSV
21
+ export OUTPUT_CSV
22
+ export OFFLINE
23
+ export PYTHONFAULTHANDLER=1
24
+
25
+ if [[ "${OFFLINE}" == "1" ]]; then
26
+ export HF_HUB_OFFLINE=1
27
+ export TRANSFORMERS_OFFLINE=1
28
+ fi
29
+
30
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
31
+ # shellcheck disable=SC1091
32
+ source "$(conda info --base)/etc/profile.d/conda.sh"
33
+ conda activate "${CONDA_ENV}"
34
+ fi
35
+
36
+ python - <<'PY'
37
+ import os
38
+
39
+ import pandas as pd
40
+ import torch
41
+ from tqdm import tqdm
42
+ from transformers import AutoTokenizer, BigBirdForMaskedLM
43
+
44
+ from CodonTransformer.CodonPrediction import predict_dna_sequence
45
+
46
+ input_csv = os.environ["INPUT_CSV"]
47
+ output_csv = os.environ["OUTPUT_CSV"]
48
+ local_files_only = os.environ.get("OFFLINE", "1") == "1"
49
+
50
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
51
+ print(f"HF_HOME: {os.environ.get('HF_HOME')}")
52
+ print(f"Device: {device}")
53
+ print(f"Local files only: {local_files_only}")
54
+ print(f"Input CSV: {input_csv}")
55
+
56
+ tokenizer = AutoTokenizer.from_pretrained(
57
+ "adibvafa/CodonTransformer",
58
+ local_files_only=local_files_only,
59
+ )
60
+ model = BigBirdForMaskedLM.from_pretrained(
61
+ "adibvafa/CodonTransformer",
62
+ local_files_only=local_files_only,
63
+ ).to(device)
64
+
65
+ dataset = pd.read_csv(input_csv)
66
+ if "Unnamed: 0" in dataset.columns:
67
+ dataset = dataset.drop(columns=["Unnamed: 0"])
68
+
69
+ required_columns = {"protein_sequence", "organism"}
70
+ missing = required_columns - set(dataset.columns)
71
+ if missing:
72
+ raise ValueError(f"Input CSV is missing required columns: {sorted(missing)}")
73
+
74
+ dataset["predicted_dna"] = ""
75
+ for index, row in tqdm(dataset.iterrows(), total=len(dataset), desc="Predicting"):
76
+ output = predict_dna_sequence(
77
+ protein=row["protein_sequence"],
78
+ organism=row["organism"],
79
+ device=device,
80
+ tokenizer=tokenizer,
81
+ model=model,
82
+ attention_type="original_full",
83
+ deterministic=True,
84
+ )
85
+ dataset.loc[index, "predicted_dna"] = output.predicted_dna
86
+
87
+ dataset.to_csv(output_csv, index=False)
88
+ print(f"Saved predictions to {output_csv}")
89
+ PY
scripts/slurm/run_inference_finetuned.sh ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Single deterministic inference with an exported finetuned .pt model.
5
+ # Run this after scripts/slurm/export_finetuned_model.sh.
6
+ # This script does not request SLURM resources.
7
+
8
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ TOKENIZER_PATH="${TOKENIZER_PATH:-${PROJECT_DIR}/model/src/CodonTransformerTokenizer.json}"
12
+ MODEL_PATH="${MODEL_PATH:-${PROJECT_DIR}/weight/checkpoints/finetune/finetuned_model.pt}"
13
+ PROTEIN="${PROTEIN:-MFWY}"
14
+ ORGANISM="${ORGANISM:-Escherichia coli general}"
15
+ MATCH_PROTEIN="${MATCH_PROTEIN:-1}"
16
+
17
+ cd "${PROJECT_DIR}"
18
+
19
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
20
+ export TOKENIZER_PATH
21
+ export MODEL_PATH
22
+ export PROTEIN
23
+ export ORGANISM
24
+ export MATCH_PROTEIN
25
+ export PYTHONFAULTHANDLER=1
26
+
27
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
28
+ # shellcheck disable=SC1091
29
+ source "$(conda info --base)/etc/profile.d/conda.sh"
30
+ conda activate "${CONDA_ENV}"
31
+ fi
32
+
33
+ if [[ ! -f "${TOKENIZER_PATH}" ]]; then
34
+ echo "Missing TOKENIZER_PATH: ${TOKENIZER_PATH}" >&2
35
+ exit 1
36
+ fi
37
+
38
+ if [[ ! -f "${MODEL_PATH}" ]]; then
39
+ echo "Missing MODEL_PATH: ${MODEL_PATH}" >&2
40
+ exit 1
41
+ fi
42
+
43
+ python - <<'PY'
44
+ import os
45
+
46
+ import torch
47
+
48
+ from CodonTransformer.CodonJupyter import format_model_output
49
+ from CodonTransformer.CodonPrediction import predict_dna_sequence
50
+
51
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
52
+ match_protein = os.environ.get("MATCH_PROTEIN", "1") == "1"
53
+
54
+ print(f"Device: {device}")
55
+ print(f"Tokenizer: {os.environ['TOKENIZER_PATH']}")
56
+ print(f"Model: {os.environ['MODEL_PATH']}")
57
+
58
+ output = predict_dna_sequence(
59
+ protein=os.environ["PROTEIN"],
60
+ organism=os.environ["ORGANISM"],
61
+ device=device,
62
+ tokenizer=os.environ["TOKENIZER_PATH"],
63
+ model=os.environ["MODEL_PATH"],
64
+ attention_type="original_full",
65
+ deterministic=True,
66
+ match_protein=match_protein,
67
+ )
68
+
69
+ print(format_model_output(output))
70
+ PY
scripts/slurm/run_inference_multiple.sh ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Multiple variable inference for one protein sequence.
5
+ # Run this inside an allocated/interactive GPU session. No SLURM resources are requested here.
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
8
+ HF_HOME="${HF_HOME:-/public/home/scnb9biwet/.cache/huggingface}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ PROTEIN="${PROTEIN:-MFWY}"
12
+ ORGANISM="${ORGANISM:-Escherichia coli general}"
13
+ OUTPUT_CSV="${OUTPUT_CSV:-${PROJECT_DIR}/outputs/multiple_predictions.csv}"
14
+ NUM_SEQUENCES="${NUM_SEQUENCES:-5}"
15
+ TEMPERATURE="${TEMPERATURE:-0.5}"
16
+ TOP_P="${TOP_P:-0.95}"
17
+ MATCH_PROTEIN="${MATCH_PROTEIN:-1}"
18
+ OFFLINE="${OFFLINE:-1}"
19
+
20
+ cd "${PROJECT_DIR}"
21
+ mkdir -p "$(dirname "${OUTPUT_CSV}")"
22
+
23
+ export HF_HOME
24
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
25
+ export PROTEIN
26
+ export ORGANISM
27
+ export OUTPUT_CSV
28
+ export NUM_SEQUENCES
29
+ export TEMPERATURE
30
+ export TOP_P
31
+ export MATCH_PROTEIN
32
+ export OFFLINE
33
+ export PYTHONFAULTHANDLER=1
34
+
35
+ if [[ "${OFFLINE}" == "1" ]]; then
36
+ export HF_HUB_OFFLINE=1
37
+ export TRANSFORMERS_OFFLINE=1
38
+ fi
39
+
40
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
41
+ # shellcheck disable=SC1091
42
+ source "$(conda info --base)/etc/profile.d/conda.sh"
43
+ conda activate "${CONDA_ENV}"
44
+ fi
45
+
46
+ python - <<'PY'
47
+ import os
48
+
49
+ import pandas as pd
50
+ import torch
51
+ from transformers import AutoTokenizer, BigBirdForMaskedLM
52
+
53
+ from CodonTransformer.CodonJupyter import format_model_output
54
+ from CodonTransformer.CodonPrediction import predict_dna_sequence
55
+
56
+ protein = os.environ["PROTEIN"]
57
+ organism = os.environ["ORGANISM"]
58
+ output_csv = os.environ["OUTPUT_CSV"]
59
+ num_sequences = int(os.environ["NUM_SEQUENCES"])
60
+ temperature = float(os.environ["TEMPERATURE"])
61
+ top_p = float(os.environ["TOP_P"])
62
+ match_protein = os.environ.get("MATCH_PROTEIN", "1") == "1"
63
+ local_files_only = os.environ.get("OFFLINE", "1") == "1"
64
+
65
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
66
+ print(f"HF_HOME: {os.environ.get('HF_HOME')}")
67
+ print(f"Device: {device}")
68
+ print(f"Local files only: {local_files_only}")
69
+ print(
70
+ "Sampling: "
71
+ f"num_sequences={num_sequences}, temperature={temperature}, "
72
+ f"top_p={top_p}, match_protein={match_protein}"
73
+ )
74
+
75
+ tokenizer = AutoTokenizer.from_pretrained(
76
+ "adibvafa/CodonTransformer",
77
+ local_files_only=local_files_only,
78
+ )
79
+ model = BigBirdForMaskedLM.from_pretrained(
80
+ "adibvafa/CodonTransformer",
81
+ local_files_only=local_files_only,
82
+ ).to(device)
83
+
84
+ outputs = predict_dna_sequence(
85
+ protein=protein,
86
+ organism=organism,
87
+ device=device,
88
+ tokenizer=tokenizer,
89
+ model=model,
90
+ attention_type="original_full",
91
+ deterministic=False,
92
+ temperature=temperature,
93
+ top_p=top_p,
94
+ num_sequences=num_sequences,
95
+ match_protein=match_protein,
96
+ )
97
+
98
+ rows = []
99
+ for index, output in enumerate(outputs, start=1):
100
+ print(f"\nSequence {index}")
101
+ print(format_model_output(output))
102
+ rows.append(
103
+ {
104
+ "sequence_index": index,
105
+ "protein_sequence": output.protein,
106
+ "organism": output.organism,
107
+ "predicted_dna": output.predicted_dna,
108
+ }
109
+ )
110
+
111
+ pd.DataFrame(rows).to_csv(output_csv, index=False)
112
+ print(f"Saved predictions to {output_csv}")
113
+ PY
scripts/slurm/run_inference_single.sh ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Single deterministic inference for one protein sequence.
5
+ # Run this inside an allocated/interactive GPU session. No SLURM resources are requested here.
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
8
+ HF_HOME="${HF_HOME:-/public/home/scnb9biwet/.cache/huggingface}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ PROTEIN="${PROTEIN:-MFWY}"
12
+ ORGANISM="${ORGANISM:-Escherichia coli general}"
13
+ OFFLINE="${OFFLINE:-1}"
14
+
15
+ cd "${PROJECT_DIR}"
16
+
17
+ export HF_HOME
18
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
19
+ export PROTEIN
20
+ export ORGANISM
21
+ export OFFLINE
22
+ export PYTHONFAULTHANDLER=1
23
+
24
+ if [[ "${OFFLINE}" == "1" ]]; then
25
+ export HF_HUB_OFFLINE=1
26
+ export TRANSFORMERS_OFFLINE=1
27
+ fi
28
+
29
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
30
+ # shellcheck disable=SC1091
31
+ source "$(conda info --base)/etc/profile.d/conda.sh"
32
+ conda activate "${CONDA_ENV}"
33
+ fi
34
+
35
+ python - <<'PY'
36
+ import os
37
+
38
+ import torch
39
+ from transformers import AutoTokenizer, BigBirdForMaskedLM
40
+
41
+ from CodonTransformer.CodonJupyter import format_model_output
42
+ from CodonTransformer.CodonPrediction import predict_dna_sequence
43
+
44
+ protein = os.environ["PROTEIN"]
45
+ organism = os.environ["ORGANISM"]
46
+ local_files_only = os.environ.get("OFFLINE", "1") == "1"
47
+
48
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
49
+ print(f"HF_HOME: {os.environ.get('HF_HOME')}")
50
+ print(f"Device: {device}")
51
+ print(f"Local files only: {local_files_only}")
52
+
53
+ tokenizer = AutoTokenizer.from_pretrained(
54
+ "adibvafa/CodonTransformer",
55
+ local_files_only=local_files_only,
56
+ )
57
+ model = BigBirdForMaskedLM.from_pretrained(
58
+ "adibvafa/CodonTransformer",
59
+ local_files_only=local_files_only,
60
+ ).to(device)
61
+
62
+ output = predict_dna_sequence(
63
+ protein=protein,
64
+ organism=organism,
65
+ device=device,
66
+ tokenizer=tokenizer,
67
+ model=model,
68
+ attention_type="original_full",
69
+ deterministic=True,
70
+ )
71
+
72
+ print(format_model_output(output))
73
+ PY
scripts/slurm/run_pretrain.sh ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Run this script inside an allocated/interactive GPU session.
5
+ # It does not request SLURM resources.
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-/public/home/scnb9biwet/jiangqq/CodonTransformer-main}"
8
+ HF_HOME="${HF_HOME:-/public/home/scnb9biwet/.cache/huggingface}"
9
+ CONDA_ENV="${CONDA_ENV:-struct-evo}"
10
+
11
+ TOKENIZER_PATH="${TOKENIZER_PATH:-${PROJECT_DIR}/model/src/CodonTransformerTokenizer.json}"
12
+ TRAIN_DATA_PATH="${TRAIN_DATA_PATH:-${PROJECT_DIR}/scripts/data/processed/pretrain_data.json}"
13
+ CHECKPOINT_DIR="${CHECKPOINT_DIR:-${PROJECT_DIR}/weight/checkpoints/pretrain}"
14
+
15
+ BATCH_SIZE="${BATCH_SIZE:-6}"
16
+ MAX_EPOCHS="${MAX_EPOCHS:-5}"
17
+ NUM_WORKERS="${NUM_WORKERS:-5}"
18
+ ACCUMULATE_GRAD_BATCHES="${ACCUMULATE_GRAD_BATCHES:-1}"
19
+ NUM_GPUS="${NUM_GPUS:-16}"
20
+ LEARNING_RATE="${LEARNING_RATE:-0.00005}"
21
+ WARMUP_FRACTION="${WARMUP_FRACTION:-0.1}"
22
+ SAVE_INTERVAL="${SAVE_INTERVAL:-5}"
23
+ SEED="${SEED:-123}"
24
+ DEBUG="${DEBUG:-0}"
25
+ OFFLINE="${OFFLINE:-1}"
26
+
27
+ cd "${PROJECT_DIR}"
28
+ mkdir -p "${CHECKPOINT_DIR}"
29
+
30
+ export HF_HOME
31
+ export PYTHONPATH="${PROJECT_DIR}/model:${PYTHONPATH:-}"
32
+ export CUBLAS_WORKSPACE_CONFIG=:4096:2
33
+ export NCCL_DEBUG="${NCCL_DEBUG:-INFO}"
34
+ export PYTHONFAULTHANDLER=1
35
+
36
+ if [[ "${OFFLINE}" == "1" ]]; then
37
+ export HF_HUB_OFFLINE=1
38
+ export TRANSFORMERS_OFFLINE=1
39
+ fi
40
+
41
+ if [[ -n "${CONDA_ENV}" ]] && command -v conda >/dev/null 2>&1; then
42
+ # shellcheck disable=SC1091
43
+ source "$(conda info --base)/etc/profile.d/conda.sh"
44
+ conda activate "${CONDA_ENV}"
45
+ fi
46
+
47
+ if [[ ! -f "${TOKENIZER_PATH}" ]]; then
48
+ echo "Missing TOKENIZER_PATH: ${TOKENIZER_PATH}" >&2
49
+ exit 1
50
+ fi
51
+
52
+ if [[ ! -f "${TRAIN_DATA_PATH}" ]]; then
53
+ echo "Missing TRAIN_DATA_PATH: ${TRAIN_DATA_PATH}" >&2
54
+ echo "Create it first with CodonTransformer.CodonData.prepare_training_data." >&2
55
+ exit 1
56
+ fi
57
+
58
+ args=(
59
+ "${PROJECT_DIR}/scripts/pretrain.py"
60
+ --tokenizer_path "${TOKENIZER_PATH}"
61
+ --train_data_path "${TRAIN_DATA_PATH}"
62
+ --checkpoint_dir "${CHECKPOINT_DIR}"
63
+ --batch_size "${BATCH_SIZE}"
64
+ --max_epochs "${MAX_EPOCHS}"
65
+ --num_workers "${NUM_WORKERS}"
66
+ --accumulate_grad_batches "${ACCUMULATE_GRAD_BATCHES}"
67
+ --num_gpus "${NUM_GPUS}"
68
+ --learning_rate "${LEARNING_RATE}"
69
+ --warmup_fraction "${WARMUP_FRACTION}"
70
+ --save_interval "${SAVE_INTERVAL}"
71
+ --seed "${SEED}"
72
+ )
73
+
74
+ if [[ "${DEBUG}" == "1" ]]; then
75
+ args+=(--debug)
76
+ fi
77
+
78
+ python "${args[@]}"
scripts/tests/__init__.py ADDED
File without changes
scripts/tests/test_CodonData.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ import unittest
3
+
4
+ import pandas as pd
5
+ from Bio.Data.CodonTable import TranslationError
6
+
7
+ from CodonTransformer.CodonData import (
8
+ build_amino2codon_skeleton,
9
+ get_amino_acid_sequence,
10
+ is_correct_seq,
11
+ preprocess_protein_sequence,
12
+ read_fasta_file,
13
+ )
14
+ from CodonTransformer.CodonUtils import ProteinConfig
15
+
16
+
17
+ class TestCodonData(unittest.TestCase):
18
+ def test_preprocess_protein_sequence(self):
19
+ with ProteinConfig() as config:
20
+ config.set("ambiguous_aminoacid_behavior", "raise_error")
21
+ protein = "Z_"
22
+ try:
23
+ preprocess_protein_sequence(protein)
24
+ self.fail("Expected ValueError")
25
+ except ValueError:
26
+ pass
27
+ config.set("ambiguous_aminoacid_behavior", "standardize_deterministic")
28
+ for _ in range(10):
29
+ preprocessed_protein = preprocess_protein_sequence(protein)
30
+ self.assertEqual(preprocessed_protein, "Q_")
31
+ config.set("ambiguous_aminoacid_behavior", "standardize_random")
32
+ random_results = set()
33
+ # The probability of getting the same result 30 times in a row is
34
+ # 1 in 1.073741824*10^9 if there are only two possible results.
35
+ for _ in range(30):
36
+ preprocessed_protein = preprocess_protein_sequence(protein)
37
+ random_results.add(preprocessed_protein)
38
+ self.assertGreater(len(random_results), 1)
39
+
40
+ def test_read_fasta_file(self):
41
+ fasta_content = ">sequence1\n" "ATGATGATGATGATG\n" ">sequence2\n" "TGATGATGATGA"
42
+
43
+ with tempfile.NamedTemporaryFile(
44
+ mode="w", delete=False, suffix=".fasta"
45
+ ) as temp_file:
46
+ temp_file.write(fasta_content)
47
+ temp_file_name = temp_file.name
48
+
49
+ try:
50
+ sequences = read_fasta_file(temp_file_name, save_to_file=None)
51
+ self.assertIsInstance(sequences, pd.DataFrame)
52
+ self.assertEqual(len(sequences), 2)
53
+ self.assertEqual(sequences.iloc[0]["dna"], "ATGATGATGATGATG")
54
+ self.assertEqual(sequences.iloc[1]["dna"], "TGATGATGATGA")
55
+ finally:
56
+ import os
57
+
58
+ os.unlink(temp_file_name)
59
+
60
+ def test_build_amino2codon_skeleton(self):
61
+ organism = "Homo sapiens"
62
+ codon_skeleton = build_amino2codon_skeleton(organism)
63
+
64
+ expected_amino_acids = "ARNDCQEGHILKMFPSTWYV_"
65
+
66
+ for amino_acid in expected_amino_acids:
67
+ self.assertIn(amino_acid, codon_skeleton)
68
+ codons, frequencies = codon_skeleton[amino_acid]
69
+ self.assertIsInstance(codons, list)
70
+ self.assertIsInstance(frequencies, list)
71
+ self.assertEqual(len(codons), len(frequencies))
72
+ self.assertTrue(all(isinstance(codon, str) for codon in codons))
73
+ self.assertTrue(all(freq == 0 for freq in frequencies))
74
+
75
+ all_codons = set(
76
+ codon for codons, _ in codon_skeleton.values() for codon in codons
77
+ )
78
+ self.assertEqual(len(all_codons), 64) # There should be 64 unique codons
79
+
80
+ def test_get_amino_acid_sequence(self):
81
+ dna = "ATGGCCTGA"
82
+ protein, is_correct = get_amino_acid_sequence(dna, return_correct_seq=True)
83
+ self.assertEqual(protein, "MA_")
84
+ self.assertTrue(is_correct)
85
+
86
+ def test_is_correct_seq(self):
87
+ dna = "ATGGCCTGA"
88
+ protein = "MA_"
89
+ self.assertTrue(is_correct_seq(dna, protein))
90
+
91
+ def test_read_fasta_file_raises_exception_for_non_dna(self):
92
+ non_dna_content = ">sequence1\nATGATGATGXYZATG\n>sequence2\nTGATGATGATGA"
93
+
94
+ with tempfile.NamedTemporaryFile(
95
+ mode="w", delete=False, suffix=".fasta"
96
+ ) as temp_file:
97
+ temp_file.write(non_dna_content)
98
+ temp_file_name = temp_file.name
99
+
100
+ try:
101
+ with self.assertRaises(TranslationError) as context:
102
+ read_fasta_file(temp_file_name)
103
+ self.assertIn("Codon 'XYZ' is invalid", str(context.exception))
104
+ finally:
105
+ import os
106
+
107
+ os.unlink(temp_file_name)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()
scripts/tests/test_CodonJupyter.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+ import ipywidgets
4
+
5
+ from CodonTransformer.CodonJupyter import (
6
+ DNASequencePrediction,
7
+ UserContainer,
8
+ create_dropdown_options,
9
+ create_organism_dropdown,
10
+ display_organism_dropdown,
11
+ display_protein_input,
12
+ format_model_output,
13
+ )
14
+ from CodonTransformer.CodonUtils import ORGANISM2ID
15
+
16
+
17
+ class TestCodonJupyter(unittest.TestCase):
18
+ def test_UserContainer(self):
19
+ user_container = UserContainer()
20
+ self.assertEqual(user_container.organism, -1)
21
+ self.assertEqual(user_container.protein, "")
22
+
23
+ def test_create_organism_dropdown(self):
24
+ container = UserContainer()
25
+ dropdown = create_organism_dropdown(container)
26
+
27
+ self.assertIsInstance(dropdown, ipywidgets.Dropdown)
28
+ self.assertGreater(len(dropdown.options), 0)
29
+ self.assertEqual(dropdown.description, "")
30
+ self.assertEqual(dropdown.layout.width, "40%")
31
+ self.assertEqual(dropdown.layout.margin, "0 0 10px 0")
32
+ self.assertEqual(dropdown.style.description_width, "initial")
33
+
34
+ # Test the dropdown options
35
+ options = dropdown.options
36
+ self.assertIn("", options)
37
+ self.assertIn("Selected Organisms", options)
38
+ self.assertIn("All Organisms", options)
39
+
40
+ def test_create_dropdown_options(self):
41
+ options = create_dropdown_options(ORGANISM2ID)
42
+ self.assertIsInstance(options, list)
43
+ self.assertGreater(len(options), 0)
44
+
45
+ def test_display_organism_dropdown(self):
46
+ container = UserContainer()
47
+ with unittest.mock.patch(
48
+ "CodonTransformer.CodonJupyter.display"
49
+ ) as mock_display:
50
+ display_organism_dropdown(container)
51
+
52
+ # Check that display was called twice (for container_widget and HTML)
53
+ self.assertEqual(mock_display.call_count, 2)
54
+
55
+ # Check that the first call to display was with a VBox widget
56
+ self.assertIsInstance(mock_display.call_args_list[0][0][0], ipywidgets.VBox)
57
+
58
+ # Check that the VBox contains a Dropdown
59
+ dropdown = mock_display.call_args_list[0][0][0].children[1]
60
+ self.assertIsInstance(dropdown, ipywidgets.Dropdown)
61
+ self.assertGreater(len(dropdown.options), 0)
62
+
63
+ def test_display_protein_input(self):
64
+ container = UserContainer()
65
+ with unittest.mock.patch(
66
+ "CodonTransformer.CodonJupyter.display"
67
+ ) as mock_display:
68
+ display_protein_input(container)
69
+
70
+ # Check that display was called twice (for container_widget and HTML)
71
+ self.assertEqual(mock_display.call_count, 2)
72
+
73
+ # Check that the first call to display was with a VBox widget
74
+ self.assertIsInstance(mock_display.call_args_list[0][0][0], ipywidgets.VBox)
75
+
76
+ # Check that the VBox contains a Textarea
77
+ textarea = mock_display.call_args_list[0][0][0].children[1]
78
+ self.assertIsInstance(textarea, ipywidgets.Textarea)
79
+
80
+ # Verify the properties of the Textarea
81
+ self.assertEqual(textarea.value, "")
82
+ self.assertEqual(textarea.placeholder, "Enter here...")
83
+ self.assertEqual(textarea.description, "")
84
+ self.assertEqual(textarea.layout.width, "100%")
85
+ self.assertEqual(textarea.layout.height, "100px")
86
+ self.assertEqual(textarea.layout.margin, "0 0 10px 0")
87
+ self.assertEqual(textarea.style.description_width, "initial")
88
+
89
+ def test_format_model_output(self):
90
+ output = DNASequencePrediction(
91
+ organism="Escherichia coli",
92
+ protein="MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG",
93
+ processed_input="MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG",
94
+ predicted_dna="ATGAAAACTGTTCGTCAGGAACGTCTGAAATCTATTGTTCGTATTCTGGAACGTTCTAAAGAACCGGTTTCTGGTGCTCAACTGGCTGAAGAACTGTCTGTTTCTCGTCAGGTTATTGTTCAGGACATTGCTTACCTGCGTTCTCTGGGTTATAA",
95
+ )
96
+ formatted_output = format_model_output(output)
97
+ self.assertIsInstance(formatted_output, str)
98
+ self.assertIn("Organism", formatted_output)
99
+ self.assertIn("Escherichia coli", formatted_output)
100
+ self.assertIn("Input Protein", formatted_output)
101
+ self.assertIn(
102
+ "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG",
103
+ formatted_output,
104
+ )
105
+ self.assertIn("Processed Input", formatted_output)
106
+ self.assertIn(
107
+ "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG",
108
+ formatted_output,
109
+ )
110
+ self.assertIn("Predicted DNA", formatted_output)
111
+ self.assertIn(
112
+ "ATGAAAACTGTTCGTCAGGAACGTCTGAAATCTATTGTTCGTATTCTGGAACGTTCTAAAGAACCGGTTTCTGGTGCTCAACTGGCTGAAGAACTGTCTGTTTCTCGTCAGGTTATTGTTCAGGACATTGCTTACCTGCGTTCTCTGGGTTATAA",
113
+ formatted_output,
114
+ )
115
+
116
+
117
+ if __name__ == "__main__":
118
+ unittest.main()
scripts/tests/test_CodonPrediction.py ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import unittest
3
+ import warnings
4
+
5
+ import torch
6
+
7
+ from CodonTransformer.CodonData import get_amino_acid_sequence
8
+ from CodonTransformer.CodonPrediction import (
9
+ load_model,
10
+ load_tokenizer,
11
+ predict_dna_sequence,
12
+ )
13
+ from CodonTransformer.CodonUtils import (
14
+ AMINO_ACIDS,
15
+ ORGANISM2ID,
16
+ STOP_SYMBOLS,
17
+ DNASequencePrediction,
18
+ )
19
+
20
+
21
+ class TestCodonPrediction(unittest.TestCase):
22
+ @classmethod
23
+ def setUpClass(cls):
24
+ cls.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
25
+
26
+ # Suppress warnings about loading from HuggingFace
27
+ for message in [
28
+ "Tokenizer path not provided. Loading from HuggingFace.",
29
+ "Model path not provided. Loading from HuggingFace.",
30
+ ]:
31
+ warnings.filterwarnings("ignore", message=message)
32
+
33
+ cls.model = load_model(device=cls.device)
34
+ cls.tokenizer = load_tokenizer()
35
+
36
+ def test_predict_dna_sequence_valid_input(self):
37
+ protein_sequence = "MWWMW"
38
+ organism = "Escherichia coli general"
39
+ result = predict_dna_sequence(
40
+ protein_sequence,
41
+ organism,
42
+ device=self.device,
43
+ tokenizer=self.tokenizer,
44
+ model=self.model,
45
+ )
46
+ self.assertIsInstance(result.predicted_dna, str)
47
+ self.assertTrue(
48
+ all(nucleotide in "ATCG" for nucleotide in result.predicted_dna)
49
+ )
50
+ self.assertEqual(result.predicted_dna, "ATGTGGTGGATGTGGTGA")
51
+
52
+ def test_predict_dna_sequence_non_deterministic(self):
53
+ protein_sequence = "MFWY"
54
+ organism = "Escherichia coli general"
55
+ num_iterations = 100
56
+ temperatures = [0.2, 0.5, 0.8]
57
+ possible_outputs = set()
58
+ possible_encodings_wo_stop = {
59
+ "ATGTTTTGGTAT",
60
+ "ATGTTCTGGTAT",
61
+ "ATGTTTTGGTAC",
62
+ "ATGTTCTGGTAC",
63
+ }
64
+ for _ in range(num_iterations):
65
+ for temperature in temperatures:
66
+ result = predict_dna_sequence(
67
+ protein=protein_sequence,
68
+ organism=organism,
69
+ device=self.device,
70
+ tokenizer=self.tokenizer,
71
+ model=self.model,
72
+ deterministic=False,
73
+ temperature=temperature,
74
+ )
75
+ possible_outputs.add(result.predicted_dna[:-3]) # Remove stop codon
76
+
77
+ self.assertEqual(possible_outputs, possible_encodings_wo_stop)
78
+
79
+ def test_predict_dna_sequence_invalid_inputs(self):
80
+ test_cases = [
81
+ ("MKTZZFVLLL?", "Escherichia coli general", "invalid protein sequence"),
82
+ ("MKTFFVLLL", "Alien $%#@!", "invalid organism code"),
83
+ ("", "Escherichia coli general", "empty protein sequence"),
84
+ ]
85
+
86
+ for protein_sequence, organism, error_type in test_cases:
87
+ with self.subTest(error_type=error_type):
88
+ with self.assertRaises(ValueError):
89
+ predict_dna_sequence(
90
+ protein_sequence,
91
+ organism,
92
+ device=self.device,
93
+ tokenizer=self.tokenizer,
94
+ model=self.model,
95
+ )
96
+
97
+ def test_predict_dna_sequence_top_p_effect(self):
98
+ """Test that changing top_p affects the diversity of outputs."""
99
+ protein_sequence = "MFWY"
100
+ organism = "Escherichia coli general"
101
+ num_iterations = 50
102
+ temperature = 0.5
103
+ top_p_values = [0.8, 0.95]
104
+ outputs_by_top_p = {top_p: set() for top_p in top_p_values}
105
+
106
+ for top_p in top_p_values:
107
+ for _ in range(num_iterations):
108
+ result = predict_dna_sequence(
109
+ protein=protein_sequence,
110
+ organism=organism,
111
+ device=self.device,
112
+ tokenizer=self.tokenizer,
113
+ model=self.model,
114
+ deterministic=False,
115
+ temperature=temperature,
116
+ top_p=top_p,
117
+ )
118
+ outputs_by_top_p[top_p].add(
119
+ result.predicted_dna[:-3]
120
+ ) # Remove stop codon
121
+
122
+ # Assert that higher top_p results in more diverse outputs
123
+ diversity_lower_top_p = len(outputs_by_top_p[0.8])
124
+ diversity_higher_top_p = len(outputs_by_top_p[0.95])
125
+ self.assertGreaterEqual(
126
+ diversity_higher_top_p,
127
+ diversity_lower_top_p,
128
+ "Higher top_p should result in more diverse outputs",
129
+ )
130
+
131
+ def test_predict_dna_sequence_invalid_temperature_and_top_p(self):
132
+ """Test that invalid temperature and top_p values raise ValueError."""
133
+ protein_sequence = "MWWMW"
134
+ organism = "Escherichia coli general"
135
+ invalid_params = [
136
+ {"temperature": -0.1, "top_p": 0.95},
137
+ {"temperature": 0, "top_p": 0.95},
138
+ {"temperature": 0.5, "top_p": -0.1},
139
+ {"temperature": 0.5, "top_p": 1.1},
140
+ ]
141
+
142
+ for params in invalid_params:
143
+ with self.subTest(params=params):
144
+ with self.assertRaises(ValueError):
145
+ predict_dna_sequence(
146
+ protein=protein_sequence,
147
+ organism=organism,
148
+ device=self.device,
149
+ tokenizer=self.tokenizer,
150
+ model=self.model,
151
+ deterministic=False,
152
+ temperature=params["temperature"],
153
+ top_p=params["top_p"],
154
+ )
155
+
156
+ def test_predict_dna_sequence_translation_consistency(self):
157
+ """Test that the predicted DNA translates back to the original protein."""
158
+ protein_sequence = "MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVE"
159
+ organism = "Escherichia coli general"
160
+ result = predict_dna_sequence(
161
+ protein=protein_sequence,
162
+ organism=organism,
163
+ device=self.device,
164
+ tokenizer=self.tokenizer,
165
+ model=self.model,
166
+ deterministic=True,
167
+ )
168
+
169
+ # Translate predicted DNA back to protein
170
+ translated_protein = get_amino_acid_sequence(result.predicted_dna[:-3])
171
+
172
+ self.assertEqual(
173
+ translated_protein,
174
+ protein_sequence,
175
+ "Translated protein does not match the original protein sequence",
176
+ )
177
+
178
+ def test_predict_dna_sequence_long_protein_sequence(self):
179
+ """Test the function with a very long protein sequence to check performance and correctness."""
180
+ protein_sequence = (
181
+ "M"
182
+ + "MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAEDLQVGQVELGG"
183
+ * 20
184
+ + STOP_SYMBOLS[0]
185
+ )
186
+ organism = "Escherichia coli general"
187
+ result = predict_dna_sequence(
188
+ protein=protein_sequence,
189
+ organism=organism,
190
+ device=self.device,
191
+ tokenizer=self.tokenizer,
192
+ model=self.model,
193
+ deterministic=True,
194
+ )
195
+
196
+ # Check that the predicted DNA translates back to the original protein
197
+ dna_sequence = result.predicted_dna[:-3]
198
+ translated_protein = get_amino_acid_sequence(dna_sequence)
199
+ self.assertEqual(
200
+ translated_protein,
201
+ protein_sequence[:-1],
202
+ "Translated protein does not match the original long protein sequence",
203
+ )
204
+
205
+ def test_predict_dna_sequence_edge_case_organisms(self):
206
+ """Test the function with organism IDs at the boundaries of the mapping."""
207
+ protein_sequence = "MWWMW"
208
+ # Assuming ORGANISM2ID has IDs starting from 0 to N
209
+ min_organism_id = min(ORGANISM2ID.values())
210
+ max_organism_id = max(ORGANISM2ID.values())
211
+ organisms = [min_organism_id, max_organism_id]
212
+
213
+ for organism_id in organisms:
214
+ with self.subTest(organism_id=organism_id):
215
+ result = predict_dna_sequence(
216
+ protein=protein_sequence,
217
+ organism=organism_id,
218
+ device=self.device,
219
+ tokenizer=self.tokenizer,
220
+ model=self.model,
221
+ deterministic=True,
222
+ )
223
+ self.assertIsInstance(result.predicted_dna, str)
224
+ self.assertTrue(
225
+ all(nucleotide in "ATCG" for nucleotide in result.predicted_dna)
226
+ )
227
+
228
+ def test_predict_dna_sequence_concurrent_calls(self):
229
+ """Test the function's behavior under concurrent execution."""
230
+ import threading
231
+
232
+ protein_sequence = "MWWMW"
233
+ organism = "Escherichia coli general"
234
+ results = []
235
+
236
+ def call_predict():
237
+ result = predict_dna_sequence(
238
+ protein=protein_sequence,
239
+ organism=organism,
240
+ device=self.device,
241
+ tokenizer=self.tokenizer,
242
+ model=self.model,
243
+ deterministic=True,
244
+ )
245
+ results.append(result.predicted_dna)
246
+
247
+ threads = [threading.Thread(target=call_predict) for _ in range(10)]
248
+ for thread in threads:
249
+ thread.start()
250
+ for thread in threads:
251
+ thread.join()
252
+
253
+ self.assertEqual(len(results), 10)
254
+ self.assertTrue(all(dna == results[0] for dna in results))
255
+
256
+ def test_predict_dna_sequence_random_seed_consistency(self):
257
+ """Test that setting a random seed results in consistent outputs in non-deterministic mode."""
258
+ protein_sequence = "MFWY"
259
+ organism = "Escherichia coli general"
260
+ temperature = 0.5
261
+ top_p = 0.95
262
+ torch.manual_seed(42)
263
+
264
+ result1 = predict_dna_sequence(
265
+ protein=protein_sequence,
266
+ organism=organism,
267
+ device=self.device,
268
+ tokenizer=self.tokenizer,
269
+ model=self.model,
270
+ deterministic=False,
271
+ temperature=temperature,
272
+ top_p=top_p,
273
+ )
274
+
275
+ torch.manual_seed(42)
276
+
277
+ result2 = predict_dna_sequence(
278
+ protein=protein_sequence,
279
+ organism=organism,
280
+ device=self.device,
281
+ tokenizer=self.tokenizer,
282
+ model=self.model,
283
+ deterministic=False,
284
+ temperature=temperature,
285
+ top_p=top_p,
286
+ )
287
+
288
+ self.assertEqual(
289
+ result1.predicted_dna,
290
+ result2.predicted_dna,
291
+ "Outputs should be consistent when random seed is set",
292
+ )
293
+
294
+ def test_predict_dna_sequence_invalid_tokenizer_and_model(self):
295
+ """Test that providing invalid tokenizer or model raises appropriate exceptions."""
296
+ protein_sequence = "MWWMW"
297
+ organism = "Escherichia coli general"
298
+
299
+ with self.subTest("Invalid tokenizer"):
300
+ with self.assertRaises(Exception):
301
+ predict_dna_sequence(
302
+ protein=protein_sequence,
303
+ organism=organism,
304
+ device=self.device,
305
+ tokenizer="invalid_tokenizer_path",
306
+ model=self.model,
307
+ )
308
+
309
+ with self.subTest("Invalid model"):
310
+ with self.assertRaises(Exception):
311
+ predict_dna_sequence(
312
+ protein=protein_sequence,
313
+ organism=organism,
314
+ device=self.device,
315
+ tokenizer=self.tokenizer,
316
+ model="invalid_model_path",
317
+ )
318
+
319
+ def test_predict_dna_sequence_stop_codon_handling(self):
320
+ """Test the function's handling of protein sequences ending with a non '_' or '*' stop symbol."""
321
+ protein_sequence = "MWW/"
322
+ organism = "Escherichia coli general"
323
+
324
+ with self.assertRaises(ValueError):
325
+ predict_dna_sequence(
326
+ protein=protein_sequence,
327
+ organism=organism,
328
+ device=self.device,
329
+ tokenizer=self.tokenizer,
330
+ model=self.model,
331
+ )
332
+
333
+ def test_predict_dna_sequence_device_compatibility(self):
334
+ """Test that the function works correctly on both CPU and GPU devices."""
335
+ protein_sequence = "MWWMW"
336
+ organism = "Escherichia coli general"
337
+
338
+ devices = [torch.device("cpu")]
339
+ if torch.cuda.is_available():
340
+ devices.append(torch.device("cuda"))
341
+
342
+ for device in devices:
343
+ with self.subTest(device=device):
344
+ result = predict_dna_sequence(
345
+ protein=protein_sequence,
346
+ organism=organism,
347
+ device=device,
348
+ tokenizer=self.tokenizer,
349
+ model=self.model,
350
+ deterministic=True,
351
+ )
352
+ self.assertIsInstance(result.predicted_dna, str)
353
+ self.assertTrue(
354
+ all(nucleotide in "ATCG" for nucleotide in result.predicted_dna)
355
+ )
356
+
357
+ def test_predict_dna_sequence_random_proteins(self):
358
+ """Test random proteins to ensure translated DNA matches the original protein."""
359
+ organism = "Escherichia coli general"
360
+ num_tests = 200
361
+
362
+ for _ in range(num_tests):
363
+ # Generate a random protein sequence of random length between 10 and 50
364
+ protein_length = random.randint(10, 500)
365
+ protein_sequence = "M" + "".join(
366
+ random.choices(AMINO_ACIDS, k=protein_length - 1)
367
+ )
368
+ protein_sequence += random.choice(STOP_SYMBOLS)
369
+
370
+ result = predict_dna_sequence(
371
+ protein=protein_sequence,
372
+ organism=organism,
373
+ device=self.device,
374
+ tokenizer=self.tokenizer,
375
+ model=self.model,
376
+ deterministic=True,
377
+ )
378
+
379
+ # Remove stop codon from predicted DNA
380
+ dna_sequence = result.predicted_dna[:-3]
381
+
382
+ # Translate predicted DNA back to protein
383
+ translated_protein = get_amino_acid_sequence(dna_sequence)
384
+ self.assertEqual(
385
+ translated_protein,
386
+ protein_sequence[:-1], # Remove stop symbol
387
+ f"Translated protein does not match the original protein sequence for protein: {protein_sequence}",
388
+ )
389
+
390
+ def test_predict_dna_sequence_long_protein_over_max_length(self):
391
+ """Test that the model handles protein sequences longer than 2048 amino acids."""
392
+ # Create a protein sequence longer than 2048 amino acids
393
+ base_sequence = (
394
+ "MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAEDLQVGQVELGG"
395
+ )
396
+ protein_sequence = base_sequence * 100 # Length > 2048 amino acids
397
+ organism = "Escherichia coli general"
398
+
399
+ result = predict_dna_sequence(
400
+ protein=protein_sequence,
401
+ organism=organism,
402
+ device=self.device,
403
+ tokenizer=self.tokenizer,
404
+ model=self.model,
405
+ deterministic=True,
406
+ )
407
+
408
+ # Remove stop codon from predicted DNA
409
+ dna_sequence = result.predicted_dna[:-3]
410
+ translated_protein = get_amino_acid_sequence(dna_sequence)
411
+
412
+ # Due to potential model limitations, compare up to the model's max supported length
413
+ max_length = len(translated_protein)
414
+ self.assertEqual(
415
+ translated_protein[:max_length],
416
+ protein_sequence[:max_length],
417
+ "Translated protein does not match the original protein sequence up to the maximum length supported.",
418
+ )
419
+
420
+ def test_predict_dna_sequence_multi_output(self):
421
+ """Test that the function returns multiple sequences when num_sequences > 1."""
422
+ protein_sequence = "MFQLLAPWY"
423
+ organism = "Escherichia coli general"
424
+ num_sequences = 20
425
+
426
+ result = predict_dna_sequence(
427
+ protein=protein_sequence,
428
+ organism=organism,
429
+ device=self.device,
430
+ tokenizer=self.tokenizer,
431
+ model=self.model,
432
+ deterministic=False,
433
+ num_sequences=num_sequences,
434
+ )
435
+
436
+ self.assertIsInstance(result, list)
437
+ self.assertEqual(len(result), num_sequences)
438
+
439
+ for prediction in result:
440
+ self.assertIsInstance(prediction, DNASequencePrediction)
441
+ self.assertTrue(
442
+ all(nucleotide in "ATCG" for nucleotide in prediction.predicted_dna)
443
+ )
444
+
445
+ # Check that all predicted DNA sequences translate back to the original protein
446
+ translated_protein = get_amino_acid_sequence(prediction.predicted_dna[:-3])
447
+ self.assertEqual(translated_protein, protein_sequence)
448
+
449
+ def test_predict_dna_sequence_deterministic_multi_raises_error(self):
450
+ """Test that requesting multiple sequences in deterministic mode raises an error."""
451
+ protein_sequence = "MFWY"
452
+ organism = "Escherichia coli general"
453
+
454
+ with self.assertRaises(ValueError):
455
+ predict_dna_sequence(
456
+ protein=protein_sequence,
457
+ organism=organism,
458
+ device=self.device,
459
+ tokenizer=self.tokenizer,
460
+ model=self.model,
461
+ deterministic=True,
462
+ num_sequences=3,
463
+ )
464
+
465
+ def test_predict_dna_sequence_multi_diversity(self):
466
+ """Test that multiple sequences generated are diverse."""
467
+ protein_sequence = "MFWYMFWY"
468
+ organism = "Escherichia coli general"
469
+ num_sequences = 10
470
+
471
+ result = predict_dna_sequence(
472
+ protein=protein_sequence,
473
+ organism=organism,
474
+ device=self.device,
475
+ tokenizer=self.tokenizer,
476
+ model=self.model,
477
+ deterministic=False,
478
+ num_sequences=num_sequences,
479
+ temperature=0.8,
480
+ )
481
+
482
+ unique_sequences = set(prediction.predicted_dna for prediction in result)
483
+
484
+ self.assertGreater(
485
+ len(unique_sequences),
486
+ 2,
487
+ "Multiple sequence generation should produce diverse results",
488
+ )
489
+
490
+ # Check that all sequences are valid translations of the input protein
491
+ for prediction in result:
492
+ translated_protein = get_amino_acid_sequence(prediction.predicted_dna[:-3])
493
+ self.assertEqual(translated_protein, protein_sequence)
494
+
495
+ def test_predict_dna_sequence_match_protein_repetitive(self):
496
+ """Test that match_protein=True correctly handles highly repetitive and unconventional sequences."""
497
+ test_sequences = (
498
+ "QQQQQQQQQQQQQQQQ_",
499
+ "KRKRKRKRKRKRKRKR_",
500
+ "PGPGPGPGPGPGPGPG_",
501
+ "DEDEDEDEDEDEDEDEDE_",
502
+ "M_M_M_M_M_",
503
+ "MMMMMMMMMM_",
504
+ "WWWWWWWWWW_",
505
+ "CCCCCCCCCC_",
506
+ "MWCHMWCHMWCH_",
507
+ "Q_QQ_QQQ_QQQQ_",
508
+ "MWMWMWMWMWMW_",
509
+ "CCCHHHMMMWWW_",
510
+ "_",
511
+ "M_",
512
+ "MGWC_",
513
+ )
514
+
515
+ organism = "Homo sapiens"
516
+
517
+ for protein_sequence in test_sequences:
518
+ # Generate sequence with match_protein=True
519
+ result = predict_dna_sequence(
520
+ protein=protein_sequence,
521
+ organism=organism,
522
+ device=self.device,
523
+ tokenizer=self.tokenizer,
524
+ model=self.model,
525
+ deterministic=False,
526
+ temperature=20, # High temperature to test protein matching
527
+ match_protein=True,
528
+ )
529
+
530
+ dna_sequence = result.predicted_dna
531
+ translated_protein = get_amino_acid_sequence(dna_sequence)
532
+
533
+ self.assertEqual(
534
+ translated_protein,
535
+ protein_sequence,
536
+ f"Translated protein must match original when match_protein=True. Failed for sequence: {protein_sequence}",
537
+ )
538
+
539
+ def test_predict_dna_sequence_match_protein_rare_amino_acids(self):
540
+ """Test match_protein with rare amino acids that have limited codon options."""
541
+ # Methionine (M) and Tryptophan (W) have only one codon each
542
+ # While Leucine (L) has 6 codons - testing contrast
543
+ protein_sequence = "MWLLLMWLLL"
544
+ organism = "Escherichia coli general"
545
+
546
+ # Run multiple predictions
547
+ results = []
548
+ num_iterations = 10
549
+
550
+ for _ in range(num_iterations):
551
+ result = predict_dna_sequence(
552
+ protein=protein_sequence,
553
+ organism=organism,
554
+ device=self.device,
555
+ tokenizer=self.tokenizer,
556
+ model=self.model,
557
+ deterministic=False,
558
+ temperature=20, # High temperature to test protein matching
559
+ match_protein=True,
560
+ )
561
+ results.append(result.predicted_dna)
562
+
563
+ # Check all sequences
564
+ for dna_sequence in results:
565
+ # Verify M always uses ATG
566
+ m_positions = [0, 5] # Known positions of M in sequence
567
+ for pos in m_positions:
568
+ self.assertEqual(
569
+ dna_sequence[pos * 3 : (pos + 1) * 3],
570
+ "ATG",
571
+ "Methionine must use ATG codon.",
572
+ )
573
+
574
+ # Verify W always uses TGG
575
+ w_positions = [1, 6] # Known positions of W in sequence
576
+ for pos in w_positions:
577
+ self.assertEqual(
578
+ dna_sequence[pos * 3 : (pos + 1) * 3],
579
+ "TGG",
580
+ "Tryptophan must use TGG codon.",
581
+ )
582
+
583
+ # Verify all L codons are valid
584
+ l_positions = [2, 3, 4, 7, 8, 9] # Known positions of L in sequence
585
+ l_codons = [dna_sequence[pos * 3 : (pos + 1) * 3] for pos in l_positions]
586
+ valid_l_codons = {"TTA", "TTG", "CTT", "CTC", "CTA", "CTG"}
587
+ self.assertTrue(
588
+ all(codon in valid_l_codons for codon in l_codons),
589
+ "All Leucine codons must be valid",
590
+ )
591
+
592
+
593
+ if __name__ == "__main__":
594
+ unittest.main()
scripts/tests/test_CodonUtils.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import tempfile
4
+ import unittest
5
+
6
+ from CodonTransformer.CodonUtils import (
7
+ ProteinConfig,
8
+ find_pattern_in_fasta,
9
+ get_organism2id_dict,
10
+ get_taxonomy_id,
11
+ load_pkl_from_url,
12
+ load_python_object_from_disk,
13
+ save_python_object_to_disk,
14
+ sort_amino2codon_skeleton,
15
+ )
16
+
17
+
18
+ class TestCodonUtils(unittest.TestCase):
19
+ def test_config_manager(self):
20
+ with ProteinConfig() as config:
21
+ config.set("ambiguous_aminoacid_behavior", "standardize_deterministic")
22
+ self.assertEqual(
23
+ config.get("ambiguous_aminoacid_behavior"), "standardize_deterministic"
24
+ )
25
+ config.set("ambiguous_aminoacid_map_override", {"X": ["A", "G"]})
26
+ self.assertEqual(
27
+ config.get("ambiguous_aminoacid_map_override"), {"X": ["A", "G"]}
28
+ )
29
+ config.update(
30
+ {
31
+ "ambiguous_aminoacid_behavior": "raise_error",
32
+ "ambiguous_aminoacid_map_override": {"X": ["A", "G"]},
33
+ }
34
+ )
35
+ self.assertEqual(config.get("ambiguous_aminoacid_behavior"), "raise_error")
36
+ self.assertEqual(
37
+ config.get("ambiguous_aminoacid_map_override"), {"X": ["A", "G"]}
38
+ )
39
+ try:
40
+ config.set("invalid_key", "invalid_value")
41
+ self.fail("Expected ValueError")
42
+ except ValueError:
43
+ pass
44
+ with ProteinConfig() as config:
45
+ self.assertEqual(
46
+ config.get("ambiguous_aminoacid_behavior"), "standardize_random"
47
+ )
48
+ self.assertEqual(config.get("ambiguous_aminoacid_map_override"), {})
49
+
50
+ def test_load_python_object_from_disk(self):
51
+ test_obj = {"key1": "value1", "key2": 2}
52
+ with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as temp_file:
53
+ temp_file_name = temp_file.name
54
+ save_python_object_to_disk(test_obj, temp_file_name)
55
+ loaded_obj = load_python_object_from_disk(temp_file_name)
56
+ self.assertEqual(test_obj, loaded_obj)
57
+ os.remove(temp_file_name)
58
+
59
+ def test_save_python_object_to_disk(self):
60
+ test_obj = [1, 2, 3, 4, 5]
61
+ with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as temp_file:
62
+ temp_file_name = temp_file.name
63
+ save_python_object_to_disk(test_obj, temp_file_name)
64
+ self.assertTrue(os.path.exists(temp_file_name))
65
+ os.remove(temp_file_name)
66
+
67
+ def test_find_pattern_in_fasta(self):
68
+ text = (
69
+ ">seq1 [keyword=value1]\nATGCGTACGTAGCTAG\n"
70
+ ">seq2 [keyword=value2]\nGGTACGATCGATCGAT"
71
+ )
72
+ self.assertEqual(find_pattern_in_fasta("keyword", text), "value1")
73
+ self.assertEqual(find_pattern_in_fasta("nonexistent", text), "")
74
+
75
+ def test_get_organism2id_dict(self):
76
+ with tempfile.NamedTemporaryFile(
77
+ mode="w", delete=True, suffix=".csv"
78
+ ) as temp_file:
79
+ temp_file.write("0,Escherichia coli\n1,Homo sapiens\n2,Mus musculus")
80
+ temp_file.flush()
81
+ organism2id = get_organism2id_dict(temp_file.name)
82
+ self.assertEqual(
83
+ organism2id,
84
+ {"Escherichia coli": 0, "Homo sapiens": 1, "Mus musculus": 2},
85
+ )
86
+
87
+ def test_get_taxonomy_id(self):
88
+ taxonomy_dict = {
89
+ "Escherichia coli": 562,
90
+ "Homo sapiens": 9606,
91
+ "Mus musculus": 10090,
92
+ }
93
+ with tempfile.NamedTemporaryFile(suffix=".pkl", delete=True) as temp_file:
94
+ temp_file_name = temp_file.name
95
+ save_python_object_to_disk(taxonomy_dict, temp_file_name)
96
+ self.assertEqual(get_taxonomy_id(temp_file_name, "Escherichia coli"), 562)
97
+ self.assertEqual(
98
+ get_taxonomy_id(temp_file_name, return_dict=True), taxonomy_dict
99
+ )
100
+
101
+ def test_sort_amino2codon_skeleton(self):
102
+ amino2codon = {
103
+ "A": (["GCT", "GCC", "GCA", "GCG"], [0.0, 0.0, 0.0, 0.0]),
104
+ "C": (["TGT", "TGC"], [0.0, 0.0]),
105
+ }
106
+ sorted_amino2codon = sort_amino2codon_skeleton(amino2codon)
107
+ self.assertEqual(
108
+ sorted_amino2codon,
109
+ {
110
+ "A": (["GCA", "GCC", "GCG", "GCT"], [0.0, 0.0, 0.0, 0.0]),
111
+ "C": (["TGC", "TGT"], [0.0, 0.0]),
112
+ },
113
+ )
114
+
115
+ def test_load_pkl_from_url(self):
116
+ url = "https://example.com/test.pkl"
117
+ expected_obj = {"key": "value"}
118
+ with unittest.mock.patch("requests.get") as mock_get:
119
+ mock_get.return_value.content = pickle.dumps(expected_obj)
120
+ loaded_obj = load_pkl_from_url(url)
121
+ self.assertEqual(loaded_obj, expected_obj)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ unittest.main()
setup.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setup.py
2
+ import os
3
+
4
+ from setuptools import find_packages, setup
5
+
6
+
7
+ def read_requirements():
8
+ with open("requirements.txt") as f:
9
+ return [line.strip() for line in f if line.strip() and not line.startswith("#")]
10
+
11
+
12
+ def read_readme():
13
+ here = os.path.abspath(os.path.dirname(__file__))
14
+ readme_path = os.path.join(here, "README.md")
15
+
16
+ with open(readme_path, "r", encoding="utf-8") as f:
17
+ return f.read()
18
+
19
+
20
+ setup(
21
+ name="CodonTransformer",
22
+ version="1.6.7",
23
+ packages=find_packages(where="model"),
24
+ package_dir={"": "model"},
25
+ install_requires=read_requirements(),
26
+ author="Adibvafa Fallahpour",
27
+ author_email="Adibvafa.fallahpour@mail.utoronto.ca",
28
+ description=(
29
+ "The ultimate tool for codon optimization, "
30
+ "transforming protein sequences into optimized DNA sequences "
31
+ "specific for your target organisms."
32
+ ),
33
+ long_description=read_readme(),
34
+ long_description_content_type="text/markdown",
35
+ url="https://github.com/adibvafa/CodonTransformer",
36
+ classifiers=[
37
+ "Programming Language :: Python :: 3",
38
+ "License :: OSI Approved :: Apache Software License",
39
+ "Operating System :: OS Independent",
40
+ ],
41
+ python_requires=">=3.9",
42
+ )
weight/checkpoints/README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ #放置CodonTransformer 模型权重