OneScience commited on
Commit
4516781
·
verified ·
1 Parent(s): 7d83ead

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .msc +0 -0
  3. .mv +1 -0
  4. LICENSE +203 -0
  5. README.md +248 -0
  6. flax_model/__init__.py +1 -0
  7. flax_model/alphagenome/__init__.py +39 -0
  8. flax_model/alphagenome/_sdk/__init__.py +25 -0
  9. flax_model/alphagenome/_sdk/colab_utils.py +62 -0
  10. flax_model/alphagenome/_sdk/data/__init__.py +15 -0
  11. flax_model/alphagenome/_sdk/data/fold_intervals.py +115 -0
  12. flax_model/alphagenome/_sdk/data/gene_annotation.py +445 -0
  13. flax_model/alphagenome/_sdk/data/genome.py +1181 -0
  14. flax_model/alphagenome/_sdk/data/junction_data.py +272 -0
  15. flax_model/alphagenome/_sdk/data/ontology.py +122 -0
  16. flax_model/alphagenome/_sdk/data/track_data.py +918 -0
  17. flax_model/alphagenome/_sdk/data/transcript.py +755 -0
  18. flax_model/alphagenome/_sdk/interpretation/__init__.py +15 -0
  19. flax_model/alphagenome/_sdk/interpretation/ism.py +150 -0
  20. flax_model/alphagenome/_sdk/models/__init__.py +15 -0
  21. flax_model/alphagenome/_sdk/models/dna_client.py +907 -0
  22. flax_model/alphagenome/_sdk/models/dna_model.py +507 -0
  23. flax_model/alphagenome/_sdk/models/dna_output.py +435 -0
  24. flax_model/alphagenome/_sdk/models/interval_scorers.py +145 -0
  25. flax_model/alphagenome/_sdk/models/junction_data_utils.py +216 -0
  26. flax_model/alphagenome/_sdk/models/track_data_utils.py +260 -0
  27. flax_model/alphagenome/_sdk/models/variant_scorers.py +878 -0
  28. flax_model/alphagenome/_sdk/protos/__init__.py +15 -0
  29. flax_model/alphagenome/_sdk/protos/dna_model.proto +559 -0
  30. flax_model/alphagenome/_sdk/protos/dna_model_pb2.py +100 -0
  31. flax_model/alphagenome/_sdk/protos/dna_model_pb2_grpc.py +4 -0
  32. flax_model/alphagenome/_sdk/protos/dna_model_service.proto +268 -0
  33. flax_model/alphagenome/_sdk/protos/dna_model_service_pb2.py +57 -0
  34. flax_model/alphagenome/_sdk/protos/dna_model_service_pb2_grpc.py +274 -0
  35. flax_model/alphagenome/_sdk/protos/tensor.proto +104 -0
  36. flax_model/alphagenome/_sdk/protos/tensor_pb2.py +33 -0
  37. flax_model/alphagenome/_sdk/protos/tensor_pb2_grpc.py +4 -0
  38. flax_model/alphagenome/_sdk/tensor_utils.py +171 -0
  39. flax_model/alphagenome/_sdk/typing.py +39 -0
  40. flax_model/alphagenome/_sdk/visualization/__init__.py +15 -0
  41. flax_model/alphagenome/_sdk/visualization/plot.py +569 -0
  42. flax_model/alphagenome/_sdk/visualization/plot_components.py +1555 -0
  43. flax_model/alphagenome/_sdk/visualization/plot_transcripts.py +480 -0
  44. flax_model/alphagenome/evals/__init__.py +14 -0
  45. flax_model/alphagenome/evals/regression_metrics.py +204 -0
  46. flax_model/alphagenome/evals/track_prediction.py +202 -0
  47. flax_model/alphagenome/finetuning/__init__.py +15 -0
  48. flax_model/alphagenome/finetuning/dataset.py +236 -0
  49. flax_model/alphagenome/finetuning/dataset_test.py +198 -0
  50. flax_model/alphagenome/finetuning/finetune.py +153 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* 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
 
 
 
 
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
36
+ weight/alphagenome-all-folds/ocdbt.process_0/d/48900811ab32b5ad3873d40e96d63846 filter=lfs diff=lfs merge=lfs -text
37
+ weight/alphagenome-all-folds/ocdbt.process_0/d/6b08a83c9d6a2ade925dbcdd91299ce2 filter=lfs diff=lfs merge=lfs -text
38
+ weight/alphagenome-all-folds/ocdbt.process_0/d/bda173d4f00d3afcef0a043614df05a6 filter=lfs diff=lfs merge=lfs -text
.msc ADDED
Binary file (13.1 kB). View file
 
.mv ADDED
@@ -0,0 +1 @@
 
 
1
+ Revision:master,CreatedAt:1784688508
LICENSE ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2025 Onescience Authors. All rights reserved.
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright 2025 Onescience Authors.
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tasks:
4
+ - genomic-sequence-modeling
5
+ frameworks:
6
+ - jax
7
+ language:
8
+ - en
9
+ - zh
10
+ tags:
11
+ - OneScience
12
+ - Life Sciences
13
+ - Genomics
14
+ - DNA Sequence Model
15
+ - Variant Effect Prediction
16
+ - AlphaGenome
17
+ datasets:
18
+ - OneScience/alphagenome_dataset
19
+ ---
20
+
21
+ <p align="center">
22
+ <strong>
23
+ <span style="font-size: 30px;">AlphaGenome</span>
24
+ </strong>
25
+ </p>
26
+
27
+ # Model Introduction
28
+
29
+ AlphaGenome is a DNA sequence model proposed by Google DeepMind. It can take DNA intervals up to 1 Mbp as input and predict multiple classes of genomic functional signals for track prediction and regulatory variant effect scoring.
30
+
31
+ Paper: Advancing regulatory variant effect prediction with AlphaGenome
32
+ https://www.nature.com/articles/s41586-025-10014-0
33
+
34
+ # Model Description
35
+
36
+ AlphaGenome is implemented based on JAX / Flax and supports genomic interval inference, variant effect scoring, track evaluation, and fine-tuning examples. This model package is accompanied by the ModelScope dataset `OneScience/alphagenome_dataset`, which can be used for quick local verification.
37
+
38
+ # Applicable Scenarios
39
+
40
+ | Scenario | Description |
41
+ | :---: | :--- |
42
+ | Genomic interval prediction | Input a reference genome FASTA, chromosome, and interval coordinates, and output predicted tracks for ATAC, DNase, CAGE, RNA-seq, ChIP, and other signals |
43
+ | Variant effect scoring | Input a VCF or built-in example variants, compare prediction differences between reference and variant sequences, and generate a variant scoring table |
44
+ | Track prediction evaluation | Use validation data from the AlphaGenome dataset to calculate regression evaluation metrics for different assay bundles |
45
+ | Fine-tuning experiments | Use a custom reference genome, interval CSV, and BigWig signal files to verify the fine-tuning workflow |
46
+ | ModelScope / OneCode runtime | After downloading the model project and accompanying dataset, quickly verify script connectivity in a biology-domain runtime environment |
47
+
48
+
49
+
50
+ # Usage Instructions
51
+
52
+ ## 1. OneCode Usage
53
+
54
+ You can experience intelligent one-click AI4S programming through the OneCode online environment:
55
+
56
+ [Click to experience intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
57
+
58
+ ## 2. Manual Installation and Usage
59
+
60
+ **Hardware Requirements**
61
+
62
+ - GPU or DCU runtime is recommended.
63
+ - CPU can be used for import checks and small-configuration connectivity verification, but full training and inference are slow.
64
+ - DCU users need to install DTK in advance. DTK 25.04.2 or later is recommended, or the OneScience-recommended version matching the current cluster.
65
+
66
+
67
+
68
+
69
+
70
+ **Environment Check**
71
+
72
+ - NVIDIA GPU:
73
+
74
+ ```bash
75
+ nvidia-smi
76
+ ```
77
+
78
+ - Hygon DCU:
79
+
80
+ ```bash
81
+ hy-smi
82
+ ```
83
+
84
+ ### Download the Model Package
85
+
86
+ ```bash
87
+ modelscope download --model OneScience/alphagenome --local_dir ./alphagenome
88
+ cd alphagenome
89
+ ```
90
+
91
+ ### Install the Runtime Environment
92
+
93
+ **DCU Environment**
94
+
95
+ ```bash
96
+ # First activate DTK and Conda
97
+ conda create -n onescience311 python=3.11 -y
98
+ conda activate onescience311
99
+ # Supports uv installation
100
+ pip install onescience[bio-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
101
+ ```
102
+
103
+ After installation, return to the model package directory:
104
+
105
+ ```bash
106
+ cd ./alphagenome
107
+ ```
108
+
109
+ ### Training and Inference Data Introduction
110
+
111
+ The OneScience community has uploaded the data required for AlphaGenome inference, evaluation, and fine-tuning to ModelScope: [OneScience/alphagenome_dataset](https://modelscope.cn/datasets/OneScience/alphagenome_dataset). After downloading, place the data in the `data/` directory of the model package.
112
+
113
+ ```bash
114
+ modelscope download --dataset OneScience/alphagenome_dataset --local_dir ./data
115
+ ```
116
+
117
+ ### Training Weights
118
+
119
+ The repository includes `weight/alphagenome-all-folds`, and all scripts also support specifying model weights through `--model_dir`.
120
+
121
+ ### Prepare Weights
122
+
123
+ If using local weights, place the AlphaGenome Orbax checkpoint in the following directory:
124
+
125
+ ```text
126
+ weight/
127
+ alphagenome-all-folds/
128
+ _CHECKPOINT_METADATA
129
+ _METADATA
130
+ ...
131
+ ```
132
+
133
+ If running in a shared runtime environment, you can also reuse a unified directory through environment variables:
134
+
135
+ ```bash
136
+ export ONESCIENCE_MODELS_DIR=/path/to/onescience/models
137
+ export ONESCIENCE_DATASETS_DIR=/path/to/onescience/datasets
138
+ ```
139
+
140
+ The scripts will preferentially read:
141
+
142
+ - `${ONESCIENCE_MODELS_DIR}/AlphaGenome/alphagenome-all-folds`
143
+ - `${ONESCIENCE_DATASETS_DIR}/AlphaGenome`
144
+
145
+ If the above environment variables are not set, the defaults under the current model package are read:
146
+
147
+ - `weight/alphagenome-all-folds`
148
+ - `data/`
149
+
150
+ ### Interval Inference
151
+
152
+ ```bash
153
+ bash scripts/inference.sh
154
+ ```
155
+
156
+ Equivalent Python command example:
157
+
158
+ ```bash
159
+ python scripts/run_inference.py \
160
+ --fasta_path ./data/reference/HOMO_SAPIENS/GRCh38.p13.genome.fa \
161
+ --model_dir ./weight/alphagenome-all-folds \
162
+ --chromosome chr19 \
163
+ --start 10587331 \
164
+ --end 11635907 \
165
+ --output_dir ./outputs
166
+ ```
167
+
168
+ Inference results will be saved to `outputs/`.
169
+
170
+ ### Variant Effect Scoring
171
+
172
+ ```bash
173
+ bash scripts/run_variant.sh
174
+ ```
175
+
176
+ When specifying VCF input, you can use:
177
+
178
+ ```bash
179
+ python scripts/run_variant_scoring.py \
180
+ --vcf_path ./data/example.vcf \
181
+ --fasta_path ./data/reference/HOMO_SAPIENS/GRCh38.p13.genome.fa \
182
+ --model_dir ./weight/alphagenome-all-folds \
183
+ --output_dir ./outputs_variant
184
+ ```
185
+
186
+ Scoring results will be saved as CSV files.
187
+
188
+ ### Track Prediction Evaluation
189
+
190
+ ```bash
191
+ bash scripts/run_track.sh
192
+ ```
193
+
194
+ You can also explicitly specify the data and output paths:
195
+
196
+ ```bash
197
+ python scripts/run_track_prediction_eval.py \
198
+ --model_dir ./weight/alphagenome-all-folds \
199
+ --model_version ALL_FOLDS \
200
+ --data_dir ./data/v1/train \
201
+ --output_path ./outputs_track/eval_results.csv
202
+ ```
203
+
204
+ ### Fine-tuning Example
205
+
206
+ ```bash
207
+ python scripts/run_finetuning.py \
208
+ --fasta_path ./data/reference/HOMO_SAPIENS/GRCh38.p13.genome.fa \
209
+ --regions_csv ./data/finetune_regions.csv \
210
+ --bigwig_paths ./data/sample_atac.bw \
211
+ --output_dir ./finetuned_model \
212
+ --num_steps 1000 \
213
+ --batch_size 2
214
+ ```
215
+
216
+ # Data Format
217
+
218
+ The ModelScope dataset `OneScience/alphagenome_dataset` is recommended to be downloaded to `data/` under the model package. The default structure is as follows:
219
+
220
+ ```text
221
+ data/
222
+ reference/
223
+ HOMO_SAPIENS/
224
+ GRCh38.p13.genome.fa
225
+ GRCh38.p13.genome.fa.fai
226
+ v1/
227
+ train/
228
+ ...
229
+ ```
230
+
231
+ Where:
232
+
233
+ - `reference/HOMO_SAPIENS/GRCh38.p13.genome.fa` is the human reference genome FASTA.
234
+ - `.fai` is the FASTA index file.
235
+ - `v1/train/` is the data directory used for track prediction evaluation.
236
+ - Custom fine-tuning also requires preparing an interval CSV file with column names `chromosome,start,end`, as well as one or more BigWig signal files.
237
+
238
+ # Official OneScience Information
239
+
240
+ | Platform | OneScience Main Repository | Skills Repository |
241
+ | --- | --- | --- |
242
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
243
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
244
+
245
+ # Citations and License
246
+
247
+ - This repository is based on the AlphaGenome open-source model and provides DCU adaptation. The related source code uses the Apache License 2.0.
248
+ - For scientific research, cite the original paper: [Advancing regulatory variant effect prediction with AlphaGenome](https://www.nature.com/articles/s41586-025-10014-0).
flax_model/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Local flax_models copied from OneScience for this standalone package."""
flax_model/alphagenome/__init__.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """AlphaGenome - 统一DNA序列基因组模型,已集成至OneScience。
16
+
17
+ AlphaGenome 是 Google DeepMind 发布的统一DNA序列模型,能够分析最长100万碱基对的
18
+ DNA序列,以单碱基对分辨率预测基因表达、剪接模式、染色质特征和接触图谱。
19
+
20
+ 模块结构:
21
+ model/ - 核心模型组件 (AlphaGenome, SequenceEncoder, 各类Head)
22
+ io/ - 数据输入输出 (BundleName, Dataset, Fasta)
23
+ finetuning/ - 微调工具 (训练步骤、数据管道)
24
+ evals/ - 评估指标 (track预测、回归指标)
25
+ _sdk/ - AlphaGenome SDK (vendored alphagenome v0.6.1)
26
+ 包含数据类型、gRPC客户端、变异/区间评分、可视化和ISM解释工具
27
+
28
+ 使用方式:
29
+ from flax_model.alphagenome.model import model as alphagenome_model
30
+ from flax_model.alphagenome.io import dataset
31
+ from flax_model.alphagenome.finetuning import finetune
32
+
33
+ # SDK 数据类型 (原 from alphagenome.data import genome):
34
+ from flax_model.alphagenome._sdk.data import genome
35
+ from flax_model.alphagenome._sdk.models import dna_model
36
+ from flax_model.alphagenome._sdk.models import dna_output
37
+ """
38
+
39
+ __version__ = '0.1.0'
flax_model/alphagenome/_sdk/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """AlphaGenome Python SDK (vendored into OneScience).
16
+
17
+ Original package: alphagenome v0.6.1
18
+ Provides data types, gRPC client, variant/interval scoring,
19
+ visualization, and ISM interpretation utilities for genomic models.
20
+
21
+ This is a vendored copy integrated into OneScience to avoid requiring
22
+ a separate alphagenome package installation.
23
+ """
24
+
25
+ __version__ = '0.6.1'
flax_model/alphagenome/_sdk/colab_utils.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utility functions for Google Colab."""
16
+
17
+ import os
18
+
19
+
20
+ def get_api_key(secret: str = 'ALPHA_GENOME_API_KEY'):
21
+ """Returns API key from environment variable or Colab secrets.
22
+
23
+ Tries to retrieve the API key from the environment first. If not found,
24
+ attempts to retrieve it from Colab secrets (if running in Colab).
25
+
26
+ Args:
27
+ secret: The name of the environment variable or Colab secret key to
28
+ retrieve.
29
+
30
+ Raises:
31
+ ValueError: If the API key cannot be found in the environment or Colab
32
+ secrets.
33
+ """
34
+
35
+ if api_key := os.environ.get(secret):
36
+ return api_key
37
+
38
+ try:
39
+ # pylint: disable=g-import-not-at-top, import-outside-toplevel
40
+ from google.colab import userdata # pytype: disable=import-error
41
+ # pylint: enable=g-import-not-at-top, import-outside-toplevel
42
+
43
+ try:
44
+ api_key = userdata.get(secret)
45
+ return api_key
46
+ except (
47
+ userdata.NotebookAccessError,
48
+ userdata.SecretNotFoundError,
49
+ userdata.TimeoutException,
50
+ ) as e:
51
+ raise ValueError(
52
+ f'Cannot find or access API key in Colab secrets with {secret=}. Make'
53
+ ' sure you have added the API key to Colab secrets and enabled'
54
+ ' access. See'
55
+ ' https://www.alphagenomedocs.com/installation.html#add-api-key-to-secrets'
56
+ ' for more details.'
57
+ ) from e
58
+ except ImportError:
59
+ # Not running in Colab.
60
+ pass
61
+
62
+ raise ValueError(f'Cannot find API key with {secret=}.')
flax_model/alphagenome/_sdk/data/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Data classes for interacting and visualizing genomic models."""
flax_model/alphagenome/_sdk/data/fold_intervals.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Genomics intervals used for training model folds."""
16
+
17
+ import enum
18
+
19
+ from flax_model.alphagenome._sdk.models import dna_client
20
+ import immutabledict
21
+ import pandas as pd
22
+
23
+
24
+ _DEFAULT_EXAMPLE_REGIONS = immutabledict.immutabledict({
25
+ dna_client.Organism.HOMO_SAPIENS: (
26
+ 'https://github.com/calico/borzoi/raw/'
27
+ '5c9358222b5026abb733ed5fb84f3f6c77239b37/data/sequences_human.bed.gz'
28
+ ),
29
+ dna_client.Organism.MUS_MUSCULUS: (
30
+ 'https://github.com/calico/borzoi/raw/'
31
+ '5c9358222b5026abb733ed5fb84f3f6c77239b37/data/sequences_mouse.bed.gz'
32
+ ),
33
+ })
34
+
35
+
36
+ class Subset(enum.Enum):
37
+ """Subset of the data."""
38
+
39
+ TRAIN = 0
40
+ VALID = 1
41
+ TEST = 2
42
+
43
+
44
+ # Fold ONE is aligned with all trained Borzoi checkpoints: 3 and 4 are held out.
45
+ _VALID_FOLD = immutabledict.immutabledict({
46
+ 0: 'fold0',
47
+ 1: 'fold3',
48
+ 2: 'fold2',
49
+ 3: 'fold6',
50
+ -1: 'fold0',
51
+ })
52
+
53
+ _TEST_FOLD = immutabledict.immutabledict({
54
+ 0: 'fold1',
55
+ 1: 'fold4',
56
+ 2: 'fold5',
57
+ 3: 'fold7',
58
+ -1: 'fold1',
59
+ })
60
+
61
+ _MODEL_VERSION_TO_FOLD = immutabledict.immutabledict({
62
+ dna_client.ModelVersion.FOLD_0: 0,
63
+ dna_client.ModelVersion.FOLD_1: 1,
64
+ dna_client.ModelVersion.FOLD_2: 2,
65
+ dna_client.ModelVersion.FOLD_3: 3,
66
+ dna_client.ModelVersion.ALL_FOLDS: -1,
67
+ })
68
+
69
+
70
+ def get_all_folds() -> list[str]:
71
+ """Returns the names of all data folds."""
72
+ return [f'fold{i}' for i in range(8)]
73
+
74
+
75
+ def get_fold_names(
76
+ model_version: dna_client.ModelVersion, subset: Subset
77
+ ) -> list[str]:
78
+ """Returns the names of the folds for a given model version and subset."""
79
+ match subset:
80
+ case Subset.VALID:
81
+ return [_VALID_FOLD[_MODEL_VERSION_TO_FOLD[model_version]]]
82
+ case Subset.TEST:
83
+ return [_TEST_FOLD[_MODEL_VERSION_TO_FOLD[model_version]]]
84
+ case Subset.TRAIN:
85
+ all_folds = get_all_folds()
86
+ if _MODEL_VERSION_TO_FOLD[model_version] == -1:
87
+ return all_folds
88
+ remove_folds = get_fold_names(
89
+ model_version, Subset.VALID
90
+ ) + get_fold_names(model_version, Subset.TEST)
91
+ for fold in remove_folds:
92
+ all_folds.remove(fold)
93
+ return all_folds
94
+ case _:
95
+ raise ValueError(f'Unknown {subset=}')
96
+
97
+
98
+ def get_fold_intervals(
99
+ model_version: dna_client.ModelVersion,
100
+ organism: dna_client.Organism,
101
+ subset: Subset,
102
+ example_regions_path: str | None = None,
103
+ ) -> pd.DataFrame:
104
+ """Returns the intervals for a given model version and subset."""
105
+ if example_regions_path is None:
106
+ example_regions_path = _DEFAULT_EXAMPLE_REGIONS[organism]
107
+
108
+ example_regions = pd.read_csv(
109
+ example_regions_path,
110
+ sep='\t',
111
+ names=['chromosome', 'start', 'end', 'fold'],
112
+ )
113
+ return example_regions[
114
+ example_regions.fold.isin(get_fold_names(model_version, subset))
115
+ ]
flax_model/alphagenome/_sdk/data/gene_annotation.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utilities for working with gene annotations (e.g., GTFs)."""
16
+
17
+ from collections.abc import Sequence
18
+ import enum
19
+
20
+ from flax_model.alphagenome._sdk.data import genome
21
+ import numpy as np
22
+ import pandas as pd
23
+
24
+
25
+ @enum.unique
26
+ class TranscriptType(enum.Enum):
27
+ """Valid Transcript types available in the GENCODE GTF."""
28
+
29
+ IG_C_GENE = 'IG_C_gene'
30
+ IG_C_PSEUDOGENE = 'IG_C_pseudogene'
31
+ IG_D_GENE = 'IG_D_gene'
32
+ IG_J_GENE = 'IG_J_gene'
33
+ IG_J_PSEUDOGENE = 'IG_J_pseudogene'
34
+ IG_V_GENE = 'IG_V_gene'
35
+ IG_V_PSEUDOGENE = 'IG_V_pseudogene'
36
+ IG_PSEUDOGENE = 'IG_pseudogene'
37
+ MT_RRNA = 'Mt_rRNA'
38
+ MT_TRNA = 'Mt_tRNA'
39
+ TEC = 'TEC'
40
+ TR_C_GENE = 'TR_C_gene'
41
+ TR_D_GENE = 'TR_D_gene'
42
+ TR_J_GENE = 'TR_J_gene'
43
+ TR_J_PSEUDOGENE = 'TR_J_pseudogene'
44
+ TR_V_GENE = 'TR_V_gene'
45
+ TR_V_PSEUDOGENE = 'TR_V_pseudogene'
46
+ ARTIFACT = 'artifact'
47
+ LNCRNA = 'lncRNA'
48
+ MIRNA = 'miRNA'
49
+ MISC_RNA = 'misc_RNA'
50
+ NON_STOP_DECAY = 'non_stop_decay'
51
+ NONSENSE_MEDIATED_DECAY = 'nonsense_mediated_decay'
52
+ PROCESSED_PSEUDOGENE = 'processed_pseudogene'
53
+ PROCESSED_TRANSCRIPT = 'processed_transcript'
54
+ PROTEIN_CODING = 'protein_coding'
55
+ PROTEIN_CODING_CDS_NOT_DEFINED = 'protein_coding_CDS_not_defined'
56
+ PROTEIN_CODING_LOF = 'protein_coding_LoF'
57
+ RRNA = 'rRNA'
58
+ RRNA_PSEUDOGENE = 'rRNA_pseudogene'
59
+ RETAINED_INTRON = 'retained_intron'
60
+ RIBOZYME = 'ribozyme'
61
+ SRNA = 'sRNA'
62
+ SCRNA = 'scRNA'
63
+ SCARNA = 'scaRNA'
64
+ SNRNA = 'snRNA'
65
+ SNORNA = 'snoRNA'
66
+ TRANSCRIBED_PROCESSED_PSEUDOGENE = 'transcribed_processed_pseudogene'
67
+ TRANSCRIBED_UNITARY_PSEUDOGENE = 'transcribed_unitary_pseudogene'
68
+ TRANSCRIBED_UNPROCESSED_PSEUDOGENE = 'transcribed_unprocessed_pseudogene'
69
+ TRANSLATED_PROCESSED_PSEUDOGENE = 'translated_processed_pseudogene'
70
+ UNITARY_PSEUDOGENE = 'unitary_pseudogene'
71
+ UNPROCESSED_PSEUDOGENE = 'unprocessed_pseudogene'
72
+ VAULT_RNA = 'vault_RNA'
73
+
74
+
75
+ def extract_tss(gtf: pd.DataFrame, feature: str = 'transcript') -> pd.DataFrame:
76
+ """Extract transcription start sites (TSS) from a DataFrame.
77
+
78
+ Args:
79
+ gtf: pd.DataFrame containing gene annotation.
80
+ feature: Feature in the GTF file to use (either transcript or gene).
81
+
82
+ Returns:
83
+ pd.DataFrame containing transcription start sites as zero-width point
84
+ intervals (Start == End, 0-based).
85
+ """
86
+ tss = gtf[(gtf.Feature == feature)].copy()
87
+
88
+ # Remove the extra base to make it width=0.
89
+ # .....[)TRANSCRIPT (strand = +)
90
+ # TPIRCSNART[)..... (strand = -)
91
+ new_start = np.where(tss.Strand == '-', tss.End, tss.Start)
92
+ tss.Start = new_start
93
+ tss.End = new_start
94
+
95
+ return tss
96
+
97
+
98
+ def filter_transcript_type(
99
+ gtf: pd.DataFrame,
100
+ transcript_types: tuple[TranscriptType, ...] | None = None,
101
+ ) -> pd.DataFrame:
102
+ """Filter GTF entries by transcript types.
103
+
104
+ This function takes a GTF DataFrame and a list of transcript types and returns
105
+ a new DataFrame containing only the transcripts with the specified types.
106
+
107
+ The GTF DataFrame must contain a column named 'transcript_type' or
108
+ 'transcript_biotype'. The function will raise a ValueError if neither of these
109
+ columns is present.
110
+
111
+ Args:
112
+ gtf: pd.DataFrame or pyranges.PyRanges.
113
+ transcript_types: List of valid transcript types to use for filtering.
114
+
115
+ Returns:
116
+ pd.DataFrame of GENCODE GTF entries subset to rows with the requested
117
+ transcript types.
118
+ """
119
+ if transcript_types is not None:
120
+ transcript_types_str = [x.value for x in transcript_types]
121
+ if 'transcript_type' in gtf.columns:
122
+ gtf = gtf[gtf.transcript_type.isin(transcript_types_str)]
123
+ elif 'transcript_biotype' in gtf.columns:
124
+ gtf = gtf[gtf.transcript_biotype.isin(transcript_types_str)]
125
+ else:
126
+ raise ValueError('transcript_type or transcript_biotype not in gtf.')
127
+ return gtf
128
+
129
+
130
+ def filter_protein_coding(
131
+ gtf: pd.DataFrame, include_gene_entries: bool = False
132
+ ) -> pd.DataFrame:
133
+ """Filter GTF entries to only protein-coding genes.
134
+
135
+ Args:
136
+ gtf: pd.DataFrame of GENCODE GTF entries. This data frame must contain a
137
+ column named 'transcript_type' or 'transcript_biotype'.
138
+ include_gene_entries: Whether to include gene entries in addition to
139
+ transcript entries.
140
+
141
+ Returns:
142
+ pd.DataFrame of GENCODE GTF entries subset to rows with protein-coding
143
+ genes.
144
+ """
145
+ if include_gene_entries:
146
+ if 'gene_type' in gtf.columns:
147
+ gtf = gtf[gtf.gene_type == TranscriptType.PROTEIN_CODING.value]
148
+ else:
149
+ raise ValueError('gene_type not in gtf.')
150
+ else:
151
+ gtf = filter_transcript_type(gtf, (TranscriptType.PROTEIN_CODING,))
152
+ return gtf
153
+
154
+
155
+ def filter_to_longest_transcript(
156
+ gtf: pd.DataFrame,
157
+ ) -> pd.DataFrame:
158
+ """Filter GTF entries to only the longest transcript per gene.
159
+
160
+ Args:
161
+ gtf: pd.DataFrame of GENCODE GTF entries. Must contain columns 'Feature',
162
+ 'End', 'Start', 'gene_id', and 'transcript_id'.
163
+
164
+ Returns:
165
+ pd.DataFrame of GENCODE GTF entries subset to rows with the longest
166
+ transcript per gene.
167
+ """
168
+ lengths = gtf[gtf['Feature'] == 'transcript'].reset_index(drop=True)
169
+ lengths['transcript_length'] = lengths['End'] - lengths['Start'] + 1
170
+
171
+ # Identify longest transcripts per gene_id.
172
+ longest_transcripts = lengths.loc[
173
+ lengths.groupby('gene_id')['transcript_length'].idxmax()
174
+ ]
175
+
176
+ return gtf[gtf['transcript_id'].isin(longest_transcripts['transcript_id'])]
177
+
178
+
179
+ def filter_to_mane_select_transcript(gtf: pd.DataFrame) -> pd.DataFrame:
180
+ """Filter GTF entries to only the MANE select transcript.
181
+
182
+ Note that the MANE_Select tag only exists for the human GTF file.
183
+
184
+ Args:
185
+ gtf: pd.DataFrame of GENCODE GTF entries. Must contain columns 'tag'.
186
+
187
+ Returns:
188
+ pd.DataFrame of GENCODE GTF entries subset to rows representing MANE
189
+ select transcripts, which are transcripts that are well-supported,
190
+ conserved, and expressed.
191
+ """
192
+ filtered_gtf = gtf[gtf['tag'].fillna('').str.contains('MANE_Select')]
193
+ if filtered_gtf.empty:
194
+ raise ValueError(
195
+ 'No MANE_Select transcripts found in the GTF, possibly due to non-human'
196
+ ' GTF.'
197
+ )
198
+ return filtered_gtf
199
+
200
+
201
+ def filter_transcript_support_level(
202
+ gtf: pd.DataFrame,
203
+ transcript_support_levels: str | Sequence[str],
204
+ ) -> pd.DataFrame:
205
+ """Filter GTF to only transcripts with specific GENCODE support levels.
206
+
207
+ As documented in the [Ensembl
208
+ glossary](https://www.ensembl.org/Help/Glossary),
209
+ the transcript support level (TSL) indicates the degree of evidence that was
210
+ used to construct the transcript.
211
+
212
+ As taken from the glossary, the levels are:
213
+
214
+ | Transcript support level | Description | |
215
+ |---|---|---|
216
+ | 1 | A transcript where all splice junctions are supported by at least one
217
+ non-suspect mRNA. |
218
+ | 2 | A transcript where the best supporting mRNA is flagged as suspect or the
219
+ support is from multiple ESTs |
220
+ | 3 | A transcript where the only support is from a single EST |
221
+ | 4 | A transcript where the best supporting EST is flagged as suspect |
222
+ | 5 | A transcript where no single transcript supports the model structure. |
223
+ | NA | A transcript that was not analysed for TSL. |
224
+
225
+ Args:
226
+ gtf: pd.DataFrame of GENCODE GTF entries. Must contain column
227
+ 'transcript_support_level'.
228
+ transcript_support_levels: List of valid transcript support levels to use
229
+ for filtering. This must be a subset of ['1', '2', '3', '4', '5']. Can
230
+ also be single string.
231
+
232
+ Returns:
233
+ pd.DataFrame exactly as provided, but subset to rows with the specified
234
+ support level(s).
235
+
236
+ Transcripts are scored by GENCODE according to how well mRNA and EST
237
+ alignments
238
+ match over its full length. Valid levels are:
239
+ '1': All splice junctions of the transcript are supported by at least one
240
+ non-suspect mRNA.
241
+ '2': The best supporting mRNA is flagged as suspect or the support is from
242
+ multiple ESTs.
243
+ '3': The only support is from a single EST.
244
+ '4': The best supporting EST is flagged as suspect.
245
+ '5': No single transcript supports the model structure.
246
+ 'NA': The transcript was not analyzed (not supported by this filter function).
247
+
248
+ See GENCODE GTF format documentation for further details:
249
+ https://www.gencodegenes.org/pages/data_format.html
250
+ """
251
+ if isinstance(transcript_support_levels, str):
252
+ transcript_support_levels = list(transcript_support_levels)
253
+
254
+ supported_tsls = {'1', '2', '3', '4', '5'}
255
+ if not set(transcript_support_levels).issubset(supported_tsls):
256
+ raise ValueError(
257
+ f'transcript_support_level must be one of {supported_tsls}, but was'
258
+ f' {transcript_support_levels}'
259
+ )
260
+ return gtf[gtf.transcript_support_level.isin(transcript_support_levels)]
261
+
262
+
263
+ def upgrade_annotation_ids(
264
+ old_ids: pd.Series, new_ids: pd.Series, patchless: bool = False
265
+ ) -> pd.Series:
266
+ """Upgrade or add transcript id patch version to Ensembl IDs.
267
+
268
+ This function works by
269
+
270
+ 1. Dropping the patch version from `old_ids` and `new_ids`
271
+ 2. Merging the two on the patch-less ids.
272
+ 3. Returning the result of the merge as a pd.Series, with the 'old_ids' as
273
+ the index and the 'new_ids' as the values.
274
+
275
+ Ensembl patch versions have two formats: ENST####.<patch>_PAR_Y or
276
+ ENST####.<patch>. Both are handled.
277
+
278
+ The function will raise a ValueError if either `old_ids` or `new_ids` result
279
+ in duplicates after dropping the patch version.
280
+
281
+ Examples:
282
+ * If the old ids are ENST00010.1 and the new ids are ENST00010.3,
283
+ then the mapping will be ENST00010.1 -> ENST00010.3.
284
+ * If the old ids are ENST00010 and the new ids are ENST00010.3, then the
285
+ mapping will be ENST00010 -> ENST00010.3.
286
+
287
+ Args:
288
+ old_ids: A pd.Series of Ensembl transcript or gene ids with older or missing
289
+ version/patch numbers. The index of the series is ignored.
290
+ new_ids: A pd.Series of transcript or gene ids with newer version/patch
291
+ numbers. The index of the series is ignored.
292
+ patchless: whether old_ids are missing patch.
293
+
294
+ Returns:
295
+ A pd.Series, with the 'old_ids' as the index and the 'new_ids' as the
296
+ values.
297
+ """
298
+ new_ids = new_ids.drop_duplicates()
299
+
300
+ def drop_version(x):
301
+ """Drop the patch version from an Ensembl ID."""
302
+ if not x.str.contains('.', regex=False).all():
303
+ raise ValueError('All ids need to contain the patch version.')
304
+
305
+ id_split = x.str.partition('.')
306
+
307
+ # Retain anything after _ such as PAR_Y for ENST####.<patch>_PAR_Y.
308
+ return id_split[0] + id_split[2].str.partition('_')[2]
309
+
310
+ old_ids_nopatch = old_ids if patchless else drop_version(old_ids)
311
+ new_ids_nopatch = drop_version(new_ids)
312
+ assert (
313
+ not old_ids_nopatch.duplicated().any()
314
+ ), 'old_ids not unique without version'
315
+ assert (
316
+ not new_ids_nopatch.duplicated().any()
317
+ ), 'new_ids not unique without version'
318
+ df = pd.merge(
319
+ pd.DataFrame({'old': old_ids.values, 'no_version': old_ids_nopatch}),
320
+ pd.DataFrame({
321
+ 'new': new_ids.values,
322
+ 'no_version': new_ids_nopatch,
323
+ }),
324
+ on='no_version',
325
+ how='left',
326
+ )
327
+ return pd.Series(
328
+ df.set_index('old').loc[old_ids].new.values, index=old_ids.index
329
+ )
330
+
331
+
332
+ def get_gene_intervals(
333
+ gtf: pd.DataFrame,
334
+ gene_symbols: Sequence[str] | None = None,
335
+ gene_ids: Sequence[str] | None = None,
336
+ ) -> list[genome.Interval]:
337
+ """Returns a list of stranded `genome.Interval`s for the given identifiers.
338
+
339
+ Args:
340
+ gtf: pd.DataFrame of GENCODE GTF entries. Must contain columns 'Feature',
341
+ 'gene_name', 'gene_id', 'Chromosome', 'Start', 'End', and 'Strand'.
342
+ gene_symbols: A sequence of gene names or gene symbols (e.g., ['EGFR',
343
+ 'TNF', 'TP53']). Matching is case-insensitive.
344
+ gene_ids: A sequence of Ensembl gene IDs, which can be patched (e.g.
345
+ ['ENSG00000141510.17']) or unpatched (e.g., ['ENSG00000141510']). Matching
346
+ is done on unpatched IDs.
347
+
348
+ Returns:
349
+ A list of `genome.Interval`s for the given identifiers. The
350
+ returned list of intervals is in the same order as the input gene
351
+ identifiers.
352
+
353
+ Raises:
354
+ ValueError: If neither or both gene_symbols and gene_ids are set, or if no
355
+ interval or multiple intervals are found for any of the given gene
356
+ identifiers.
357
+ """
358
+ if (gene_symbols is None) == (gene_ids is None):
359
+ raise ValueError('Exactly one of gene_symbols or gene_ids must be set.')
360
+
361
+ gtf_genes = gtf[gtf['Feature'] == 'gene'].copy()
362
+
363
+ if gene_symbols is not None:
364
+ id_col = 'gene_name'
365
+ input_ids = gene_symbols
366
+ process_fn = lambda s: s.str.upper()
367
+ else:
368
+ id_col = 'gene_id'
369
+ input_ids = gene_ids
370
+ process_fn = lambda s: s.str.split('.', n=1).str[0]
371
+
372
+ processed_input_ids = process_fn(pd.Series(input_ids, dtype=str))
373
+ gtf_genes['processed_id'] = process_fn(gtf_genes[id_col])
374
+
375
+ # Filter the GTF to only the genes that are in the input IDs.
376
+ gtf_subset = gtf_genes[
377
+ gtf_genes['processed_id'].isin(processed_input_ids.unique())
378
+ ]
379
+
380
+ dup_mask = gtf_subset['processed_id'].duplicated(keep=False)
381
+ if dup_mask.any():
382
+ offending_ids = gtf_subset.loc[dup_mask, id_col].unique()
383
+ raise ValueError(
384
+ 'Multiple intervals found for gene(s):'
385
+ f' {", ".join(sorted(offending_ids))}.'
386
+ )
387
+
388
+ # Create a lookup map from processed_id to GTF data.
389
+ # Use reindex to order genes by input and insert NaNs for missing genes.
390
+ gtf_map = gtf_subset.set_index('processed_id')
391
+ result_df = gtf_map.reindex(processed_input_ids)
392
+
393
+ missing_mask = result_df['Chromosome'].isnull()
394
+ if missing_mask.any():
395
+ missing_ids = pd.Series(input_ids)[missing_mask.values].unique()
396
+ raise ValueError(
397
+ f'No interval found for gene(s): {", ".join(sorted(missing_ids))}.'
398
+ )
399
+
400
+ # Add original identifiers to the result for the 'name' field of the interval.
401
+ result_df[id_col] = list(input_ids)
402
+
403
+ return [
404
+ genome.Interval(
405
+ chromosome=row.Chromosome,
406
+ start=row.Start,
407
+ end=row.End,
408
+ strand=row.Strand,
409
+ name=getattr(row, id_col),
410
+ )
411
+ for row in result_df.itertuples()
412
+ ]
413
+
414
+
415
+ def get_gene_interval(
416
+ gtf: pd.DataFrame,
417
+ gene_symbol: str | None = None,
418
+ gene_id: str | None = None,
419
+ ) -> genome.Interval:
420
+ """Returns a stranded `genome.Interval` given a gene identifier.
421
+
422
+ Either gene_symbol or gene_id must be set, but not both.
423
+
424
+ Args:
425
+ gtf: pd.DataFrame of GENCODE GTF entries. Must contain columns 'Feature',
426
+ 'gene_name', 'gene_id', 'Chromosome', 'Start', 'End', and 'Strand'.
427
+ gene_symbol: A gene name or gene symbol (e.g., 'EGFR', 'TNF', 'TP53')
428
+ gene_id: An Ensembl gene ID, which can be patched (e.g.
429
+ 'ENSG00000141510.17') or unpatched (e.g., 'ENSG00000141510').
430
+
431
+ Returns:
432
+ A `genome.Interval` for the given gene identifier.
433
+
434
+ Raises:
435
+ ValueError: If neither or both gene_symbol and gene_id are set, or if no
436
+ interval or multiple intervals are found for the given gene identifier.
437
+ """
438
+ if sum(x is not None for x in [gene_symbol, gene_id]) != 1:
439
+ raise ValueError('Exactly one of gene_symbol or gene_id must be set.')
440
+
441
+ return get_gene_intervals(
442
+ gtf,
443
+ [gene_symbol] if gene_symbol else None,
444
+ [gene_id] if gene_id else None,
445
+ )[0]
flax_model/alphagenome/_sdk/data/genome.py ADDED
@@ -0,0 +1,1181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utilities for working with genome-related objects such as intervals."""
16
+
17
+ import collections
18
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
19
+ import copy
20
+ import dataclasses
21
+ import enum
22
+ import re
23
+ import sys
24
+ from typing import Any, Protocol
25
+
26
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
27
+ import numpy as np
28
+ from typing_extensions import Self
29
+
30
+ STRAND_POSITIVE = '+' # Also called forward strand, 5'->3' direction.
31
+ STRAND_NEGATIVE = '-' # Also called negative strand, 3'->5' direction.
32
+ STRAND_UNSTRANDED = '.'
33
+ STRAND_OPTIONS = (STRAND_POSITIVE, STRAND_NEGATIVE, STRAND_UNSTRANDED)
34
+ _INTERVAL_START_END_REGEX = re.compile(r'(-?\d+)-(-?\d+)')
35
+ VALID_VARIANT_BASES = frozenset('ACGTN')
36
+
37
+
38
+ class Strand(enum.IntEnum):
39
+ """Represents the strand of a DNA sequence.
40
+
41
+ This enum defines the possible strands for a DNA sequence:
42
+
43
+ * `POSITIVE`: The forward strand (5' to 3').
44
+ * `NEGATIVE`: The reverse strand (3' to 5').
45
+ * `UNSTRANDED`: The strand is not specified.
46
+ """
47
+
48
+ POSITIVE = enum.auto()
49
+ NEGATIVE = enum.auto()
50
+ UNSTRANDED = enum.auto()
51
+
52
+ def __str__(self):
53
+ match self:
54
+ case Strand.POSITIVE:
55
+ return STRAND_POSITIVE
56
+ case Strand.NEGATIVE:
57
+ return STRAND_NEGATIVE
58
+ case Strand.UNSTRANDED:
59
+ return STRAND_UNSTRANDED
60
+
61
+ @classmethod
62
+ def from_str(cls, strand: str) -> Self:
63
+ match strand:
64
+ case '+':
65
+ return cls.POSITIVE
66
+ case '-':
67
+ return cls.NEGATIVE
68
+ case '.':
69
+ return cls.UNSTRANDED
70
+ case _:
71
+ raise ValueError(f'Strand needs to be in {STRAND_OPTIONS}')
72
+
73
+ def to_proto(self) -> dna_model_pb2.Strand:
74
+ match self:
75
+ case Strand.POSITIVE:
76
+ return dna_model_pb2.Strand.STRAND_POSITIVE
77
+ case Strand.NEGATIVE:
78
+ return dna_model_pb2.Strand.STRAND_NEGATIVE
79
+ case Strand.UNSTRANDED:
80
+ return dna_model_pb2.Strand.STRAND_UNSTRANDED
81
+
82
+ @classmethod
83
+ def from_proto(cls, strand: dna_model_pb2.Strand) -> Self:
84
+ match strand:
85
+ case dna_model_pb2.Strand.STRAND_POSITIVE:
86
+ return cls.POSITIVE
87
+ case dna_model_pb2.Strand.STRAND_NEGATIVE:
88
+ return cls.NEGATIVE
89
+ case dna_model_pb2.Strand.STRAND_UNSTRANDED:
90
+ return cls.UNSTRANDED
91
+ case _:
92
+ raise ValueError(f'Strand needs to be in {STRAND_OPTIONS}')
93
+
94
+
95
+ PYRANGES_INTERVAL_COLUMNS = ('Chromosome', 'Start', 'End', 'Strand', 'Name')
96
+
97
+
98
+ @dataclasses.dataclass(order=True)
99
+ class Interval:
100
+ """Represents a genomic interval.
101
+
102
+ A genomic interval is a region on a chromosome defined by a start and end
103
+ position. This class provides methods for manipulating and comparing
104
+ intervals, and for calculating coverage and overlap.
105
+
106
+ Attributes:
107
+ chromosome: The chromosome name (e.g., 'chr1', '1').
108
+ start: The 0-based start position.
109
+ end: The 0-based end position (must be greater than or equal to start).
110
+ strand: The strand of the interval ('+', '-', or '.'). Defaults to '.'
111
+ (unstranded).
112
+ name: An optional name for the interval.
113
+ info: An optional dictionary to store additional information.
114
+ negative_strand: True if the interval is on the negative strand, False
115
+ otherwise.
116
+ width: The width of the interval (end - start).
117
+ """
118
+
119
+ chromosome: str
120
+ start: int
121
+ end: int
122
+ strand: str = STRAND_UNSTRANDED
123
+ name: str = dataclasses.field(default='', compare=False, hash=False)
124
+ info: dict[str, Any] = dataclasses.field(
125
+ default_factory=dict, repr=False, compare=False, hash=False
126
+ )
127
+
128
+ def __post_init__(self):
129
+ if self.end < self.start:
130
+ raise ValueError('end < start. Interval: ' + str(self))
131
+ if self.strand not in STRAND_OPTIONS:
132
+ raise ValueError(
133
+ f'Strand needs to be in {STRAND_OPTIONS}, found {self.strand}.'
134
+ )
135
+
136
+ @property
137
+ def negative_strand(self) -> bool:
138
+ """Returns True if interval is on the negative strand, False otherwise."""
139
+ return self.strand == STRAND_NEGATIVE
140
+
141
+ @property
142
+ def width(self) -> int:
143
+ """Returns the width of the interval."""
144
+ return self.end - self.start
145
+
146
+ def copy(self) -> Self:
147
+ """Returns a deep copy of the interval."""
148
+ return copy.deepcopy(self)
149
+
150
+ def __str__(self) -> str:
151
+ """Returns a string representation of the interval."""
152
+ return f'{self.chromosome}:{self.start}-{self.end}:{self.strand}'
153
+
154
+ @classmethod
155
+ def from_str(cls, string: str) -> Self:
156
+ """Creates an Interval from a string (e.g., 'chr1:100-200:+')."""
157
+ chromosome, interval, *strand = string.split(':', maxsplit=2)
158
+ if strand:
159
+ strand = strand[0]
160
+ else:
161
+ strand = STRAND_UNSTRANDED
162
+ # Get start and end from the interval string.
163
+ match = _INTERVAL_START_END_REGEX.fullmatch(interval)
164
+ if match:
165
+ start, end = int(match.group(1)), int(match.group(2))
166
+ else:
167
+ raise ValueError(f'Invalid interval: {string}')
168
+ return cls(
169
+ chromosome=chromosome, start=int(start), end=int(end), strand=strand
170
+ )
171
+
172
+ def to_proto(self) -> dna_model_pb2.Interval:
173
+ """Converts the interval to a protobuf message."""
174
+ return dna_model_pb2.Interval(
175
+ chromosome=self.chromosome,
176
+ start=self.start,
177
+ end=self.end,
178
+ strand=Strand.from_str(self.strand).to_proto(),
179
+ )
180
+
181
+ @classmethod
182
+ def from_proto(cls, proto: dna_model_pb2.Interval) -> Self:
183
+ """Creates an Interval from a protobuf message."""
184
+ return cls(
185
+ chromosome=proto.chromosome,
186
+ start=proto.start,
187
+ end=proto.end,
188
+ strand=str(Strand.from_proto(proto.strand)),
189
+ )
190
+
191
+ def to_interval_dict(self) -> dict[str, str | int]:
192
+ """Converts the interval to a dictionary."""
193
+ return dict(
194
+ chrom=self.chromosome,
195
+ start=self.start,
196
+ end=self.end,
197
+ strand=self.strand,
198
+ )
199
+
200
+ @classmethod
201
+ def from_interval_dict(cls, interval: Mapping[str, str | int]) -> Self:
202
+ """Creates an Interval from a dictionary."""
203
+ return cls(
204
+ chromosome=str(interval['chrom']),
205
+ start=int(interval['start']),
206
+ end=int(interval['end']),
207
+ strand=str(interval.get('strand', STRAND_UNSTRANDED)),
208
+ )
209
+
210
+ @classmethod
211
+ def from_pyranges_dict(
212
+ cls, row: Mapping[str, Any], ignore_info: bool = False
213
+ ) -> 'Interval':
214
+ """Creates an Interval from a pyranges-like dictionary.
215
+
216
+ This method constructs an `Interval` object from a dictionary that follows
217
+ the pyranges format, such as a row from a :class:`pandas.DataFrame`
218
+ converted to a dict.
219
+
220
+ The dictionary should have the following keys:
221
+
222
+ * 'Chromosome': The chromosome name.
223
+ * 'Start': The start position.
224
+ * 'End': The end position.
225
+ * 'Strand': The strand (optional, defaults to unstranded).
226
+ * 'Name': The interval name (optional).
227
+
228
+ Any other keys in the dictionary will be added to the `info` attribute of
229
+ the `Interval` object, unless `ignore_info` is set to True.
230
+
231
+ Args:
232
+ row: A dictionary containing interval data.
233
+ ignore_info: If True, any keys in the dictionary that are not part of the
234
+ standard pyranges columns ('Chromosome', 'Start', 'End', 'Strand',
235
+ 'Name') will not be added to the `info` attribute.
236
+
237
+ Returns:
238
+ An `Interval` object created from the input dictionary.
239
+ """
240
+ if ignore_info:
241
+ info = {}
242
+ else:
243
+ info = {
244
+ k: v for k, v in row.items() if k not in PYRANGES_INTERVAL_COLUMNS
245
+ }
246
+
247
+ return cls(
248
+ chromosome=str(row['Chromosome']),
249
+ start=int(row['Start']),
250
+ end=int(row['End']),
251
+ strand=str(row.get('Strand', STRAND_UNSTRANDED)),
252
+ name=str(row.get('Name', '')),
253
+ info=info,
254
+ )
255
+
256
+ def to_pyranges_dict(self) -> dict[str, int | str]:
257
+ """Converts the interval to a pyranges-like dictionary."""
258
+ return {
259
+ 'Chromosome': self.chromosome,
260
+ 'Start': self.start,
261
+ 'End': self.end,
262
+ 'Name': self.name,
263
+ 'Strand': self.strand,
264
+ **self.info,
265
+ }
266
+
267
+ def swap_strand(self) -> Self:
268
+ """Swaps the strand of the interval."""
269
+ obj = self.copy()
270
+ if obj.strand == STRAND_POSITIVE:
271
+ obj.strand = STRAND_NEGATIVE
272
+ elif obj.strand == STRAND_NEGATIVE:
273
+ obj.strand = STRAND_POSITIVE
274
+ elif obj.strand == STRAND_UNSTRANDED:
275
+ raise ValueError('Cannot swap unstranded intervals.')
276
+ return obj
277
+
278
+ def as_unstranded(self) -> Self:
279
+ """Returns an unstranded copy of the interval."""
280
+ obj = self.copy()
281
+ obj.strand = STRAND_UNSTRANDED
282
+ return obj
283
+
284
+ def within_reference(self, reference_length: int = sys.maxsize) -> bool:
285
+ """Checks if the interval is within the valid reference range."""
286
+ return self.start >= 0 and self.end <= reference_length
287
+
288
+ def truncate(self, reference_length: int = sys.maxsize) -> Self:
289
+ """Truncates the interval to fit within the valid reference range."""
290
+ obj = self.copy()
291
+ if reference_length <= 0:
292
+ raise ValueError('Reference length should be larger than 0.')
293
+ if self.within_reference(reference_length):
294
+ return obj
295
+ else:
296
+ obj.start = max(self.start, 0)
297
+ obj.end = min(self.end, reference_length)
298
+ return obj
299
+
300
+ def center(self, use_strand: bool = True) -> int:
301
+ """Computes the center of the interval.
302
+
303
+ For intervals with an odd width, the center is rounded up for
304
+ positive/unstranded intervals and rounded down for negative strand
305
+ intervals.
306
+
307
+ If `use_strand` is True and the interval is on the negative strand, the
308
+ center is calculated differently to maintain consistency when stacking
309
+ sequences from different intervals oriented in the forward strand direction.
310
+ This ensures that the relative distance between the interval's upstream
311
+ boundary and its center is preserved.
312
+
313
+ Args:
314
+ use_strand: If True, the strand of the interval is considered when
315
+ calculating the center.
316
+
317
+ Returns:
318
+ The integer representing the center position of the interval.
319
+
320
+ Examples:
321
+ >>> Interval('1', 1, 3, '+').center()
322
+ 2
323
+ >>> Interval('1', 1, 3, '-').center() # Strand doesn't matter.
324
+ 2
325
+ >>> Interval('1', 1, 4, '+').center()
326
+ 3
327
+ >>> Interval('1', 1, 4, '-').center()
328
+ 2
329
+ >>> Interval('1', 1, 4, '-').center()
330
+ 2
331
+ >>> Interval('1', 1, 4, '+').center(use_strand=False)
332
+ 3
333
+ >>> Interval('1', 1, 2, '-').center()
334
+ 1
335
+ """
336
+ center = (self.start + self.end) // 2
337
+ if use_strand and self.negative_strand:
338
+ return center
339
+ else:
340
+ return center + self.width % 2
341
+
342
+ def shift(self, offset: int, use_strand: bool = True) -> Self:
343
+ """Shifts the interval by the given offset.
344
+
345
+ Args:
346
+ offset: The amount to shift the interval.
347
+ use_strand: If True, the shift direction is reversed for negative strand
348
+ intervals.
349
+
350
+ Returns:
351
+ A new shifted interval.
352
+ """
353
+ obj = self.copy()
354
+ if use_strand and self.negative_strand:
355
+ offset = -offset
356
+ obj.start = self.start + offset
357
+ obj.end = self.end + offset
358
+ return obj
359
+
360
+ def boundary_shift(
361
+ self, start_offset: int = 0, end_offset: int = 0, use_strand: bool = True
362
+ ) -> Self:
363
+ """Extends or shrinks the interval by adjusting the positions with padding.
364
+
365
+ Args:
366
+ start_offset: The amount to shift the start position.
367
+ end_offset: The amount to shift the end position.
368
+ use_strand: If True, the offsets are applied in reverse for negative
369
+ strand intervals.
370
+
371
+ Returns:
372
+ A new interval with adjusted boundaries.
373
+ """
374
+ return self.pad(-start_offset, end_offset, use_strand=use_strand)
375
+
376
+ def pad(
377
+ self, start_pad: int, end_pad: int, *, use_strand: bool = True
378
+ ) -> Self:
379
+ """Pads the interval by adding the specified padding to the start and end.
380
+
381
+ Args:
382
+ start_pad: The amount of padding to add to the start.
383
+ end_pad: The amount of padding to add to the end.
384
+ use_strand: If True, padding is applied in reverse for negative strand
385
+ intervals.
386
+
387
+ Returns:
388
+ A new padded interval.
389
+ """
390
+ obj = self.copy()
391
+ obj.pad_inplace(start_pad, end_pad, use_strand=use_strand)
392
+ return obj
393
+
394
+ def pad_inplace(
395
+ self, start_pad: int, end_pad: int, *, use_strand: bool = True
396
+ ):
397
+ """Pads the interval in place by adding padding to the start and end.
398
+
399
+ Args:
400
+ start_pad: The amount of padding to add to the start.
401
+ end_pad: The amount of padding to add to the end.
402
+ use_strand: If True, padding is applied in reverse for negative strand
403
+ intervals.
404
+ """
405
+ if use_strand and self.strand == '-':
406
+ start_pad, end_pad = end_pad, start_pad
407
+ self.start -= start_pad
408
+ self.end += end_pad
409
+ if self.width < 0:
410
+ raise ValueError('Resulting interval has negative length')
411
+
412
+ def resize(self, width: int, use_strand: bool = True) -> Self:
413
+ """Resizes the interval to a new width, centered around the original center.
414
+
415
+ Args:
416
+ width: The new width of the interval.
417
+ use_strand: If True, resizing considers the strand orientation.
418
+
419
+ Returns:
420
+ A new resized interval.
421
+ """
422
+ obj = self.copy()
423
+ obj.resize_inplace(width, use_strand)
424
+ return obj
425
+
426
+ def resize_inplace(self, width: int, use_strand: bool = True) -> None:
427
+ """Resizes the interval in place, centered around the original center.
428
+
429
+ Args:
430
+ width: The new width of the interval.
431
+ use_strand: If True, resizing considers the strand orientation.
432
+ """
433
+ if width < 0:
434
+ raise ValueError(f'Width needs to be > 0. Found: {width}.')
435
+
436
+ if width is None or self.width == width:
437
+ return
438
+
439
+ center = self.center()
440
+ if use_strand and self.negative_strand:
441
+ self.start = center - width // 2
442
+ self.end = center + (width + 1) // 2
443
+ else:
444
+ self.start = center - (width + 1) // 2
445
+ self.end = center + width // 2
446
+ assert self.width == width
447
+
448
+ def overlaps(self, interval: Self) -> bool:
449
+ """Checks if this interval overlaps with another interval."""
450
+ return (
451
+ self.chromosome == interval.chromosome
452
+ and self.start < interval.end
453
+ and interval.start < self.end
454
+ )
455
+
456
+ def contains(self, interval: Self) -> bool:
457
+ """Checks if this interval completely contains another interval."""
458
+ return (
459
+ self.chromosome == interval.chromosome
460
+ and self.start <= interval.start
461
+ and self.end >= interval.end
462
+ )
463
+
464
+ def intersect(self, interval: Self) -> Self | None:
465
+ """Returns the intersection of this interval with another interval."""
466
+ output = self.copy()
467
+ if not self.overlaps(interval):
468
+ return None
469
+ output.start = max(self.start, interval.start)
470
+ output.end = min(self.end, interval.end)
471
+ return output
472
+
473
+ def coverage(
474
+ self, intervals: Sequence[Self], *, bin_size: int = 1
475
+ ) -> np.ndarray:
476
+ """Computes coverage track from sequence of intervals overlapping interval.
477
+
478
+ This method calculates the coverage of this interval by a set of other
479
+ intervals. The coverage is defined as the number of intervals that overlap
480
+ each position within this interval.
481
+
482
+ The `bin_size` parameter allows you to bin the coverage into equal-sized
483
+ windows. This can be useful for summarizing coverage over larger regions.
484
+ If `bin_size` is 1, the coverage is calculated at single-base resolution.
485
+
486
+ Args:
487
+ intervals: A sequence of `Interval` objects that may overlap this
488
+ interval.
489
+ bin_size: The size of the bins used to calculate coverage. Must be a
490
+ positive integer that divides the width of the interval.
491
+
492
+ Returns:
493
+ A 1D numpy array representing the coverage track. The length of the array
494
+ is `self.width // bin_size`. Each element in the array represents the
495
+ summed coverage within the corresponding bin.
496
+
497
+ Raises:
498
+ ValueError: If `bin_size` is not a positive integer or if the interval
499
+ width is not divisible by `bin_size`.
500
+ """
501
+ if bin_size <= 0:
502
+ raise ValueError('bin_size needs to be larger or equal to 1.')
503
+ if self.width % bin_size != 0:
504
+ raise ValueError(
505
+ f'interval width {self.width} needs to be divisible '
506
+ f'by bin_size {bin_size}.'
507
+ )
508
+ output = np.zeros((self.width,), dtype=np.int32)
509
+ for interval in intervals:
510
+ if not self.overlaps(interval):
511
+ continue
512
+ relative_start = max(interval.start - self.start, 0)
513
+ relative_end = min(interval.end - self.start, self.width)
514
+ output[relative_start:relative_end] += 1
515
+ if bin_size > 1:
516
+ return output.reshape((self.width // bin_size, bin_size)).sum(axis=-1)
517
+ else:
518
+ return output
519
+
520
+ def overlap_ranges(
521
+ self,
522
+ intervals: Sequence[Self],
523
+ ) -> np.ndarray:
524
+ """Returns overlapping ranges from intervals overlapping this interval.
525
+
526
+ Args:
527
+ intervals: Sequence of candidate intervals to test for overlap.
528
+
529
+ Returns:
530
+ 2D numpy array indicating the start and end of the overlapping ranges.
531
+ """
532
+ output = []
533
+ for interval in intervals:
534
+ if not self.overlaps(interval):
535
+ continue
536
+ relative_start = max(interval.start - self.start, 0)
537
+ relative_end = min(interval.end - self.start, self.width)
538
+ output.append([relative_start, relative_end])
539
+
540
+ return (
541
+ np.asarray(output, dtype=np.int32)
542
+ if output
543
+ else np.empty((0, 2), dtype=np.int32)
544
+ )
545
+
546
+ def binary_mask(
547
+ self, intervals: Sequence[Self], bin_size: int = 1
548
+ ) -> np.ndarray:
549
+ """Boolean mask True if any interval overlaps the bin: coverage > 0."""
550
+ return self.coverage(intervals, bin_size=bin_size) > 0
551
+
552
+ def coverage_stranded(
553
+ self, intervals: Sequence[Self], *, bin_size: int = 1
554
+ ) -> np.ndarray:
555
+ """Computes a coverage track from intervals overlapping this interval.
556
+
557
+ This method considers the strand information of both self and intervals.
558
+
559
+ Args:
560
+ intervals: Sequence of intervals possibly overlapping self.
561
+ bin_size: Resolution at which to bin the output coverage track. Coverage
562
+ within each bin (if larger than 1) will be summarized using sum().
563
+
564
+ Returns:
565
+ Numpy array of shape (self.width // bin_size, 2) where output[:, 0]
566
+ represents coverage for intervals on the same strand as self and
567
+ output[:, 1] represents coverage of intervals on the opposite strand.
568
+ """
569
+ # Split intervals based on strand.
570
+ forward_intervals = []
571
+ reverse_intervals = []
572
+ for interval in intervals:
573
+ if interval.negative_strand:
574
+ reverse_intervals.append(interval)
575
+ else:
576
+ forward_intervals.append(interval)
577
+
578
+ coverage = np.stack(
579
+ [
580
+ self.coverage(forward_intervals, bin_size=bin_size),
581
+ self.coverage(reverse_intervals, bin_size=bin_size),
582
+ ],
583
+ axis=-1,
584
+ )
585
+ if self.negative_strand:
586
+ return coverage[::-1, ::-1]
587
+ else:
588
+ return coverage
589
+
590
+ def binary_mask_stranded(
591
+ self, intervals: Sequence[Self], bin_size: int = 1
592
+ ) -> np.ndarray:
593
+ """Boolean mask True if any interval overlaps the bin: coverage > 0."""
594
+ return self.coverage_stranded(intervals, bin_size=bin_size) > 0
595
+
596
+
597
+ _DEFAULT_REGEX = re.compile(r'(chr(?:X|Y|M|\d+)):(\d+):([ACGTN]*)>([ACGTN]*)')
598
+ _GTEX_REGEX = re.compile(
599
+ r'(chr(?:X|Y|M|\d+))_(\d+)_([ACGTN]*)_([ACGTN]*)_?[a-zA-Z0-9]*'
600
+ )
601
+ _OPEN_TARGETS_REGEX = re.compile(r'((?:X|Y|M|\d+))_(\d+)_([ACGTN]*)_([ACGTN]*)')
602
+ _OPEN_TARGETS_BIGQUERY_REGEX = re.compile(
603
+ r'((?:X|Y|M|\d+)):(\d+):([ACGTN]*):([ACGTN]*)'
604
+ )
605
+ _GNOMAD_REGEX = re.compile(r'((?:X|Y|M|\d+))-(\d+)-([ACGTN]*)-([ACGTN]*)')
606
+
607
+
608
+ class VariantFormat(enum.Enum):
609
+ """A format for parsing a string into a Variant object.
610
+
611
+ This is used to convert from a string to a formal Variant object.
612
+ Note that it does not perform any validation (e.g. it does not verify that the
613
+ reference allele corresponds to the reference genome or that the
614
+ position is valid).
615
+
616
+ Example formats:
617
+ DEFAULT: chr22:1024:A>C
618
+ GTEX: chr22_1024_A_C_b38 (build suffix is optional)
619
+ OPEN_TARGETS: 22_1024_A_C (chr prefix is omitted)
620
+ OPEN_TARGETS_BIGQUERY: 22:1024:A:C (chr prefix is omitted)
621
+ GNOMAD: 22-1024-A-C (chr prefix is omitted)
622
+ """
623
+
624
+ DEFAULT = 'default'
625
+ GTEX = 'gtex'
626
+ OPEN_TARGETS = 'open_targets'
627
+ OPEN_TARGETS_BIGQUERY = 'open_targets_bigquery'
628
+ GNOMAD = 'gnomad'
629
+
630
+ def to_regex(self) -> re.Pattern[str]:
631
+ """Returns a regular expression for the variant format."""
632
+ match self:
633
+ case VariantFormat.DEFAULT:
634
+ return _DEFAULT_REGEX
635
+ case VariantFormat.GTEX:
636
+ return _GTEX_REGEX
637
+ case VariantFormat.OPEN_TARGETS:
638
+ return _OPEN_TARGETS_REGEX
639
+ case VariantFormat.OPEN_TARGETS_BIGQUERY:
640
+ return _OPEN_TARGETS_BIGQUERY_REGEX
641
+ case VariantFormat.GNOMAD:
642
+ return _GNOMAD_REGEX
643
+
644
+
645
+ @dataclasses.dataclass
646
+ class Variant:
647
+ """Represents a genomic variant/mutation.
648
+
649
+ Differs from the Variant definition in a VCF file, which allows
650
+ for multiple alternative bases and contains sample information. This
651
+ `Variant` class does not include sample information or variant call
652
+ quality information.
653
+
654
+ Attributes:
655
+ chromosome: The chromosome name (e.g., 'chr1', '1').
656
+ position: The 1-based position of the variant on the chromosome.
657
+ reference_bases: The reference base(s) at the variant position. Most
658
+ frequently (not always!), these correspond to the sequence in the
659
+ reference genome at positions: [position, ..., position +
660
+ len(reference_bases) - 1]
661
+ alternate_bases: The alternate base(s) that replace the reference. For
662
+ example, if sequence='ACT', position=2, reference_bases='C',
663
+ alternate_bases='TG', then the actual (alternate) sequence would be ATGT.
664
+ name: An optional name for the variant (e.g., a dbSNP ID like rs206437).
665
+ info: An optional dictionary for additional variant information.
666
+ """
667
+
668
+ chromosome: str
669
+ position: int
670
+ reference_bases: str
671
+ alternate_bases: str
672
+ name: str = dataclasses.field(default='', compare=False, hash=False)
673
+ info: dict[str, Any] = dataclasses.field(
674
+ default_factory=dict, repr=False, compare=False, hash=False
675
+ )
676
+
677
+ def __post_init__(self):
678
+ """Validates the variant's position."""
679
+ if self.position < 1:
680
+ raise ValueError(f'Position has to be >=1. Found: {self.position}.')
681
+ if not set(self.reference_bases).issubset(VALID_VARIANT_BASES):
682
+ raise ValueError(
683
+ f'Invalid reference bases: "{self.reference_bases}". Must only'
684
+ ' contain "ACGTN".'
685
+ )
686
+ if not set(self.alternate_bases).issubset(VALID_VARIANT_BASES):
687
+ raise ValueError(
688
+ f'Invalid alternate bases: "{self.alternate_bases}". Must only'
689
+ ' contain "ACGTN".'
690
+ )
691
+
692
+ def __str__(self):
693
+ """Returns a string representation of the variant."""
694
+ ref_alt = f'{self.reference_bases}>{self.alternate_bases}'
695
+ return f'{self.chromosome}:{self.position}:{ref_alt}'
696
+
697
+ def as_truncated_str(self, max_length: int = 50):
698
+ """Truncates the variant str's ref and alt bases to the given max length."""
699
+
700
+ def _truncate(s: str):
701
+ if len(s) <= max_length:
702
+ return s
703
+ else:
704
+ return s[: max_length // 2] + '...' + s[-max_length // 2 :]
705
+
706
+ return (
707
+ f'{self.chromosome}:{self.position}:'
708
+ f'{_truncate(self.reference_bases)}>{_truncate(self.alternate_bases)}'
709
+ )
710
+
711
+ @property
712
+ def start(self) -> int:
713
+ """Returns the 0-based start position of the variant."""
714
+ return self.position - 1
715
+
716
+ @property
717
+ def end(self) -> int:
718
+ """Returns the 0-based end position of the variant."""
719
+ return self.start + len(self.reference_bases)
720
+
721
+ @property
722
+ def reference_interval(self) -> Interval:
723
+ """Returns an `Interval` for the variant's reference sequence."""
724
+ return Interval(self.chromosome, self.start, self.end)
725
+
726
+ def reference_overlaps(self, interval: Interval) -> bool:
727
+ """Checks if the variant's reference overlaps with the interval."""
728
+ return interval.overlaps(Interval(self.chromosome, self.start, self.end))
729
+
730
+ def alternate_overlaps(self, interval: Interval) -> bool:
731
+ """Checks if the variant's alternate overlaps with the interval."""
732
+ return interval.overlaps(
733
+ Interval(
734
+ self.chromosome, self.start, self.start + len(self.alternate_bases)
735
+ )
736
+ )
737
+
738
+ @property
739
+ def is_snv(self) -> bool:
740
+ """Return if the variant is a Single Nucleotide Variant (SNV)."""
741
+ return len(self.reference_bases) == 1 and len(self.alternate_bases) == 1
742
+
743
+ @property
744
+ def is_deletion(self) -> bool:
745
+ """Return if the variant is a deletion."""
746
+ return len(self.reference_bases) > len(self.alternate_bases)
747
+
748
+ @property
749
+ def is_insertion(self) -> bool:
750
+ """Return if the variant is an insertion."""
751
+ return len(self.reference_bases) < len(self.alternate_bases)
752
+
753
+ @property
754
+ def is_frameshift(self) -> bool:
755
+ """Return if the variant is a frameshift."""
756
+ indel_size = abs(len(self.reference_bases) - len(self.alternate_bases))
757
+ return indel_size > 0 and indel_size % 3 != 0
758
+
759
+ @property
760
+ def is_indel(self) -> bool:
761
+ """Return if the variant is an insertion or deletion."""
762
+ return self.is_insertion or self.is_deletion
763
+
764
+ @property
765
+ def is_structural(self) -> bool:
766
+ """Return if the variant is a structural variant."""
767
+ indel_size = abs(len(self.reference_bases) - len(self.alternate_bases))
768
+ return indel_size >= 50
769
+
770
+ def copy(self) -> Self:
771
+ """Returns a deep copy of the variant."""
772
+ return copy.deepcopy(self)
773
+
774
+ @classmethod
775
+ def from_str(
776
+ cls, string: str, variant_format: VariantFormat = VariantFormat.DEFAULT
777
+ ) -> Self:
778
+ """Creates a `Variant` from a string representation.
779
+
780
+ Args:
781
+ string: The string representation.
782
+ variant_format: The format of the variant string. By default, this uses
783
+ "chromosome:position:ref>alt" (for example, "chr1:1024:A>C"). See
784
+ VariantFormat for alternate formatting options.
785
+
786
+ Returns:
787
+ A `Variant` object.
788
+ """
789
+ result = re.fullmatch(variant_format.to_regex(), string)
790
+ if result is None:
791
+ raise ValueError(f'Invalid format for variant string: {string}')
792
+
793
+ chromosome, position, reference, alternate = result.groups()
794
+ # Add chr prefix if not already present.
795
+ if not chromosome.startswith('chr'):
796
+ chromosome = f'chr{chromosome}'
797
+ return cls(
798
+ chromosome=chromosome,
799
+ position=int(position),
800
+ reference_bases=reference,
801
+ alternate_bases=alternate,
802
+ )
803
+
804
+ def to_dict(self) -> dict[str, Any]:
805
+ """Converts the variant to a dictionary."""
806
+ return dataclasses.asdict(self)
807
+
808
+ @classmethod
809
+ def from_dict(cls, dictionary: Mapping[str, Any] | Self) -> Self:
810
+ """Creates a `Variant` from a dictionary."""
811
+ return cls(**dictionary) # pytype: disable=bad-return-type
812
+
813
+ def to_proto(self) -> dna_model_pb2.Variant:
814
+ """Converts the variant to a protobuf message."""
815
+ return dna_model_pb2.Variant(
816
+ chromosome=self.chromosome,
817
+ position=self.position,
818
+ reference_bases=self.reference_bases,
819
+ alternate_bases=self.alternate_bases,
820
+ )
821
+
822
+ @classmethod
823
+ def from_proto(cls, variant: dna_model_pb2.Variant) -> Self:
824
+ """Creates a `Variant` from a protobuf message."""
825
+ return cls(
826
+ chromosome=variant.chromosome,
827
+ position=variant.position,
828
+ reference_bases=variant.reference_bases,
829
+ alternate_bases=variant.alternate_bases,
830
+ )
831
+
832
+ def split(self, anchor: int) -> tuple[Self | None, Self | None]:
833
+ """Splits the variant into two at the anchor point.
834
+
835
+ If the anchor point falls within the variant's reference sequence, the
836
+ variant is split into two new variants: one upstream of the anchor and one
837
+ downstream. If the anchor is outside the variant's reference sequence,
838
+ the original variant is returned on the appropriate side, and None on the
839
+ other.
840
+
841
+ Example:
842
+ position= 3
843
+ ref: ...[ A C ]...
844
+ alt: .....T G T C
845
+ anchor=3 |
846
+ returns: (chr1:3:A>T, chr1:4:C>GTC)
847
+
848
+ Args:
849
+ anchor: The 0-based anchor point to split the variant.
850
+
851
+ Returns:
852
+ A tuple of the upstream and downstream variants. If the variant is only
853
+ on one side of the anchorpoint, then None is returned.
854
+ """
855
+ if anchor <= self.start:
856
+ return None, self.copy()
857
+ elif anchor >= self.end:
858
+ return self.copy(), None
859
+ else:
860
+ mid = anchor - self.start
861
+ upstream, downstream = self.copy(), self.copy()
862
+ upstream.reference_bases = self.reference_bases[:mid]
863
+ upstream.alternate_bases = self.alternate_bases[:mid]
864
+
865
+ downstream.position = anchor + 1
866
+ downstream.reference_bases = self.reference_bases[mid:]
867
+ downstream.alternate_bases = self.alternate_bases[mid:]
868
+ return upstream, downstream
869
+
870
+
871
+ @dataclasses.dataclass
872
+ class Junction(Interval):
873
+ """Represents a splice junction.
874
+
875
+ A splice junction is a point in a pre-mRNA transcript where an intron is
876
+ removed and exons are joined during RNA splicing. This class inherits from
877
+ `Interval` and adds properties and methods specific to splice junctions.
878
+
879
+ Attributes:
880
+ chromosome: The chromosome name.
881
+ start: The 0-based start position of the junction.
882
+ end: The 0-based end position of the junction.
883
+ strand: The strand of the junction ('+' or '-').
884
+ name: An optional name for the junction.
885
+ info: An optional dictionary to store additional information.
886
+ k: An optional integer representing the number of reads supporting the
887
+ splice junction.
888
+
889
+ Raises:
890
+ ValueError: If the strand is unstranded.
891
+ """
892
+
893
+ k: int | None = None
894
+
895
+ def __post_init__(self):
896
+ """Validates that the junction is stranded."""
897
+ super().__post_init__()
898
+ if self.strand == STRAND_UNSTRANDED:
899
+ raise ValueError('Junctions must be stranded.')
900
+
901
+ @property
902
+ def acceptor(self) -> int:
903
+ """Returns the acceptor site position."""
904
+ return self.start if self.strand == STRAND_NEGATIVE else self.end
905
+
906
+ @property
907
+ def donor(self) -> int:
908
+ """Returns the donor site position."""
909
+ return self.end if self.strand == STRAND_NEGATIVE else self.start
910
+
911
+ def dinucleotide_region(self) -> tuple[Interval, Interval]:
912
+ """Returns the dinucleotide regions around acceptor and donor sites."""
913
+ return (
914
+ Interval(
915
+ self.chromosome, self.start, self.start + 2, strand=self.strand
916
+ ),
917
+ Interval(self.chromosome, self.end - 2, self.end, strand=self.strand),
918
+ )
919
+
920
+ def acceptor_region(self, overhang: tuple[int, int] = (250, 250)) -> Interval:
921
+ """Returns the region around the acceptor site with overhang."""
922
+ return Interval(
923
+ self.chromosome, self.acceptor, self.acceptor, strand=self.strand
924
+ ).pad(start_pad=overhang[0], end_pad=overhang[1])
925
+
926
+ def donor_region(self, overhang: tuple[int, int] = (250, 250)) -> Interval:
927
+ """Returns the region around the donor site with overhang."""
928
+ return Interval(
929
+ self.chromosome, self.donor, self.donor, strand=self.strand
930
+ ).pad(start_pad=overhang[0], end_pad=overhang[1])
931
+
932
+
933
+ class _FastaExtractorType(Protocol):
934
+ """Protocol definition for extracting intervals from a Fasta file."""
935
+
936
+ def extract(self, interval: Interval) -> str:
937
+ """Extract and return the DNA sequence for a given interval."""
938
+
939
+
940
+ def _prefix_length(*sequences) -> int:
941
+ """Returns the length of the common prefix for a sequence of strings."""
942
+ i = 0
943
+ for chars in zip(*sequences, strict=False):
944
+ if all(c == chars[0] for c in chars):
945
+ i += 1
946
+ else:
947
+ break
948
+ return i
949
+
950
+
951
+ def normalize_variant(
952
+ variant: Variant, extractor: _FastaExtractorType
953
+ ) -> Variant:
954
+ """Normalize a Variant by left-aligning the reference and alternate bases.
955
+
956
+ Normalization applied following algorithm described in
957
+ https://doi.org/10.1093/bioinformatics/btv112.
958
+
959
+ Args:
960
+ variant: The Variant to normalize.
961
+ extractor: The FastaExtractor to use for extracting the reference sequence.
962
+
963
+ Returns:
964
+ The normalized Variant.
965
+ """
966
+ if variant.is_snv:
967
+ return variant
968
+
969
+ chromosome = variant.chromosome
970
+ genome_sequence = extractor.extract(
971
+ Interval(
972
+ chromosome,
973
+ variant.start,
974
+ variant.start
975
+ + max(len(variant.reference_bases), len(variant.alternate_bases)),
976
+ )
977
+ )
978
+
979
+ position = variant.position
980
+ alleles = [genome_sequence, variant.reference_bases, variant.alternate_bases]
981
+
982
+ # Remove any common suffix from the alleles.
983
+ finished = False
984
+ while not finished:
985
+ suffix_length = _prefix_length(*[reversed(a) for a in alleles])
986
+ if suffix_length > 0:
987
+ alleles = [a[:-suffix_length] for a in alleles]
988
+
989
+ # If any alleles are empty, extend all alleles by 1 nucleotide to the left.
990
+ if not all(alleles):
991
+ position -= 1
992
+ base = extractor.extract(Interval(chromosome, position - 1, position))[0]
993
+ alleles = [base + a for a in alleles]
994
+ else:
995
+ # Finished iff we haven't shifted alleles and don't have a common suffix.
996
+ finished = suffix_length == 0
997
+
998
+ # Left-align the variant to the genome, ensuring at least 1 bp of context.
999
+ max_prefix_length = len(min(alleles)) - 1
1000
+ prefix_length = _prefix_length(*alleles)
1001
+ i = min(max_prefix_length, prefix_length)
1002
+
1003
+ _, reference_bases, alternate_bases = alleles
1004
+ return Variant(
1005
+ chromosome=chromosome,
1006
+ position=position + i,
1007
+ reference_bases=reference_bases[i:],
1008
+ alternate_bases=alternate_bases[i:],
1009
+ )
1010
+
1011
+
1012
+ def _split_intervals(
1013
+ intervals: Iterable[Interval], marker: int, bounds: list[tuple[int, int]]
1014
+ ):
1015
+ """Splits intervals into start and end points with markers."""
1016
+ for i in intervals:
1017
+ bounds.append((i.start, +marker))
1018
+ bounds.append((i.end, -marker))
1019
+
1020
+
1021
+ def _group_by_chromosome(
1022
+ intervals: Iterable[Interval],
1023
+ ) -> dict[str, list[Interval]]:
1024
+ """Groups intervals by chromosome."""
1025
+ interval_map = collections.defaultdict(list)
1026
+ for i in intervals:
1027
+ interval_map[i.chromosome].append(i)
1028
+ return dict(interval_map)
1029
+
1030
+
1031
+ def intersect_intervals(
1032
+ lhs: Iterable[Interval],
1033
+ rhs: Iterable[Interval],
1034
+ *,
1035
+ result_strand: str = '.',
1036
+ ) -> Iterator[Interval]:
1037
+ """Generates the intersection of two interval sets.
1038
+
1039
+ Point ranges (width == 0) are considered to intersect any range which contains
1040
+ them.
1041
+
1042
+ In these examples, the intersection is a point range (2,2)
1043
+ ```
1044
+ 1 2 3 4
1045
+ ..|..|..|..|..
1046
+ <> (start=2, end=2)
1047
+ -------> (..., end=2)
1048
+ ```
1049
+
1050
+ ```
1051
+ 1 2 3 4
1052
+ ..|..|..|..|..
1053
+ <> (start=2, end=2)
1054
+ <------- (start=2, end=...)
1055
+ ```
1056
+
1057
+ For consistency, this means that the following results in a point
1058
+ intersection:
1059
+
1060
+ ```
1061
+ 1 2 3 4
1062
+ ..|..|..|..|..
1063
+ -------> (start=..., end=2)
1064
+ <------- (start=2, end=...)
1065
+ ```
1066
+
1067
+ Args:
1068
+ lhs: A set of intervals.
1069
+ rhs: A set of intervals.
1070
+ result_strand: The strand for the resulting intervals.
1071
+
1072
+ Yields:
1073
+ The intersection of intervals. Overlapping intervals within either `lhs`
1074
+ or `rhs` are implicitly unioned.
1075
+ """
1076
+
1077
+ def _intersect(lhs, rhs, chrom):
1078
+ """Calculates the intersection for a specific chromosome.
1079
+
1080
+ Deconstructs two sets of intervals into (start, +k) and (end, -k) positions,
1081
+ where k is different for each set, then sorts all interval endpoints. By
1082
+ walking the sorted endpoints and accumulating the k's, we can work out when
1083
+ we enter or exit an interval from either set.
1084
+
1085
+ This allows emitting intervals corresponding to the intersection, and with
1086
+ appropriate k's, detecting when we enter an interval in either set before
1087
+ exiting the previous.
1088
+
1089
+ Args:
1090
+ lhs: The first list of `Interval` objects.
1091
+ rhs: The second list of `Interval` objects.
1092
+ chrom: The chromosome name.
1093
+
1094
+ Yields:
1095
+ `Interval` objects representing the intersections.
1096
+ """
1097
+ bounds = []
1098
+ _split_intervals(lhs, 0x00001, bounds)
1099
+ _split_intervals(rhs, 0x10000, bounds)
1100
+ accum = 0
1101
+ start = None
1102
+ for pos, delta in sorted(bounds, key=lambda x: (x[0], -x[1])):
1103
+ old_accum = accum
1104
+ accum += delta
1105
+ if accum & 0xFFFF and accum & 0xFFFF0000:
1106
+ if start is None:
1107
+ start = pos
1108
+ elif old_accum & 0xFFFF and old_accum & 0xFFFF0000:
1109
+ yield Interval(chrom, start, pos, result_strand)
1110
+ start = None
1111
+
1112
+ lhs = _group_by_chromosome(lhs)
1113
+ rhs = _group_by_chromosome(rhs)
1114
+ for chromosome in set(lhs) & set(rhs):
1115
+ yield from _intersect(lhs[chromosome], rhs[chromosome], chromosome)
1116
+
1117
+
1118
+ def union_intervals(
1119
+ lhs: Iterable[Interval],
1120
+ rhs: Iterable[Interval],
1121
+ *,
1122
+ result_strand: str = '.',
1123
+ ) -> Iterator[Interval]:
1124
+ """Generates the union of two interval sets.
1125
+
1126
+ Args:
1127
+ lhs: A non-overlapping set of intervals.
1128
+ rhs: A non-overlapping set of intervals.
1129
+ result_strand: The strand for the resulting intervals.
1130
+
1131
+ Yields:
1132
+ The union of intervals. Any position covered by an interval in `lhs` or
1133
+ `rhs` is covered in the result.
1134
+ """
1135
+
1136
+ def _union(lhs, rhs, chrom):
1137
+ """Calculates the union for a specific chromosome, analog of _intersect."""
1138
+ bounds = []
1139
+ _split_intervals(lhs, 0x00001, bounds)
1140
+ _split_intervals(rhs, 0x10000, bounds)
1141
+ accum = 0
1142
+ start = end = None
1143
+ for pos, delta in sorted(bounds, key=lambda x: (x[0], -x[1])):
1144
+ if accum == 0 and end is not None and end < pos:
1145
+ # Delay generating an interval until after the observed end point.
1146
+ # This merges abutting ranges.
1147
+ yield Interval(chrom, start, end, result_strand)
1148
+ start = end = None
1149
+ accum += delta
1150
+ if accum:
1151
+ if start is None:
1152
+ start = pos
1153
+ else:
1154
+ assert start is not None
1155
+ end = pos
1156
+ if end is not None:
1157
+ yield Interval(chrom, start, end, result_strand)
1158
+
1159
+ lhs = _group_by_chromosome(lhs)
1160
+ rhs = _group_by_chromosome(rhs)
1161
+ for chromosome in set(lhs) | set(rhs):
1162
+ yield from _union(
1163
+ lhs.get(chromosome, []), rhs.get(chromosome, []), chromosome
1164
+ )
1165
+
1166
+
1167
+ def merge_overlapping_intervals(
1168
+ intervals: Sequence[Interval],
1169
+ ) -> list[Interval]:
1170
+ """Merges overlapping intervals and returns a sorted list.
1171
+
1172
+ Args:
1173
+ intervals: A sequence of intervals with the same strand.
1174
+
1175
+ Returns:
1176
+ A new sorted list of merged intervals.
1177
+ """
1178
+ if not intervals:
1179
+ return []
1180
+ assert all(i.strand == intervals[0].strand for i in intervals)
1181
+ return list(union_intervals(intervals, [], result_strand=intervals[0].strand))
flax_model/alphagenome/_sdk/data/junction_data.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Splice junction data container.
16
+
17
+ `JunctionData` stores splice junction data for a given transcript or interval.
18
+ """
19
+
20
+ from collections.abc import Sequence
21
+ import dataclasses
22
+ from typing import Any
23
+
24
+ from flax_model.alphagenome._sdk import typing
25
+ from flax_model.alphagenome._sdk.data import genome
26
+ from flax_model.alphagenome._sdk.data import ontology
27
+ from jaxtyping import Float, Shaped # pylint: disable=g-multiple-import, g-importing-member
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ JunctionMetadata = pd.DataFrame
32
+
33
+
34
+ @typing.jaxtyped
35
+ @dataclasses.dataclass(frozen=True)
36
+ class JunctionData:
37
+ """Container for storing splice junction data.
38
+
39
+ Attributes:
40
+ junctions: A numpy array representing the splice junctions.
41
+ values: A numpy array of floats representing the values associated with each
42
+ junction for each track.
43
+ metadata: A pandas DataFrame containing metadata for each track.
44
+ interval: An optional `Interval` object representing the genomic region
45
+ containing the junctions.
46
+ uns: An optional dictionary to store additional unstructured data.
47
+
48
+ Raises:
49
+ ValueError: If the number of tracks in `values` does not match the number
50
+ of rows in `metadata`, or if `metadata` contains duplicate names.
51
+ """
52
+
53
+ junctions: Shaped[np.ndarray, 'num_junctions']
54
+ values: Float[np.ndarray, 'num_junctions num_tracks']
55
+ metadata: JunctionMetadata
56
+ interval: genome.Interval | None = None
57
+ uns: dict[str, Any] | None = None
58
+
59
+ def __post_init__(self):
60
+ """Validates the consistency of the data."""
61
+ if self.values.shape[1] != len(self.metadata):
62
+ raise ValueError(
63
+ f'Number of tracks {self.values.shape[1]} and '
64
+ f'metadata {len(self.metadata)} do not match.'
65
+ )
66
+
67
+ if self.metadata['name'].duplicated().any():
68
+ raise ValueError('Metadata contain duplicated names.')
69
+
70
+ def __len__(self):
71
+ """Returns the number of junctions."""
72
+ return len(self.junctions)
73
+
74
+ @property
75
+ def num_tracks(self) -> int:
76
+ """Returns the number of tracks."""
77
+ return len(self.metadata)
78
+
79
+ @property
80
+ def names(self) -> np.ndarray:
81
+ """Returns an array of track names (not necessarily unique)."""
82
+ return self.metadata['name'].values
83
+
84
+ @property
85
+ def strands(self) -> np.ndarray:
86
+ """Returns an array of junction strands."""
87
+ return np.array([j.strand for j in self.junctions])
88
+
89
+ @property
90
+ def possible_strands(self) -> np.ndarray:
91
+ """All possible strands."""
92
+ return np.unique(self.strands)
93
+
94
+ @property
95
+ def ontology_terms(self) -> Sequence[ontology.OntologyTerm | None] | None:
96
+ """Returns a list of ontology terms (if available)."""
97
+ if 'ontology_curie' in self.metadata.columns:
98
+ return [
99
+ ontology.from_curie(curie) if curie is not None else None
100
+ for curie in self.metadata['ontology_curie'].values
101
+ ]
102
+ else:
103
+ return None
104
+
105
+ # Track order / subset wrangling:
106
+ def filter_tracks(self, mask: np.ndarray | list[bool]) -> 'JunctionData':
107
+ """Filters tracks by a boolean mask.
108
+
109
+ Args:
110
+ mask: A boolean mask to select tracks.
111
+
112
+ Returns:
113
+ A new `JunctionData` object with the filtered tracks.
114
+ """
115
+ return JunctionData(
116
+ junctions=self.junctions,
117
+ values=self.values[:, mask],
118
+ metadata=self.metadata.loc[mask],
119
+ interval=self.interval,
120
+ uns=self.uns,
121
+ )
122
+
123
+ def filter_to_strand(self, strand: str) -> 'JunctionData':
124
+ """Filters junctions to a specific DNA strand.
125
+
126
+ Args:
127
+ strand: The strand to filter by ('+' or '-').
128
+
129
+ Returns:
130
+ A new `JunctionData` object with junctions on the specified strand.
131
+ """
132
+ mask = self.strands == strand
133
+ return JunctionData(
134
+ junctions=self.junctions[mask],
135
+ values=self.values[mask, :],
136
+ metadata=self.metadata,
137
+ interval=self.interval,
138
+ uns=self.uns,
139
+ )
140
+
141
+ def normalize_values(self, total_k: float = 10.0) -> 'JunctionData':
142
+ """Normalizes the values by the k value."""
143
+ values = self.values * total_k / self.values.sum()
144
+ return JunctionData(
145
+ junctions=self.junctions,
146
+ values=values,
147
+ metadata=self.metadata,
148
+ interval=self.interval,
149
+ uns=self.uns,
150
+ )
151
+
152
+ def filter_to_positive_strand(self) -> 'JunctionData':
153
+ """Filters junctions to the positive DNA strand."""
154
+ return self.filter_to_strand(genome.STRAND_POSITIVE)
155
+
156
+ def filter_to_negative_strand(self) -> 'JunctionData':
157
+ """Filters junctions to the negative DNA strand."""
158
+ return self.filter_to_strand(genome.STRAND_NEGATIVE)
159
+
160
+ def filter_by_tissue(self, tissue: str) -> 'JunctionData':
161
+ """Filters tracks by GTEx tissue type.
162
+
163
+ Args:
164
+ tissue: The GTEx tissue type to filter by.
165
+
166
+ Returns:
167
+ A new `JunctionData` object with tracks from the specified tissue.
168
+
169
+ Raises:
170
+ ValueError: If the metadata does not contain a 'gtex_tissue' column.
171
+ """
172
+ if 'gtex_tissue' not in self.metadata.columns:
173
+ raise ValueError(
174
+ 'Metadata does not contain gtex_tissue column. '
175
+ f'Got {set(self.metadata.columns)}.'
176
+ )
177
+ return self.filter_tracks(self.metadata['gtex_tissue'] == tissue)
178
+
179
+ def filter_by_name(self, name: str) -> 'JunctionData':
180
+ """Filters tracks by name."""
181
+ return self.filter_tracks(self.metadata['name'] == name)
182
+
183
+ def filter_by_ontology(self, ontology_curie: str) -> 'JunctionData':
184
+ """Filters tracks by ontology term.
185
+
186
+ Args:
187
+ ontology_curie: The ontology term CURIE to filter by.
188
+
189
+ Returns:
190
+ A new `JunctionData` object with tracks associated with the specified
191
+ ontology term.
192
+
193
+ Raises:
194
+ ValueError: If the metadata does not contain an 'ontology_curie' column.
195
+ """
196
+ if 'ontology_curie' not in self.metadata.columns:
197
+ raise ValueError(
198
+ 'Metadata does not contain ontology_curie column. '
199
+ f'Got {set(self.metadata.columns)}.'
200
+ )
201
+ return self.filter_tracks(self.metadata['ontology_curie'] == ontology_curie)
202
+
203
+ def intersect_with_interval(
204
+ self, interval: genome.Interval
205
+ ) -> 'JunctionData':
206
+ """Returns the intersection of the junctions and the interval."""
207
+ mask = np.array([j.overlaps(interval) for j in self.junctions])
208
+ return JunctionData(
209
+ junctions=self.junctions[mask],
210
+ values=self.values[mask, :],
211
+ metadata=self.metadata,
212
+ interval=self.interval,
213
+ uns=self.uns,
214
+ )
215
+
216
+
217
+ def get_junctions_to_plot(
218
+ *,
219
+ predictions: JunctionData,
220
+ name: str,
221
+ strand: str,
222
+ k_threshold: float | None = 0.0,
223
+ ) -> list[genome.Junction]:
224
+ """Gets a list of junctions to plot.
225
+
226
+ Filters the junctions in the `predictions` by name and strand, and
227
+ applies a threshold on the `k` value (read count).
228
+
229
+ Args:
230
+ predictions: A `JunctionData` object containing junction predictions.
231
+ name: The name to filter by.
232
+ strand: The strand to filter by ('+' or '-').
233
+ k_threshold: The minimum `k` value for a junction to be included. If None,
234
+ use 5% of the maximum value.
235
+
236
+ Returns:
237
+ A list of `Junction` objects to plot.
238
+
239
+ Raises:
240
+ ValueError: If more than one track is found for the specified name.
241
+ """
242
+ filtered = predictions.filter_by_name(name)
243
+ if filtered.num_tracks > 1:
244
+ raise ValueError(
245
+ f'Expected only one ontology term, got {filtered.num_tracks}.'
246
+ )
247
+ filtered_junctions = []
248
+ if filtered.values.size == 0:
249
+ return filtered_junctions
250
+
251
+ if k_threshold is None:
252
+ k_threshold = filtered.values.max() * 0.05
253
+
254
+ # Round k for better visualization.
255
+ for interval, k in zip(filtered.junctions, filtered.values, strict=True):
256
+ # Filter by threshold and strand.
257
+ if interval.strand != strand:
258
+ continue
259
+ k = k.item()
260
+ if k >= k_threshold:
261
+ k = round(k, 2)
262
+
263
+ filtered_junctions.append(
264
+ genome.Junction(
265
+ interval.chromosome,
266
+ interval.start,
267
+ interval.end,
268
+ interval.strand,
269
+ k=k,
270
+ )
271
+ )
272
+ return filtered_junctions
flax_model/alphagenome/_sdk/data/ontology.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Handling of biological ontologies."""
16
+
17
+ from collections.abc import Sequence
18
+ import dataclasses
19
+ import enum
20
+
21
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
22
+ import immutabledict
23
+
24
+
25
+ class OntologyType(enum.IntEnum):
26
+ """Supported ontology types.
27
+
28
+ CLO: Cell Line Ontology.
29
+ UBERON: Uber-anatomy ontology.
30
+ CL: Cell Ontology.
31
+ EFO: Experimental Factor Ontology.
32
+ NTR: New Term Requested.
33
+ """
34
+
35
+ CLO = 1
36
+ UBERON = 2
37
+ CL = 3
38
+ EFO = 4
39
+ NTR = 5
40
+
41
+
42
+ _ONTOLOGY_TYPE_TO_PROTO_ENUM = immutabledict.immutabledict({
43
+ OntologyType.CLO: dna_model_pb2.OntologyType.ONTOLOGY_TYPE_CLO,
44
+ OntologyType.UBERON: dna_model_pb2.OntologyType.ONTOLOGY_TYPE_UBERON,
45
+ OntologyType.CL: dna_model_pb2.OntologyType.ONTOLOGY_TYPE_CL,
46
+ OntologyType.EFO: dna_model_pb2.OntologyType.ONTOLOGY_TYPE_EFO,
47
+ OntologyType.NTR: dna_model_pb2.OntologyType.ONTOLOGY_TYPE_NTR,
48
+ })
49
+
50
+
51
+ @dataclasses.dataclass(frozen=True)
52
+ class OntologyTerm:
53
+ """A single biological ontology term.
54
+
55
+ Attributes:
56
+ type: The ontology type.
57
+ id: The ID of the term within the ontology.
58
+ """
59
+
60
+ type: OntologyType
61
+ id: int
62
+
63
+ @property
64
+ def ontology_curie(self) -> str:
65
+ """Returns the CURIE (Compact Uniform Resource Identifier) for the term."""
66
+ return f'{self.type.name}:{self.id:07d}'
67
+
68
+ def to_proto(self) -> dna_model_pb2.OntologyTerm:
69
+ """Converts the ontology term to a protobuf message."""
70
+ return dna_model_pb2.OntologyTerm(
71
+ ontology_type=_ONTOLOGY_TYPE_TO_PROTO_ENUM[self.type], id=self.id
72
+ )
73
+
74
+
75
+ def from_curie(ontology_curie: str) -> OntologyTerm:
76
+ """Creates an `OntologyTerm` from a CURIE.
77
+
78
+ Args:
79
+ ontology_curie: The CURIE (Compact Uniform Resource Identifier) for the
80
+ ontology term.
81
+
82
+ Returns:
83
+ An `OntologyTerm` object.
84
+ """
85
+ try:
86
+ ontology_type, local_id = ontology_curie.split(':')
87
+ except ValueError as e:
88
+ raise ValueError(
89
+ f'Invalid {ontology_curie=}. CURIEs must be of the form <type>:<id>.'
90
+ ) from e
91
+ try:
92
+ ontology_type = OntologyType[ontology_type]
93
+ except KeyError as e:
94
+ raise ValueError(f'Cannot parse {ontology_type=}') from e
95
+ return OntologyTerm(ontology_type, int(local_id))
96
+
97
+
98
+ def from_curies(ontology_curies: Sequence[str]) -> Sequence[OntologyTerm]:
99
+ """Creates a list of `OntologyTerm` objects from a list of CURIEs.
100
+
101
+ Args:
102
+ ontology_curies: A list of CURIEs (Compact Uniform Resource Identifiers).
103
+
104
+ Returns:
105
+ A list of `OntologyTerm` objects.
106
+ """
107
+ return [from_curie(curie) for curie in ontology_curies]
108
+
109
+
110
+ def from_proto(proto: dna_model_pb2.OntologyTerm) -> OntologyTerm:
111
+ """Creates an `OntologyTerm` from a protobuf message.
112
+
113
+ Args:
114
+ proto: An `OntologyTerm` protobuf message.
115
+
116
+ Returns:
117
+ An `OntologyTerm` object.
118
+ """
119
+ return OntologyTerm(
120
+ OntologyType(proto.ontology_type),
121
+ proto.id,
122
+ )
flax_model/alphagenome/_sdk/data/track_data.py ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """Track data container analogous to AnnData."""
17
+
18
+ from collections.abc import Sequence
19
+ import copy
20
+ import dataclasses
21
+ import enum
22
+ from typing import Any, Union
23
+
24
+ from flax_model.alphagenome._sdk import typing
25
+ from flax_model.alphagenome._sdk.data import genome
26
+ from flax_model.alphagenome._sdk.data import ontology
27
+ from jaxtyping import Bool, Float32, Int32 # pylint: disable=g-multiple-import, g-importing-member
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ # Required columns: name, strand.
32
+ # Optional standardized columns: cell_type, assay, padding.
33
+ TrackMetadata = pd.DataFrame
34
+ PositionalIndex = slice | genome.Interval | int
35
+ TrackIndex = np.ndarray | Sequence[int] | Sequence[str] | slice | int | str
36
+ Index = PositionalIndex | tuple[PositionalIndex, TrackIndex] | TrackIndex
37
+
38
+
39
+ @enum.unique
40
+ class AggregationType(enum.Enum):
41
+ """Aggregation types for downsampling/upsampling track resolutions.
42
+
43
+ SUM: Sum pooling, where values within a bin are summed. This is recommended
44
+ for continuous tracks where the total value within a bin is meaningful (e.g.
45
+ read counts, coverage).
46
+ MAX: Max pooling, where the maximum value within a bin is selected. This is
47
+ recommended for binary tracks (e.g., gene masks, regions of interest).
48
+ """
49
+
50
+ SUM = 'sum'
51
+ MAX = 'max'
52
+
53
+
54
+ @typing.jaxtyped
55
+ @dataclasses.dataclass(frozen=True)
56
+ class TrackData:
57
+ """Container for storing track values and metadata.
58
+
59
+ `TrackData` stores multiple genomic tracks at the same resolution, stacked
60
+ into an ND matrix of shape (positional_bins, num_tracks). It also contains
61
+ metadata information as a pandas DataFrame with `num_tracks` rows.
62
+
63
+ Metadata DataFrame has two main required columns:
64
+
65
+ * name: The name of the track.
66
+ * strand: The strand of the track ('+', '-', or '.').
67
+
68
+ Other columns are optional.
69
+
70
+ Valid shapes of `TrackData.values` are:
71
+
72
+ * [num_tracks]
73
+ * [positional_bins, num_tracks]
74
+ * [positional_bins, positional_bins, num_tracks]
75
+ * ...
76
+
77
+ `TrackData` can store both model predictions and raw data. It can
78
+ optionally hold information about the `genome.Interval` from which the data
79
+ were derived and `.uns` for storing additional unstructured data.
80
+
81
+ In addition to being a container, `TrackData` provides functionality for
82
+ common aggregation and slicing operations.
83
+
84
+ Attributes:
85
+ values: A numpy array of floats or integers representing the track values.
86
+ Positional axes have the same length. Example valid shapes are:
87
+ [num_tracks], [positional_bins, num_tracks], and [positional_bins,
88
+ positional_bins, num_tracks].
89
+ metadata: A pandas DataFrame containing metadata for each track. The
90
+ DataFrame must have at least two columns: 'name' and 'strand'.
91
+ resolution: The resolution of the track data in base pairs.
92
+ interval: An optional `Interval` object representing the genomic region.
93
+ uns: An optional dictionary to store additional unstructured data.
94
+
95
+ Raises:
96
+ ValueError: If the number of tracks in `values` does not match the number
97
+ of rows in `metadata`, or if `metadata` contains duplicate (name, strand)
98
+ pairs, or if the positional axes have different lengths, or if the
99
+ interval width does not match the expected width.
100
+ """
101
+
102
+ # Use Union due to https://github.com/patrick-kidger/jaxtyping/issues/73.
103
+ values: Union[
104
+ Float32[np.ndarray, '*positional_bins num_tracks'],
105
+ Int32[np.ndarray, '*positional_bins num_tracks'],
106
+ Bool[np.ndarray, '*positional_bins num_tracks'],
107
+ ]
108
+ metadata: TrackMetadata
109
+ resolution: int = 1
110
+ interval: genome.Interval | None = None
111
+ uns: dict[str, Any] | None = (
112
+ # Unstructured data dict, analagous to anndata.AnnData.uns.
113
+ None
114
+ )
115
+
116
+ def __post_init__(self):
117
+ """Validates the consistency of the data."""
118
+ if self.values.shape[-1] != len(self.metadata):
119
+ raise ValueError(
120
+ f'value number of tracks {self.values.shape[-1]} and '
121
+ f'metadata {len(self.metadata)} do not match.'
122
+ )
123
+
124
+ if self.positional_axes:
125
+ if len(set(np.array(self.values.shape)[self.positional_axes])) != 1:
126
+ raise ValueError('All positional axes must have the same length.')
127
+
128
+ if self.interval and self.interval.width != self.width:
129
+ raise ValueError(
130
+ f'Interval width must match expected width. {self.interval.width=},'
131
+ f' {self.width=}'
132
+ )
133
+
134
+ if not {'name', 'strand'}.issubset(self.metadata.columns):
135
+ raise ValueError('Metadata must contain columns "name" and "strand".')
136
+
137
+ if self.metadata[['name', 'strand']].duplicated().any():
138
+ raise ValueError(
139
+ 'Metadata contain duplicated values for (name, strand) tuples.'
140
+ )
141
+
142
+ @property
143
+ def positional_axes(self) -> list[int]:
144
+ """Returns a list of the positional axes."""
145
+ return list(range(self.values.ndim - 1))
146
+
147
+ @property
148
+ def num_tracks(self) -> int:
149
+ """Returns the number of tracks."""
150
+ return self.values.shape[-1]
151
+
152
+ @property
153
+ def width(self) -> int:
154
+ """Returns the interval width covered by the tracks."""
155
+ if self.positional_axes:
156
+ return self.values.shape[0] * self.resolution
157
+ else:
158
+ return 0
159
+
160
+ @property
161
+ def names(self) -> np.ndarray:
162
+ """Returns an array of track names (not necessarily unique)."""
163
+ return self.metadata['name'].values
164
+
165
+ @property
166
+ def strands(self) -> np.ndarray:
167
+ """Returns an array of track strands."""
168
+ return self.metadata['strand'].values
169
+
170
+ @property
171
+ def ontology_terms(self) -> Sequence[ontology.OntologyTerm | None] | None:
172
+ """Returns a list of ontology terms (if available)."""
173
+ if 'ontology_curie' in self.metadata.columns:
174
+ return [
175
+ ontology.from_curie(curie) if curie is not None else None
176
+ for curie in self.metadata['ontology_curie'].values
177
+ ]
178
+ else:
179
+ return None
180
+
181
+ def copy(self) -> 'TrackData':
182
+ """Returns a deep copy of the `TrackData` object."""
183
+ if self.interval:
184
+ interval = self.interval.copy()
185
+ else:
186
+ interval = None
187
+ return TrackData(
188
+ self.values.copy(),
189
+ resolution=self.resolution,
190
+ metadata=self.metadata.copy(),
191
+ interval=interval,
192
+ uns=copy.deepcopy(self.uns),
193
+ )
194
+
195
+ def bin_index(self, relative_position: int) -> int:
196
+ """Returns the bin index for a relative position.
197
+
198
+ Args:
199
+ relative_position: The relative position within the interval.
200
+
201
+ Returns:
202
+ The corresponding bin index.
203
+ """
204
+ return relative_position // self.resolution
205
+
206
+ def slice_by_positions(self, start: int, end: int) -> 'TrackData':
207
+ """Slices the track data along the positional axes.
208
+
209
+ The slicing follows Python slicing conventions (0 indexed, and includes
210
+ elements up to end-1).
211
+
212
+ Args:
213
+ start: The 1-bp resolution start position for slicing.
214
+ end: The 1-bp resolution end position for slicing.
215
+
216
+ Returns:
217
+ A new `TrackData` object with the sliced values.
218
+
219
+ Raises:
220
+ ValueError: If (end - start) is greater than the width, or if (end -
221
+ start) is not divisible by the resolution.
222
+ """
223
+ if (end - start) > self.width:
224
+ raise ValueError(
225
+ 'When slicing track data, (end - start) must be less than or '
226
+ 'equal to width.'
227
+ )
228
+
229
+ if (end - start) % self.resolution != 0:
230
+ raise ValueError(
231
+ f'end - start needs to be to be divisible by {self.resolution=}'
232
+ )
233
+
234
+ sl = slice(self.bin_index(start), self.bin_index(end))
235
+ slice_list = [slice(None)] * self.values.ndim
236
+ for i in self.positional_axes:
237
+ slice_list[i] = sl
238
+
239
+ interval = self.interval
240
+ if interval:
241
+ interval = genome.Interval(
242
+ interval.chromosome,
243
+ interval.start + start,
244
+ interval.start + end,
245
+ strand=interval.strand,
246
+ name=interval.name,
247
+ info=interval.info,
248
+ )
249
+
250
+ return TrackData(
251
+ self.values[tuple(slice_list)],
252
+ resolution=self.resolution,
253
+ metadata=self.metadata,
254
+ interval=interval,
255
+ uns=self.uns,
256
+ )
257
+
258
+ def slice_by_interval(
259
+ self, interval: genome.Interval, match_resolution: bool = False
260
+ ) -> 'TrackData':
261
+ """Slices the track data using a `genome.Interval`.
262
+
263
+ Args:
264
+ interval: The interval to slice to.
265
+ match_resolution: If True, the interval will first be extended to make
266
+ sure the width is divisible by resolution.
267
+
268
+ Returns:
269
+ A new `TrackData` object sliced to the interval.
270
+
271
+ Raises:
272
+ ValueError: If `.interval` is not specified or if the specified interval
273
+ is not fully contained within the current interval.
274
+ """
275
+ if self.interval is None:
276
+ raise ValueError(
277
+ '.interval is needs to be specified for slice_by_interval.'
278
+ )
279
+ if not self.interval.contains(interval):
280
+ raise ValueError(
281
+ f'Interval {self.interval=} does not fully contain {interval=}.'
282
+ )
283
+ start = interval.start - self.interval.start
284
+ end = interval.end - self.interval.start
285
+
286
+ if match_resolution and self.resolution != 1:
287
+ start = int(np.floor(start / self.resolution) * self.resolution)
288
+ end = int(np.ceil(end / self.resolution) * self.resolution)
289
+ return self.slice_by_positions(start, end)
290
+
291
+ def pad(self, start_pad: int, end_pad: int) -> 'TrackData':
292
+ """Pads the track data along positional axes.
293
+
294
+ Args:
295
+ start_pad: The amount of padding to add at the beginning.
296
+ end_pad: The amount of padding to add at the end.
297
+
298
+ Returns:
299
+ A new `TrackData` object with padded values.
300
+
301
+ Raises:
302
+ ValueError: If `start_pad` or `end_pad` is not divisible by the
303
+ resolution.
304
+ """
305
+ if start_pad == 0 and end_pad == 0:
306
+ return self
307
+ if start_pad % self.resolution != 0:
308
+ raise ValueError(f'start_pad needs to be divisible by {self.resolution}')
309
+ if end_pad % self.resolution != 0:
310
+ raise ValueError(f'end_pad needs to be divisible by {self.resolution}')
311
+
312
+ pad = [(0, 0)] * self.values.ndim
313
+ for axis in self.positional_axes:
314
+ pad[axis] = (start_pad // self.resolution, end_pad // self.resolution)
315
+
316
+ return TrackData(
317
+ np.pad(self.values, tuple(pad)),
318
+ resolution=self.resolution,
319
+ metadata=self.metadata,
320
+ interval=None, # Padding invalidates the interval.
321
+ uns=self.uns,
322
+ )
323
+
324
+ def resize(self, width: int) -> 'TrackData':
325
+ """Resizes the track data by cropping or padding with a fixed center.
326
+
327
+ Args:
328
+ width: The desired width in base pairs.
329
+
330
+ Returns:
331
+ A new `TrackData` object with resized values.
332
+
333
+ Raises:
334
+ ValueError: If `width` is not divisible by the resolution.
335
+ """
336
+ if width == self.width:
337
+ return self
338
+ elif width > self.width:
339
+ if width % self.resolution != 0:
340
+ raise ValueError(f'width needs to be divisible by {self.resolution}')
341
+ pad_amount = (width - self.width) // self.resolution
342
+ pad_start = (pad_amount // 2 + pad_amount % 2) * self.resolution
343
+ pad_end = (pad_amount // 2) * self.resolution
344
+ return self.pad(pad_start, pad_end)
345
+ else:
346
+ crop_amount = (self.width - width) // self.resolution
347
+ start = (crop_amount // 2 + crop_amount % 2) * self.resolution
348
+ return self.slice_by_positions(start, start + width)
349
+
350
+ def upsample(
351
+ self,
352
+ resolution: int,
353
+ aggregation_type: AggregationType = AggregationType.SUM,
354
+ ) -> 'TrackData':
355
+ """Upsamples the track data to a higher resolution.
356
+
357
+ Args:
358
+ resolution: The desired resolution in base pairs.
359
+ aggregation_type: The aggregation method to use for pooling the values.
360
+
361
+ Returns:
362
+ A new `TrackData` object with upsampled values.
363
+
364
+ Raises:
365
+ ValueError: If `resolution` is not lower than the current resolution
366
+ or not divisible by the current resolution.
367
+ """
368
+ if resolution == self.resolution:
369
+ return self
370
+ if resolution > self.resolution:
371
+ raise ValueError(f'Resolution must be lower than {self.resolution}')
372
+ repeat = self.resolution // resolution
373
+ if self.resolution % resolution != 0:
374
+ raise ValueError(f'Resolution not divisible by {resolution}')
375
+
376
+ values = self.values
377
+ for axis in self.positional_axes:
378
+ values = np.repeat(values, repeat, axis=axis)
379
+ match aggregation_type:
380
+ case AggregationType.SUM:
381
+ values = values / repeat
382
+ case AggregationType.MAX:
383
+ pass
384
+ return TrackData(
385
+ values,
386
+ resolution=resolution,
387
+ metadata=self.metadata,
388
+ interval=self.interval,
389
+ uns=self.uns,
390
+ )
391
+
392
+ def downsample(
393
+ self,
394
+ resolution: int,
395
+ aggregation_type: AggregationType = AggregationType.SUM,
396
+ ) -> 'TrackData':
397
+ """Downsamples the track data to a lower resolution.
398
+
399
+ Args:
400
+ resolution: The desired resolution in base pairs.
401
+ aggregation_type: The aggregation method to use for pooling the values.
402
+
403
+ Returns:
404
+ A new `TrackData` object with downsampled values.
405
+
406
+ Raises:
407
+ ValueError: If `resolution` is not greater than the current resolution
408
+ or not divisible by the current resolution.
409
+ """
410
+ if resolution == self.resolution:
411
+ return self
412
+ if resolution < self.resolution:
413
+ raise ValueError(f'Resolution must be greater than {self.resolution}')
414
+ if resolution % self.resolution != 0:
415
+ raise ValueError(f'Resolution not divisible by {resolution}')
416
+ pool_width = resolution // self.resolution
417
+
418
+ values = self.values
419
+ for axis in self.positional_axes:
420
+ # Bring axis of interest to the front, reshape and aggregate, and reswap
421
+ values = np.swapaxes(values, 0, axis)
422
+ shape = list(values.shape)
423
+ reshaped_values = values.reshape(
424
+ [shape[0] // pool_width, pool_width] + shape[1:]
425
+ )
426
+ match aggregation_type:
427
+ case AggregationType.SUM:
428
+ values = reshaped_values.sum(axis=1)
429
+ case AggregationType.MAX:
430
+ values = reshaped_values.max(axis=1)
431
+ values = np.swapaxes(values, 0, axis)
432
+
433
+ return TrackData(
434
+ values,
435
+ resolution=resolution,
436
+ metadata=self.metadata,
437
+ interval=self.interval,
438
+ uns=self.uns,
439
+ )
440
+
441
+ def change_resolution(
442
+ self,
443
+ resolution: int,
444
+ aggregation_type: AggregationType = AggregationType.SUM,
445
+ ) -> 'TrackData':
446
+ """Changes the resolution of the track data.
447
+
448
+ Args:
449
+ resolution: The desired resolution in base pairs.
450
+ aggregation_type: The aggregation method to use for pooling the values.
451
+
452
+ Returns:
453
+ A new `TrackData` object with the new resolution.
454
+ """
455
+ if resolution >= self.resolution:
456
+ return self.downsample(resolution, aggregation_type)
457
+ else:
458
+ return self.upsample(resolution, aggregation_type)
459
+
460
+ def filter_tracks(self, mask: np.ndarray | list[bool]) -> 'TrackData':
461
+ """Filters tracks by a boolean mask.
462
+
463
+ Args:
464
+ mask: A boolean mask to select tracks.
465
+
466
+ Returns:
467
+ A new `TrackData` object with the filtered tracks.
468
+ """
469
+ return TrackData(
470
+ self.values[..., mask],
471
+ resolution=self.resolution,
472
+ metadata=self.metadata.iloc[mask],
473
+ interval=self.interval,
474
+ uns=self.uns,
475
+ )
476
+
477
+ def filter_to_positive_strand(self) -> 'TrackData':
478
+ """Filters tracks to the positive DNA strand."""
479
+ return self.filter_tracks(self.strands == genome.STRAND_POSITIVE)
480
+
481
+ def filter_to_negative_strand(self) -> 'TrackData':
482
+ """Filters tracks to the negative DNA strand."""
483
+ return self.filter_tracks(self.strands == genome.STRAND_NEGATIVE)
484
+
485
+ def filter_to_nonnegative_strand(self) -> 'TrackData':
486
+ """Filters tracks to the non-negative DNA strands (positive and unstranded)."""
487
+ return self.filter_tracks(self.strands != genome.STRAND_NEGATIVE)
488
+
489
+ def filter_to_nonpositive_strand(self) -> 'TrackData':
490
+ """Filters tracks to the non-positive DNA strands (negative and unstranded)."""
491
+ return self.filter_tracks(self.strands != genome.STRAND_POSITIVE)
492
+
493
+ def filter_to_stranded(self) -> 'TrackData':
494
+ """Filters tracks to stranded tracks (excluding unstranded)."""
495
+ return self.filter_tracks(self.strands != genome.STRAND_UNSTRANDED)
496
+
497
+ def filter_to_unstranded(self) -> 'TrackData':
498
+ """Filters tracks to unstranded tracks."""
499
+ return self.filter_tracks(self.strands == genome.STRAND_UNSTRANDED)
500
+
501
+ def select_tracks_by_index(
502
+ self, idx: np.ndarray | Sequence[int]
503
+ ) -> 'TrackData':
504
+ """Selects tracks by numerical index.
505
+
506
+ Args:
507
+ idx: A list or array of numerical indices to select tracks.
508
+
509
+ Returns:
510
+ A new `TrackData` object with the selected tracks.
511
+ """
512
+ return TrackData(
513
+ self.values[..., idx],
514
+ resolution=self.resolution,
515
+ metadata=self.metadata.iloc[idx],
516
+ interval=self.interval,
517
+ uns=self.uns,
518
+ )
519
+
520
+ def select_tracks_by_name(
521
+ self, names: np.ndarray | Sequence[str]
522
+ ) -> 'TrackData':
523
+ """Selects tracks by name.
524
+
525
+ Args:
526
+ names: A list or array of track names to select.
527
+
528
+ Returns:
529
+ A new `TrackData` object with the selected tracks.
530
+ """
531
+ track_idx = pd.Series(np.arange(self.num_tracks), index=self.names)
532
+ return self.select_tracks_by_index(track_idx.loc[names].values)
533
+
534
+ def __getitem__(self, index: Index) -> 'TrackData':
535
+ """Retrieves a subset of TrackData using positional and/or track indices.
536
+
537
+ This method allows slicing `TrackData` similar to numpy arrays or pandas
538
+ DataFrames. The index can be a single value or a tuple.
539
+
540
+ Args:
541
+ index: A single index or a tuple of indices. If a single index, it's
542
+ treated as a positional index if the `TrackData` has positional axes,
543
+ otherwise as a track index. If a tuple, the first element specifies the
544
+ positional slice, and the second element specifies the track index.
545
+ Positional indices can be `int`, `slice`, or `genome.Interval`, and this
546
+ slice is applied to all positional axes of the `values` array. Track
547
+ indices can be `int`, `str` (track name), `slice`, `Sequence[int]`, or
548
+ `Sequence[str]` (track names).
549
+
550
+ Returns:
551
+ A new `TrackData` object containing the selected subset.
552
+
553
+ Raises:
554
+ IndexError: If a slice step is not 1 for positional indexing.
555
+ IndexError: If an unsupported index type is provided.
556
+ """
557
+ if isinstance(index, tuple):
558
+ position_index, track_index = index
559
+ elif self.positional_axes:
560
+ position_index, track_index = index, None
561
+ else:
562
+ position_index, track_index = None, index
563
+ if isinstance(track_index, genome.Interval):
564
+ raise IndexError(
565
+ 'Track indexing by interval is supported only when there are'
566
+ ' positional axes.'
567
+ )
568
+
569
+ tdata = self
570
+ match position_index:
571
+ case None:
572
+ pass
573
+ case int():
574
+ tdata = tdata.slice_by_positions(position_index, position_index + 1)
575
+ case slice():
576
+ if position_index.step is not None and position_index.step != 1:
577
+ raise IndexError('Slice step must be 1 for positional indexing.')
578
+ if position_index != slice(None):
579
+ tdata = tdata.slice_by_positions(
580
+ position_index.start, position_index.stop
581
+ )
582
+ case genome.Interval():
583
+ tdata = tdata.slice_by_interval(position_index)
584
+ case _:
585
+ raise IndexError(
586
+ f'Unsupported positional index type: {type(position_index)}'
587
+ )
588
+
589
+ match track_index:
590
+ case None:
591
+ pass
592
+ case str():
593
+ tdata = tdata.select_tracks_by_name([track_index])
594
+ case int():
595
+ tdata = tdata.select_tracks_by_index([track_index])
596
+ case slice():
597
+ if track_index != slice(None):
598
+ indices = np.arange(tdata.num_tracks)[track_index]
599
+ tdata = tdata.select_tracks_by_index(indices)
600
+ case np.ndarray() if np.issubdtype(track_index.dtype, np.character):
601
+ tdata = tdata.select_tracks_by_name(track_index)
602
+ case np.ndarray():
603
+ tdata = tdata.select_tracks_by_index(track_index)
604
+ case Sequence():
605
+ track_index_arr = np.asarray(track_index)
606
+ if np.issubdtype(track_index_arr.dtype, np.character):
607
+ tdata = tdata.select_tracks_by_name(track_index_arr)
608
+ else:
609
+ tdata = tdata.select_tracks_by_index(track_index_arr)
610
+ case _:
611
+ raise IndexError(f'Unsupported track index type: {type(track_index)}')
612
+ return tdata
613
+
614
+ def groupby(self, column: str) -> dict[str, 'TrackData']:
615
+ """Splits tracks into groups based on a metadata column.
616
+
617
+ This method splits the tracks in the `TrackData` object into separate
618
+ `TrackData` objects based on the unique values in the specified metadata
619
+ column. It returns a dictionary where the keys are the unique values in
620
+ the column, and the values are new `TrackData` objects containing the
621
+ tracks corresponding to each key.
622
+
623
+ Args:
624
+ column: The name of the metadata column to split by.
625
+
626
+ Returns:
627
+ A dictionary mapping unique values in the column to `TrackData` objects
628
+ containing the corresponding tracks.
629
+ """
630
+ output = {}
631
+ for key in self.metadata[column].unique():
632
+ mask = (self.metadata[column] == key).values
633
+ output[key] = self.filter_tracks(mask)
634
+ return output
635
+
636
+ def _reverse_complement_idx(self) -> np.ndarray:
637
+ """Gets indices for reverse complementing the tracks.
638
+
639
+ Returns:
640
+ An array of indices that reorders the tracks to achieve reverse
641
+ complementation.
642
+
643
+ Raises:
644
+ ValueError: If not all stranded tracks have both '+' and '-' strands,
645
+ or if the number of '+' and '-' stranded tracks is not equal.
646
+ """
647
+ df_strands = pd.DataFrame({
648
+ 'name': self.names,
649
+ 'strand': self.strands,
650
+ 'old_idx': np.arange(self.num_tracks),
651
+ })
652
+ df_strands = df_strands[df_strands.strand != genome.STRAND_UNSTRANDED]
653
+ df_strands.sort_values(['strand', 'name'], inplace=True)
654
+ if np.all(df_strands.groupby('name').size() != 2):
655
+ raise ValueError('Not all stranded tracks have both + and - strand.')
656
+ if (df_strands.strand == genome.STRAND_POSITIVE).sum() != (
657
+ df_strands.strand == genome.STRAND_NEGATIVE
658
+ ).sum():
659
+ raise ValueError(
660
+ 'We need to have the exact same number of + and - stranded tracks'
661
+ )
662
+ new_idx = df_strands.old_idx.values.reshape((2, -1))[::-1].ravel()
663
+ # Swap strands by idx.
664
+ idx = np.arange(self.num_tracks)
665
+ idx[df_strands.old_idx.values] = new_idx
666
+ return idx
667
+
668
+ def reverse_complement(self) -> 'TrackData':
669
+ """Reverse complements the track data and interval if present.
670
+
671
+ Returns:
672
+ A new `TrackData` object with reverse complemented tracks.
673
+ """
674
+ if self.interval:
675
+ # Note that the interval needs to be stranded in order to perform
676
+ # this operation.
677
+ interval = self.interval.swap_strand()
678
+ else:
679
+ interval = None
680
+
681
+ idx = self._reverse_complement_idx()
682
+ slices = [slice(None)] * self.values.ndim
683
+ slices[-1] = idx
684
+ for axis in self.positional_axes:
685
+ slices[axis] = slice(None, None, -1)
686
+
687
+ return TrackData(
688
+ self.values[tuple(slices)],
689
+ resolution=self.resolution,
690
+ metadata=self.metadata.iloc[idx],
691
+ interval=interval,
692
+ uns=self.uns,
693
+ )
694
+
695
+ def _check_track_data_compatibility(self, other: 'TrackData') -> None:
696
+ """Checks if two `TrackData` objects are compatible for sum/diff.
697
+
698
+ Args:
699
+ other: The other `TrackData` object to compare.
700
+
701
+ Raises:
702
+ TypeError: If `other` is not a `TrackData` object.
703
+ ValueError: If the intervals, resolutions, shapes, or metadata
704
+ shapes don't match between the two objects.
705
+ """
706
+ if not isinstance(other, TrackData):
707
+ raise TypeError(
708
+ f'Unsupported type "{type(other)}". Must be a TrackData object'
709
+ )
710
+ if self.interval != other.interval:
711
+ raise ValueError('Intervals must match for the two TrackData objects.')
712
+ if self.resolution != other.resolution:
713
+ raise ValueError('Resolutions must match for the two TrackData objects.')
714
+ if self.values.shape != other.values.shape:
715
+ raise ValueError('Shapes must match for the two TrackData objects.')
716
+ if self.metadata.shape != other.metadata.shape:
717
+ raise ValueError(
718
+ 'Metadata shapes must match for the two TrackData objects.'
719
+ )
720
+
721
+ def __add__(self, other: 'TrackData') -> 'TrackData':
722
+ """Adds the values of two `TrackData` objects.
723
+
724
+ Args:
725
+ other: The `TrackData` object to add.
726
+
727
+ Returns:
728
+ A new `TrackData` object with the summed values.
729
+
730
+ Raises:
731
+ ValueError: If the objects are not compatible (see
732
+ `_check_track_data_compatibility`).
733
+ TypeError: If `other` is not a `TrackData` object.
734
+ """
735
+ self._check_track_data_compatibility(other)
736
+ new_values = self.values + other.values
737
+ return TrackData(
738
+ values=new_values,
739
+ metadata=self.metadata,
740
+ resolution=self.resolution,
741
+ interval=self.interval,
742
+ uns=self.uns,
743
+ )
744
+
745
+ def __sub__(self, other: 'TrackData') -> 'TrackData':
746
+ """Subtracts the values of two `TrackData` objects.
747
+
748
+ Args:
749
+ other: The `TrackData` object to subtract.
750
+
751
+ Returns:
752
+ A new `TrackData` object with the difference of the values.
753
+
754
+ Raises:
755
+ ValueError: If the objects are not compatible (see
756
+ `_check_track_data_compatibility`).
757
+ TypeError: If `other` is not a `TrackData` object.
758
+ """
759
+ self._check_track_data_compatibility(other)
760
+ new_values = self.values - other.values
761
+ return TrackData(
762
+ values=new_values,
763
+ metadata=self.metadata,
764
+ resolution=self.resolution,
765
+ interval=self.interval,
766
+ uns=self.uns,
767
+ )
768
+
769
+
770
+ def concat(
771
+ track_datas: Sequence[TrackData],
772
+ extra_metadata_name_and_keys: (
773
+ tuple[str, Sequence[str | int | float]] | None
774
+ ) = None,
775
+ ) -> TrackData:
776
+ """Concatenates multiple `TrackData` objects along the track dimension.
777
+
778
+ This function combines multiple `TrackData` objects into a single object
779
+ by concatenating their values and metadata. The resulting `TrackData`
780
+ object will have the same resolution and interval as the input objects.
781
+
782
+ Args:
783
+ track_datas: A sequence of `TrackData` objects to concatenate. All objects
784
+ must have the same resolution, interval, and width.
785
+ extra_metadata_name_and_keys: An optional tuple specifying a new metadata
786
+ column to add. The first element is the column name, and the second is a
787
+ sequence of values to populate the column.
788
+
789
+ Returns:
790
+ A new `TrackData` object containing the concatenated data.
791
+
792
+ Raises:
793
+ ValueError: If the input `TrackData` objects have different resolutions,
794
+ intervals, or widths, or if the length of
795
+ `extra_metadata_name_and_keys[1]` does not match the length of
796
+ `track_datas`.
797
+ """
798
+ if len(set(x.resolution for x in track_datas)) != 1:
799
+ raise ValueError('Track data contain multiple resolutions')
800
+ if len(set(str(x.interval) for x in track_datas)) != 1:
801
+ raise ValueError('Track data contain multiple intervals')
802
+ if len(set(x.width for x in track_datas)) != 1:
803
+ raise ValueError('Track data are of different width')
804
+ if extra_metadata_name_and_keys:
805
+ if len(extra_metadata_name_and_keys[1]) != len(track_datas):
806
+ raise ValueError(
807
+ 'Second element of new_metadata_name_and_keys must be the same'
808
+ ' length as track_datas'
809
+ )
810
+
811
+ concatenated_metadata = (
812
+ pd.concat(
813
+ [x.metadata for x in track_datas],
814
+ keys=extra_metadata_name_and_keys[1]
815
+ if extra_metadata_name_and_keys
816
+ else None,
817
+ names=[extra_metadata_name_and_keys[0]]
818
+ if extra_metadata_name_and_keys
819
+ else None,
820
+ )
821
+ .reset_index(level=0, drop=extra_metadata_name_and_keys is None)
822
+ .reset_index(drop=True)
823
+ )
824
+ return TrackData(
825
+ np.concatenate(
826
+ [x.values for x in track_datas], axis=track_datas[0].values.ndim - 1
827
+ ),
828
+ resolution=track_datas[0].resolution,
829
+ metadata=concatenated_metadata,
830
+ interval=track_datas[0].interval,
831
+ uns=None,
832
+ )
833
+
834
+
835
+ def interleave(
836
+ track_datas: Sequence[TrackData], name_prefixes: Sequence[str]
837
+ ) -> TrackData:
838
+ """Interleaves multiple `TrackData` objects by alternating rows.
839
+
840
+ This function combines multiple `TrackData` objects into a single object
841
+ by interleaving their rows and metadata. This interleaves operation alternates
842
+ between the trackdatas, like shuffling cards, i.e., 'abcd' interleaved with
843
+ 'efgh' would be "aebfcgdh". The resulting `TrackData` object will have the
844
+ same resolution and interval as the input objects, but the number of tracks
845
+ will be the sum of the tracks in the input objects.
846
+
847
+ Args:
848
+ track_datas: A sequence of `TrackData` objects to interleave. All objects
849
+ must have the same shape, resolution, and interval. The order in this list
850
+ will determine the interleaving order.
851
+ name_prefixes: A sequence of name prefixes to add to the track names in the
852
+ metadata to ensure uniqueness of (name, strand) pairs.
853
+
854
+ Returns:
855
+ A new `TrackData` object containing the interleaved data.
856
+
857
+ Raises:
858
+ ValueError: If the input `TrackData` objects have different shapes,
859
+ resolutions, or intervals.
860
+ """
861
+ # Checks on the track data.
862
+ shapes = set(data.values.shape for data in track_datas)
863
+ if len(shapes) != 1:
864
+ raise ValueError(
865
+ 'Cannot interleave track data which have different shapes. '
866
+ f'Detected shapes: {shapes}'
867
+ )
868
+
869
+ if any(data.resolution != track_datas[0].resolution for data in track_datas):
870
+ raise ValueError(
871
+ 'Cannot interleave track data which have different resolutions. '
872
+ f'Detected shapes: {shapes}'
873
+ )
874
+
875
+ if any(data.interval != track_datas[0].interval for data in track_datas):
876
+ raise ValueError(
877
+ 'Cannot interleave track data which have different intervals. '
878
+ )
879
+
880
+ # Interleave arrays.
881
+ shape = list(track_datas[0].values.shape)
882
+ shape[-1] = shape[-1] * len(track_datas)
883
+ interleaved_data = np.empty(tuple(shape), dtype=track_datas[0].values.dtype)
884
+
885
+ for i, data in enumerate(track_datas):
886
+ interleaved_data[..., i :: len(track_datas)] = data.values
887
+
888
+ # Interleave metadata.
889
+ metadatas = []
890
+ for prefix, data in zip(name_prefixes, track_datas):
891
+ metadata = data.metadata.copy()
892
+ metadata['name'] = prefix + metadata['name']
893
+ metadatas.append(metadata)
894
+
895
+ # Assign a new index idx that, when sorted, will produce an interleave.
896
+ interleaved_metadata = (
897
+ pd.concat([
898
+ metadata.assign(
899
+ idx=np.arange(
900
+ stop=(len(metadata) * len(track_datas)),
901
+ step=len(track_datas),
902
+ )
903
+ + i
904
+ )
905
+ for i, metadata in enumerate(metadatas)
906
+ ])
907
+ .sort_values('idx')
908
+ .drop('idx', axis=1)
909
+ .reset_index(drop=True)
910
+ )
911
+
912
+ return TrackData(
913
+ values=interleaved_data,
914
+ metadata=interleaved_metadata,
915
+ resolution=track_datas[0].resolution,
916
+ interval=track_datas[0].interval,
917
+ uns={'num_interleaved_trackdatas': len(track_datas)},
918
+ )
flax_model/alphagenome/_sdk/data/transcript.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utilities for working with transcripts."""
16
+
17
+ import collections
18
+ import copy
19
+ import dataclasses
20
+ import functools
21
+ import sys
22
+ from typing import Any
23
+
24
+ from flax_model.alphagenome._sdk.data import genome
25
+ import pandas as pd
26
+
27
+
28
+ MITOCHONDRIAL_CHROMS = ['M', 'chrM', 'MT']
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True)
32
+ class Transcript:
33
+ """Represents transcript object containing attributes from a GTF file.
34
+
35
+ A transcript is a region of DNA that encodes a single RNA molecule. The
36
+ Transcript dataclass contains attributes that describe the structure and
37
+ content of a transcript, namely:
38
+
39
+ Attributes:
40
+ exons: A list of `genome.Interval`s representing exons within transcript.
41
+ Each `Transcript` must contain exons.
42
+ cds: An optional list of `genome.Interval`s representing coding sequences
43
+ (CDS) within a transcript. CDS include start codon and exclude stop codon.
44
+ start_codon: An optional list of `genome.Interval`s representing a single
45
+ start codon. Start codons can be split by introns, therefore might have
46
+ more than one genomic interval. Some coding transcripts are missing start
47
+ codons, e.g., ENST00000455638.6.
48
+ stop_codon: An optional list of `genome.Interval`s representing a single
49
+ stop codon. Stop codon can be split by introns, therefore might have more
50
+ than one genomic interval. Some transcripts coding transcripts are missing
51
+ stop codons, e.g., ENST00000574051.5.
52
+ transcript_id: An optional string representing a transcript id.
53
+ gene_id: An optional string representing a gene id.
54
+ protein_id: An optional string representing a protein id which is encoded by
55
+ the transcript.
56
+ uniprot_id: An optional UniprotKB-AC id string.
57
+ info: a dictionary of additional information on a transcript.
58
+ chromosome: chromosome name on which the transcript is present. Must be the
59
+ same for all genomic intervals within a transcript.
60
+ is_mitochondrial: whether the transcript is on the mitochondria chromosome.
61
+ strand_int: strand on which transcript is present as an int. -1 for negative
62
+ strand +1 for positive strand
63
+ strand: strand (positive or negative) on which transcript is present. Must
64
+ be the same for all genomic intervals within a transcript.
65
+ is_negative_strand: a boolean value indicating whether transcript is on
66
+ negative strand.
67
+ is_positive_strand: a boolean value indicating whether transcript is on
68
+ positive strand.
69
+ transcript_interval: a genomic interval of a transcript.
70
+ selenocysteines: a list of intervals where selenocysteines are present
71
+ within a transcript.
72
+ selenocysteine_pos_in_protein: a list of 0-based positions of
73
+ selenocysteines in protein encoded by the transcript.
74
+ is_coding: a value indicating whether a `Transcript` contains coding
75
+ sequences (CDS) or not.
76
+ cds_including_stop_codon: a list of CDS and stop_codon intervals with
77
+ overlapping intervals merged.
78
+ utr5: A list of genomic intervals representing 5' untranslated region. 5'
79
+ UTR doesn't include start codon. There may be no 5' UTR present in the
80
+ transcript or UTRs can be split by introns.
81
+ utr3: A list of genomic intervals representing 3' untranslated region. 3'
82
+ UTR doesn't include stop codon. There may be no 3' UTR present in the
83
+ transcript or UTRs can be split by introns.
84
+ splice_regions: a list of splice regions within a transcript.
85
+ splice_donor_sites: a list of splice donor sites. Commonly, the RNA sequence
86
+ that is removed begins with the dinucleotide GU at its 5′ end.
87
+ splice_acceptor_sites: a list of splice acceptor sites. Commonly, the RNA
88
+ sequence that is removed ends with AG at its 3′ end.
89
+ splice_donors: a list of splice donors. The first nucleotide of the intron
90
+ (0-based).
91
+ splice_acceptors: a list of splice acceptors. The last nucleotide of the
92
+ intron (0-based).
93
+ """
94
+
95
+ exons: list[genome.Interval]
96
+ cds: list[genome.Interval] | None = None
97
+ start_codon: list[genome.Interval] | None = None
98
+ stop_codon: list[genome.Interval] | None = None
99
+ transcript_id: str | None = dataclasses.field(compare=False, default=None)
100
+ gene_id: str | None = dataclasses.field(compare=False, default=None)
101
+ protein_id: str | None = dataclasses.field(compare=False, default=None)
102
+ uniprot_id: str | None = dataclasses.field(compare=False, default=None)
103
+ info: dict[str, Any] = dataclasses.field(
104
+ default_factory=dict, repr=False, compare=False, hash=False
105
+ )
106
+
107
+ def offset_in_cds(self, genome_position: int) -> int | None:
108
+ """Return the offset within the set of CDS exons of `genome_position`.
109
+
110
+ Args:
111
+ genome_position: A coordinate presumed to be on the same chromosome as
112
+ this transcript.
113
+
114
+ Returns:
115
+ The offset of `genome_position` from the start of the CDS, accounting for
116
+ strand, or None, if `genome_position` does not overlap the CDS.
117
+ """
118
+ offset = 0
119
+ for cds_exon in self.cds_including_stop_codon[:: self.strand_int]:
120
+ if cds_exon.start <= genome_position < cds_exon.end:
121
+ if self.is_positive_strand:
122
+ return offset + genome_position - cds_exon.start
123
+ else:
124
+ return offset + cds_exon.end - genome_position - 1
125
+ offset += cds_exon.width
126
+ return None
127
+
128
+ @property
129
+ def chromosome(self) -> str:
130
+ """Gets the chromosome name on which the transcript is present.
131
+
132
+ Returns:
133
+ The chromosome name.
134
+ """
135
+ return self.exons[0].chromosome
136
+
137
+ @property
138
+ def is_mitochondrial(self) -> bool:
139
+ """Gets whether the transcript is on the mitochondria chromosome.
140
+
141
+ Returns:
142
+ True if the transcript is on the mitochondria chromosome, False otherwise.
143
+ """
144
+ return self.chromosome in MITOCHONDRIAL_CHROMS
145
+
146
+ # TODO: b/376466056 - Unify strand representations.
147
+ @property
148
+ def strand_int(self) -> int:
149
+ """Gets the strand as an integer.
150
+
151
+ Returns:
152
+ -1 for negative strand, +1 for positive strand, 0 for unknown.
153
+ """
154
+ return {genome.STRAND_NEGATIVE: -1, genome.STRAND_POSITIVE: +1}.get(
155
+ self.strand, 0
156
+ )
157
+
158
+ @property
159
+ def strand(self) -> str:
160
+ """Gets the strand on which the transcript is present.
161
+
162
+ Returns:
163
+ The strand (positive or negative).
164
+ """
165
+ return self.exons[0].strand
166
+
167
+ @property
168
+ def is_positive_strand(self) -> bool:
169
+ return self.strand == genome.STRAND_POSITIVE
170
+
171
+ @property
172
+ def is_negative_strand(self) -> bool:
173
+ return self.strand == genome.STRAND_NEGATIVE
174
+
175
+ @functools.cached_property
176
+ def transcript_interval(self) -> genome.Interval:
177
+ """Gets a genomic interval of a transcript.
178
+
179
+ Returns:
180
+ A genomic interval of a transcript where transcript start is equal to the
181
+ first exon start and transcript end is equal to the last exon end.
182
+ """
183
+ return genome.Interval(
184
+ self.chromosome,
185
+ self.exons[0].start,
186
+ self.exons[-1].end,
187
+ strand=self.strand,
188
+ )
189
+
190
+ @functools.cached_property
191
+ def selenocysteines(self) -> list[genome.Interval]:
192
+ if 'selenocysteines' not in self.info:
193
+ return []
194
+ return self.info['selenocysteines']
195
+
196
+ @functools.cached_property
197
+ def selenocysteine_pos_in_protein(self) -> list[int]:
198
+ # 0-based
199
+ selenocystein_pos = []
200
+ for selenocysteine in self.selenocysteines:
201
+ if selenocysteine.info['cds_offset'] is None:
202
+ raise ValueError(
203
+ 'Transcript cannot be translated due to '
204
+ 'bad input data of selenocysteines.'
205
+ )
206
+ selenocystein_pos.append(selenocysteine.info['cds_offset'] // 3)
207
+ return selenocystein_pos
208
+
209
+ @functools.cached_property
210
+ def introns(self) -> list[genome.Junction]:
211
+ """Get a list of intron intervals.
212
+
213
+ Returns:
214
+ A list of genomic intervals representing introns, where a single intron
215
+ junction is an interval spanning between two adjacent exons.
216
+ """
217
+ intron_intervals = []
218
+ for i in range(1, len(self.exons)):
219
+ intron_intervals.append(
220
+ genome.Junction(
221
+ self.chromosome,
222
+ self.exons[i - 1].end,
223
+ self.exons[i].start,
224
+ strand=self.strand,
225
+ )
226
+ )
227
+ return intron_intervals
228
+
229
+ @functools.cached_property
230
+ def is_coding(self) -> bool:
231
+ return bool(self.cds)
232
+
233
+ @functools.cached_property
234
+ def cds_including_stop_codon(self) -> list[genome.Interval]:
235
+ """Obtains coding sequences including stop codon.
236
+
237
+ By default gtf files exclude stop codons from CDS while gff include stop
238
+ codons within coding sequences.
239
+ """
240
+ if not self.is_coding:
241
+ return []
242
+ return genome.merge_overlapping_intervals(
243
+ self.cds + (self.stop_codon or [])
244
+ )
245
+
246
+ @functools.cached_property
247
+ def utr5(self) -> list[genome.Interval]:
248
+ return self._get_utr(self.strand != genome.STRAND_NEGATIVE)
249
+
250
+ @functools.cached_property
251
+ def utr3(self) -> list[genome.Interval]:
252
+ return self._get_utr(self.strand == genome.STRAND_NEGATIVE)
253
+
254
+ def _get_utr(self, before: bool) -> list[genome.Interval]:
255
+ """Gets the UTRs located before/after first/last coding sequence."""
256
+ utrs = []
257
+ if not self.cds:
258
+ return utrs
259
+
260
+ merged_cds_stop = genome.merge_overlapping_intervals(
261
+ self.cds + (self.stop_codon or [])
262
+ )
263
+
264
+ if before:
265
+ start, end = 0, merged_cds_stop[0].start
266
+ else:
267
+ start, end = merged_cds_stop[-1].end, sys.maxsize
268
+
269
+ valid_interval = genome.Interval(
270
+ self.chromosome, start, end, strand=self.strand
271
+ )
272
+
273
+ for exon in filter(lambda x: x.overlaps(valid_interval), self.exons):
274
+ intersect = valid_interval.intersect(exon)
275
+ if intersect:
276
+ utrs.append(intersect)
277
+ return utrs
278
+
279
+ # TODO: b/376465275 - deal with cases where exon shorter than 3 bp length
280
+ # TODO: b/376465275 - deal with cases where intron is shorther than 4 bp
281
+ @functools.cached_property
282
+ def splice_regions(self) -> list[genome.Interval]:
283
+ """Obtains and returns splice regions of a transcript.
284
+
285
+ splice region (SO:0001630) is "within 1-3 bases of the exon or 3-8 bases of
286
+ the intron.
287
+ """
288
+ if not self.introns:
289
+ return []
290
+ splice_regions = []
291
+
292
+ for intron, prev_exon, next_exon in zip(
293
+ self.introns, self.exons[:-1], self.exons[1:]
294
+ ):
295
+ if prev_exon.width > 2:
296
+ splice_regions.append(
297
+ genome.Interval(
298
+ self.chromosome,
299
+ intron.start - 3,
300
+ intron.start,
301
+ strand=self.strand,
302
+ )
303
+ )
304
+
305
+ if next_exon.width > 2:
306
+ splice_regions.append(
307
+ genome.Interval(
308
+ self.chromosome, intron.end, intron.end + 3, strand=self.strand
309
+ )
310
+ )
311
+
312
+ if intron.width > 4:
313
+ splice_regions.append(
314
+ genome.Interval(
315
+ self.chromosome,
316
+ intron.start + 2,
317
+ min(intron.start + 8, intron.end - 2),
318
+ strand=self.strand,
319
+ )
320
+ )
321
+ splice_regions.append(
322
+ genome.Interval(
323
+ self.chromosome,
324
+ max(intron.end - 8, intron.start + 2),
325
+ intron.end - 2,
326
+ strand=self.strand,
327
+ )
328
+ )
329
+ return genome.merge_overlapping_intervals(splice_regions)
330
+
331
+ @functools.cached_property
332
+ def splice_donor_sites(self) -> list[genome.Interval]:
333
+ return self._get_splice_sites(False, intron_overhang=2, exon_overhang=0)
334
+
335
+ @functools.cached_property
336
+ def splice_acceptor_sites(self) -> list[genome.Interval]:
337
+ return self._get_splice_sites(True, intron_overhang=2, exon_overhang=0)
338
+
339
+ @functools.cached_property
340
+ def splice_donors(self) -> list[genome.Interval]:
341
+ # To be consistent with the splice sites defined by intron start and end,
342
+ # the overhang for donor and acceptor are different.
343
+ return self._get_splice_sites( # pytype: disable=bad-return-type # enable-cached-property
344
+ False, intron_overhang=1, exon_overhang=0
345
+ )
346
+
347
+ @functools.cached_property
348
+ def splice_acceptors(self) -> list[genome.Interval]:
349
+ return self._get_splice_sites( # pytype: disable=bad-return-type # enable-cached-property
350
+ True, intron_overhang=0, exon_overhang=1
351
+ )
352
+
353
+ # TODO: b/376465275 - deal with cases where intron shorter than 4 bp length.
354
+ def _get_splice_sites(
355
+ self, acceptor: bool, intron_overhang: int, exon_overhang: int
356
+ ) -> list[genome.Interval]:
357
+ """Obtains splice acceptor/donor intervals.
358
+
359
+ https://www.nature.com/scitable/topicpage/rna-splicing-introns-exons-and-spliceosome-12375/#:~:text=Introns%20are%20removed%20from%20primary,AG%20at%20its%203%E2%80%B2%20end
360
+ Introns are removed from primary transcripts by cleavage at conserved
361
+ sequences called splice sites. These sites are found at the 5′ and 3′
362
+ ends of introns. Most commonly, the RNA sequence that is removed begins with
363
+ the dinucleotide GU at its 5′ end, and ends with AG at its 3′ end.
364
+
365
+ if - strand, the end two bases of intron are splice donor bases,
366
+ if +, then the start two bases.
367
+
368
+ Args:
369
+ acceptor: value indicating whether splice acceptor or donor should be
370
+ obtained.
371
+ intron_overhang: bases into the intron.
372
+ exon_overhang: bases into the exon.
373
+
374
+ Returns:
375
+ List of intervals of splice acceptor/donor sites.
376
+ """
377
+ if not self.introns:
378
+ return []
379
+ splice_sites = []
380
+ for intron in self.introns: # pylint:disable=not-an-iterable
381
+ if intron.width < 4:
382
+ continue
383
+ if self.is_negative_strand != acceptor:
384
+ splice = genome.Interval(
385
+ self.chromosome,
386
+ intron.end - intron_overhang,
387
+ intron.end + exon_overhang,
388
+ strand=self.strand,
389
+ )
390
+ else:
391
+ splice = genome.Interval(
392
+ self.chromosome,
393
+ intron.start - exon_overhang,
394
+ intron.start + intron_overhang,
395
+ strand=self.strand,
396
+ )
397
+ splice_sites.append(splice)
398
+ return splice_sites
399
+
400
+ def __post_init__(self):
401
+ if not self.exons:
402
+ raise ValueError('Transcript must contain at least one exon.')
403
+
404
+ for exon in self.exons:
405
+ if exon.strand != self.strand or exon.chromosome != self.chromosome:
406
+ raise ValueError(
407
+ 'Transcript intervals are inconsistent. All intervals of a '
408
+ 'transcript should have same strand and chromosome.'
409
+ )
410
+
411
+ if self.cds:
412
+ # first exons can be part of UTR. Searching for the first coding exon.
413
+ index = 0
414
+ for exon in self.exons:
415
+ # if overlaps, will check whether exon contains it in the latter loop.
416
+ if exon.overlaps(self.cds[0]):
417
+ break
418
+ index += 1
419
+ if index == len(self.exons) or len(self.cds) + index > len(self.exons):
420
+ raise ValueError(
421
+ 'The number of coding exons must be the same as CDS '
422
+ 'and CDS cannot be outside of the exon intervals.'
423
+ )
424
+
425
+ # checks each subsequent exon is coding
426
+ for seq, exon in zip(self.cds, self.exons[index : index + len(self.cds)]):
427
+ if not exon.contains(seq):
428
+ raise ValueError(
429
+ 'The number of coding exons must be the same as CDS '
430
+ 'and CDS cannot be outside of the exon intervals.'
431
+ )
432
+ if seq.strand != self.strand:
433
+ raise ValueError(
434
+ 'Transcript intervals are inconsistent. All intervals of a '
435
+ 'transcript should have same strand and chromosome.'
436
+ )
437
+
438
+ for sc in self.selenocysteines: # pylint:disable=not-an-iterable
439
+ sc_pos = sc.end - 1 if sc.negative_strand else sc.start
440
+ sc.info['cds_offset'] = self.offset_in_cds(sc_pos)
441
+
442
+ def __len__(self):
443
+ return self.transcript_interval.width
444
+
445
+ @classmethod
446
+ def from_gtf_df(
447
+ cls,
448
+ transcript_df: pd.DataFrame,
449
+ ignore_info: bool = True,
450
+ fix_truncation: bool = False,
451
+ ) -> 'Transcript':
452
+ """Initialises Trancript object from a given transcript dataframe.
453
+
454
+ Args:
455
+ transcript_df: Dataframe representing a transcript. The dataframe must
456
+ contain a single transcript.
457
+ ignore_info: If True, other columns in transcript_df won't be added to the
458
+ info field, except transcript_type and selenocysteines.
459
+ fix_truncation: Whether or not apply truncation fixation to CDS.
460
+
461
+ Returns:
462
+ Initialised Transcript object.
463
+ Raises:
464
+ ValueError: if the dataframe provided is invalid (no or more than one
465
+ transcript, transcript has inconsistent strand or chromosome,
466
+ transcript doesn't contain exons, CDS are not within exons, etc.)
467
+ """
468
+ if transcript_df.empty:
469
+ raise ValueError('transcript_df is empty')
470
+ if 'Feature' not in transcript_df:
471
+ raise ValueError('transcript_df must contain Feature column.')
472
+
473
+ if (
474
+ 'transcript_id' in transcript_df
475
+ and len(transcript_df.transcript_id.unique()) > 1
476
+ ):
477
+ raise ValueError('transcript_df should only contain a single transcript.')
478
+
479
+ # Convert rows to genome.Interval list.
480
+ transcript_df = transcript_df.sort_values(by='Start')
481
+ intervals_per_feature = collections.defaultdict(list)
482
+ exon_row = None
483
+ for _, row in transcript_df.iterrows():
484
+ interval = genome.Interval.from_pyranges_dict(
485
+ row, ignore_info=True
486
+ ) # pytype: disable=wrong-arg-types # pandas-drop-duplicates-overloads
487
+ if row.Feature in ['CDS', 'stop_codon']:
488
+ interval.info['frame'] = int(row.Frame)
489
+ if exon_row is None and row.Feature == 'exon':
490
+ exon_row = row
491
+ intervals_per_feature[row.Feature].append(interval)
492
+
493
+ if exon_row is None:
494
+ raise ValueError('transcript_df must contain at least one exon')
495
+
496
+ # Seed info.
497
+ if ignore_info:
498
+ info = {}
499
+ else:
500
+ skip = list(genome.PYRANGES_INTERVAL_COLUMNS) + [
501
+ 'Feature',
502
+ 'transcript_type',
503
+ 'Selenocysteines',
504
+ 'gene_type',
505
+ ]
506
+ info = {k: v for k, v in exon_row.items() if k not in skip}
507
+
508
+ if 'transcript_type' in exon_row:
509
+ info['transcript_type'] = exon_row['transcript_type']
510
+
511
+ if 'Selenocysteine' in intervals_per_feature:
512
+ info['selenocysteines'] = intervals_per_feature['Selenocysteine']
513
+
514
+ if 'gene_type' in exon_row:
515
+ info['gene_type'] = exon_row['gene_type']
516
+
517
+ transcript_obj = cls(
518
+ exons=intervals_per_feature['exon'],
519
+ cds=intervals_per_feature.get('CDS', None),
520
+ start_codon=intervals_per_feature.get('start_codon', None),
521
+ stop_codon=intervals_per_feature.get('stop_codon', None),
522
+ transcript_id=exon_row.get('transcript_id', None),
523
+ gene_id=exon_row.get('gene_id', None),
524
+ protein_id=exon_row.get('protein_id', None),
525
+ uniprot_id=exon_row.get('uniprot_id', None),
526
+ info=info,
527
+ )
528
+ if fix_truncation:
529
+ return Transcript.fix_truncation(transcript_obj)
530
+ return transcript_obj
531
+
532
+ @classmethod
533
+ def fix_truncation(cls, transcript: 'Transcript') -> 'Transcript':
534
+ """Fixes CDS start and stop positions to be within coding frame.
535
+
536
+ Args:
537
+ transcript: a transcript to fix.
538
+
539
+ Returns:
540
+ New transcript with set start/stop codons and fixed CDS if the total
541
+ length of CDS is > 6. Returns a copy of original transcript otherwise.
542
+ """
543
+ cds = sorted(
544
+ (transcript.cds or []) + (transcript.stop_codon or []),
545
+ key=lambda x: x.start,
546
+ )
547
+ cdna_len = sum(seq.width for seq in cds)
548
+ if cdna_len < 7:
549
+ return copy.deepcopy(transcript)
550
+
551
+ positive_strand = transcript.is_positive_strand
552
+ try:
553
+ frame = cds[0 if positive_strand else -1].info['frame']
554
+ except KeyError as key_error:
555
+ raise KeyError(
556
+ 'CDS intervals are missing frame information,'
557
+ ' truncations cannot be deduced.'
558
+ ) from key_error
559
+ frame_last = (cdna_len - frame) % 3
560
+
561
+ cds, start_codon = cls._fix_coding_frame(
562
+ five_prime=True,
563
+ beginning=positive_strand,
564
+ frame=frame,
565
+ cds_transcript=cds,
566
+ )
567
+
568
+ cds, stop_codon = cls._fix_coding_frame(
569
+ five_prime=False,
570
+ beginning=not positive_strand,
571
+ frame=frame_last,
572
+ cds_transcript=cds,
573
+ )
574
+
575
+ return cls(
576
+ exons=[exon.copy() for exon in transcript.exons],
577
+ cds=cds,
578
+ start_codon=start_codon,
579
+ stop_codon=stop_codon,
580
+ transcript_id=transcript.transcript_id,
581
+ gene_id=transcript.gene_id,
582
+ protein_id=transcript.protein_id,
583
+ uniprot_id=transcript.uniprot_id,
584
+ info={**transcript.info, 'truncation_fixed': True},
585
+ )
586
+
587
+ @classmethod
588
+ def _fix_coding_frame(
589
+ cls,
590
+ five_prime: bool,
591
+ beginning: bool,
592
+ frame: int,
593
+ cds_transcript: list[genome.Interval],
594
+ ) -> tuple[list[genome.Interval], list[genome.Interval]]:
595
+ """Fixes coding frame for a transcript and returns a new start/stop codon.
596
+
597
+ Args:
598
+ five_prime: a value indicating whether a 5' end to be fixed.
599
+ beginning: indicates whether the beginning or the end of the transript to
600
+ be fixed.
601
+ frame: a current coding frame (0, 1, 2). For fixing 5' end, it is a
602
+ position at which the first full codon starts within a cds. For fixing
603
+ 3' end, this indicates where the last full codon ends within a cds.
604
+ cds_transcript: a list of coding sequence intervals.
605
+
606
+ Returns:
607
+ A pair of lists where the first item is a list of CDS with fixed coding
608
+ frame and the second item is a start/stop codon.
609
+ """
610
+
611
+ def shorten_intervals(
612
+ cds_transcript: list[genome.Interval],
613
+ beginning: bool,
614
+ frame: int,
615
+ ) -> list[genome.Interval]:
616
+ cds = [interval.copy() for interval in cds_transcript]
617
+ index = 0 if beginning else -1
618
+ while frame > 0:
619
+ if cds[index].width > frame:
620
+ if beginning:
621
+ cds[index].start += frame
622
+ else:
623
+ cds[index].end -= frame
624
+ frame = 0
625
+ else:
626
+ frame -= cds[index].width
627
+ del cds[index]
628
+ return cds
629
+
630
+ # Fix CDS coding frame.
631
+ fixed_cds = shorten_intervals(cds_transcript, beginning, frame)
632
+
633
+ # Set codon.
634
+ codon_bases = 3
635
+ fixed_codon = []
636
+ for seq in fixed_cds if beginning else fixed_cds[::-1]:
637
+ if codon_bases == 0:
638
+ break
639
+ start, end = seq.start, seq.end
640
+ if seq.width > codon_bases and beginning:
641
+ end = seq.start + codon_bases
642
+ elif seq.width > codon_bases:
643
+ start = seq.end - codon_bases
644
+ interval = genome.Interval(seq.chromosome, start, end, strand=seq.strand)
645
+ fixed_codon.append(interval)
646
+ codon_bases -= interval.width
647
+
648
+ if five_prime:
649
+ fixed_cds[0 if beginning else -1].info['frame'] = 0
650
+ else:
651
+ # Remove newly set stop interval from cds.
652
+ fixed_cds = shorten_intervals(fixed_cds, beginning, 3)
653
+ return fixed_cds, fixed_codon
654
+
655
+
656
+ class _RangeExtractor:
657
+ """Range extractor from gtf df."""
658
+
659
+ def __init__(self, df: pd.DataFrame):
660
+ self._df_start_end = {
661
+ chromosome: (dfc, dfc['Start'].values, dfc['End'].values)
662
+ for chromosome, dfc in df.groupby('Chromosome')
663
+ }
664
+ self._df_empty = df.iloc[:0]
665
+
666
+ def extract(self, interval: genome.Interval) -> pd.DataFrame:
667
+ """Finds all rows that contain the input Interval.
668
+
669
+ Args:
670
+ interval: query Interval
671
+
672
+ Returns:
673
+ a dataframe containing genome intervals that contain the query Interval
674
+ """
675
+ if interval.chromosome not in self._df_start_end:
676
+ return self._df_empty
677
+ else:
678
+ dfc, start, end = self._df_start_end[interval.chromosome]
679
+ start_contained = (interval.start <= start) & (start <= interval.end)
680
+ end_contained = (interval.start <= end) & (end <= interval.end)
681
+ interval_contained = (interval.start >= start) & (end >= interval.end)
682
+ return dfc[start_contained | end_contained | interval_contained]
683
+
684
+
685
+ class TranscriptExtractor:
686
+ """Transcript extractor from gtf."""
687
+
688
+ def __init__(self, gtf_df: pd.DataFrame) -> None:
689
+ """Init.
690
+
691
+ Args:
692
+ gtf_df: pd.DataFrame of GENCODE GTF entries containing transcript
693
+ annotation. Must contain columns 'Chromosome', 'Start', 'End', 'Strand',
694
+ 'Feature', and 'transcript_id'.
695
+ """
696
+ self._transcript_extractor = _RangeExtractor(
697
+ gtf_df[gtf_df.Feature == 'transcript'][
698
+ ['Chromosome', 'Start', 'End', 'Strand', 'transcript_id']
699
+ ]
700
+ )
701
+ self._transcript_indexed_gtf = gtf_df.set_index('transcript_id')
702
+ self._transcript_from_id_cache = None
703
+
704
+ def cache_transcripts(self) -> None:
705
+ """Speed up extract() by converting GTF to dictionary of Transcripts.
706
+
707
+ This may take ca 11 minutes on the full human genome GTF of 84k protein
708
+ coding transcripts and 15 s on chr22 (1.5k transcripts).
709
+
710
+ Running cache_transcripts() will speed up .extract() by ca 5-10x:
711
+ (11 ms vs 65 ms tested on chr22, or 15 ms vs 160 ms on whole genome).
712
+ """
713
+ self._transcript_from_id_cache = self._transcripts_from_gtf(
714
+ self._transcript_indexed_gtf.reset_index()
715
+ )
716
+
717
+ def _transcripts_from_gtf(
718
+ self,
719
+ gtf_df: pd.DataFrame,
720
+ ) -> dict[str, Transcript]:
721
+ return (
722
+ { # pytype: disable=bad-return-type # pandas-drop-duplicates-overloads
723
+ transcript_id: (
724
+ Transcript.fix_truncation(
725
+ Transcript.from_gtf_df(gtf_subset, ignore_info=False)
726
+ )
727
+ )
728
+ for transcript_id, gtf_subset in gtf_df.groupby('transcript_id')
729
+ }
730
+ )
731
+
732
+ def extract(self, interval: genome.Interval) -> list[Transcript]:
733
+ """Extract transcripts overlapping an interval.
734
+
735
+ Args:
736
+ interval: Interval used to overlap with transcripts.
737
+
738
+ Returns:
739
+ List of transcript overlapping `interval`.
740
+ """
741
+ gtf_df_within_interval = self._transcript_extractor.extract(interval)
742
+ if gtf_df_within_interval.empty:
743
+ return []
744
+
745
+ transcript_ids = gtf_df_within_interval.transcript_id.dropna().unique()
746
+ if self._transcript_from_id_cache is not None:
747
+ return [
748
+ self._transcript_from_id_cache[transcript_id]
749
+ for transcript_id in transcript_ids
750
+ ]
751
+ else:
752
+ transcript_gtfs = self._transcript_indexed_gtf.loc[
753
+ transcript_ids
754
+ ].reset_index()
755
+ return list(self._transcripts_from_gtf(transcript_gtfs).values())
flax_model/alphagenome/_sdk/interpretation/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Library of tools for interpreting sequences and model predictions."""
flax_model/alphagenome/_sdk/interpretation/ism.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """In-silico mutagenesis (ISM) functions for sequence interpretation."""
15
+
16
+ from collections.abc import Sequence
17
+
18
+ from flax_model.alphagenome._sdk.data import genome
19
+ import numpy as np
20
+
21
+
22
+ def ism_variants(
23
+ interval: genome.Interval,
24
+ sequence: str,
25
+ vocabulary: str = 'ACGT',
26
+ skip_n: bool = False,
27
+ ) -> list[genome.Variant]:
28
+ """Create a list of all possible single nucleotide variants for an interval.
29
+
30
+ Args:
31
+ interval: Interval for which to generate the variants.
32
+ sequence: Sequence extracted from the reference genome at the `interval`.
33
+ Needs to have the same length as `interval.width`.
34
+ vocabulary: Vocabulary of possible alternative bases contained in
35
+ `sequence`.
36
+ skip_n: If True, skip the N bases.
37
+
38
+ Returns:
39
+ List of all possible single nucleotide variants for a genomic interval.
40
+ """
41
+ if len(sequence) != interval.width:
42
+ raise ValueError('Sequence must have the same length as interval.')
43
+ variants = []
44
+ for position in range(interval.width):
45
+ reference_base = sequence[position]
46
+ if skip_n and reference_base == 'N':
47
+ continue
48
+ for alternative_base in vocabulary:
49
+ if reference_base == alternative_base:
50
+ continue
51
+ variants.append(
52
+ genome.Variant(
53
+ chromosome=interval.chromosome,
54
+ reference_bases=reference_base,
55
+ alternate_bases=alternative_base,
56
+ position=interval.start + position + 1,
57
+ )
58
+ )
59
+ return variants
60
+
61
+
62
+ def ism_matrix(
63
+ variant_scores: Sequence[float],
64
+ variants: Sequence[genome.Variant],
65
+ interval: genome.Interval | None = None,
66
+ multiply_by_sequence: bool = True,
67
+ vocabulary: str = 'ACGT',
68
+ require_fully_filled: bool = True,
69
+ ) -> np.ndarray:
70
+ """Construct the ISM (position, base) matrix from individual ISM scores.
71
+
72
+ This function returns the relative effect of the variants compared to the
73
+ per-position average: score[position, base] - mean(score[position, :]]).
74
+
75
+ Args:
76
+ variant_scores: Variant effect scores corresponding to the variants. These
77
+ could be obtained from the score_variants() output summarised to a single
78
+ scalar. Summarisation could be for example be obtained by selecting a
79
+ specific variant scorer output and extract a specific value from the
80
+ (variant, track) matrix.
81
+ variants: Sequence of variants used to transform into the ISM matrix.
82
+ interval: Interval for which to get the contribution scores. All variants
83
+ need to be contained within that interval. If None, it will be
84
+ automatically inferred from variants.
85
+ multiply_by_sequence: If True, only return non-zero values at
86
+ one-hot-encoded reference genome sequence bases.
87
+ vocabulary: Vocabulary of possible alternative bases contained in
88
+ `sequence`. The order determines the column order of the returned matrix.
89
+ require_fully_filled: If True, raise an error if not all positions are
90
+ covered by variants.
91
+
92
+ Returns:
93
+ Matrix of shape (interval.width, 4) containing variant scores.
94
+ """
95
+ if len(variants) != len(variant_scores):
96
+ raise ValueError(
97
+ 'Variants and variant_scores need to have the same length.'
98
+ )
99
+ if interval is None:
100
+ interval = genome.Interval(
101
+ chromosome=variants[0].chromosome,
102
+ start=min(variant.start for variant in variants),
103
+ end=max(variant.end for variant in variants),
104
+ )
105
+ scores = np.zeros((interval.width, 4), dtype=np.float32)
106
+ filled = np.zeros((interval.width, 4), dtype=bool)
107
+ base_index = {base: i for i, base in enumerate(vocabulary)}
108
+
109
+ for variant, score in zip(variants, variant_scores, strict=True):
110
+ if len(variant.alternate_bases) != 1 or len(variant.reference_bases) != 1:
111
+ # Only looking for single nucleotide variants.
112
+ continue
113
+ if not interval.contains(variant.reference_interval):
114
+ continue
115
+ position = variant.start - interval.start
116
+ scores[position, base_index[variant.alternate_bases]] = score
117
+ filled[position, base_index[variant.alternate_bases]] = True
118
+
119
+ # Check that all positions were covered by variants.
120
+ if (filled.sum(axis=-1) == 0).any() and require_fully_filled:
121
+ missing_positions = list(
122
+ np.where(filled.sum(axis=-1) == 0)[0] + interval.start + 1
123
+ )
124
+ raise ValueError(
125
+ 'No variants were found for the (1-based) positions on chromosome '
126
+ f'{interval.chromosome}: {missing_positions}'
127
+ )
128
+ elif (filled.sum(axis=-1) == 4).any():
129
+ full_positions = list(
130
+ np.where(filled.sum(axis=-1) == 4)[0] + interval.start + 1
131
+ )
132
+ raise ValueError(
133
+ 'Some variants were found with 4 different alternative bases for the'
134
+ ' position instead of 3. List of positions on chromosome '
135
+ f'{interval.chromosome}: {full_positions}'
136
+ )
137
+ elif (filled.sum(axis=-1) != 3).any() and require_fully_filled:
138
+ missing_positions = list(
139
+ np.where(filled.sum(axis=-1) != 3)[0] + interval.start + 1
140
+ )
141
+ raise ValueError(
142
+ 'Some variants were found with only 1 or 2 alternative bases for the'
143
+ ' position instead of 3. List of positions on chromosome '
144
+ f'{interval.chromosome}: {missing_positions}'
145
+ )
146
+
147
+ scores -= np.sum(scores, axis=-1, keepdims=True) / (len(vocabulary) - 1)
148
+ if multiply_by_sequence:
149
+ scores = scores * (~filled)
150
+ return scores
flax_model/alphagenome/_sdk/models/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Models library for interacting with AlphaGenome models."""
flax_model/alphagenome/_sdk/models/dna_client.py ADDED
@@ -0,0 +1,907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Client implementation for interacting with a DNA model server."""
16
+
17
+ from collections.abc import Container, Iterable, Iterator, Mapping, Sequence
18
+ import concurrent.futures
19
+ import functools
20
+ import random
21
+ import time
22
+ from typing import TypeVar
23
+
24
+ from flax_model.alphagenome._sdk import tensor_utils
25
+ from flax_model.alphagenome._sdk.data import genome
26
+ from flax_model.alphagenome._sdk.data import junction_data
27
+ from flax_model.alphagenome._sdk.data import ontology
28
+ from flax_model.alphagenome._sdk.data import track_data
29
+ from flax_model.alphagenome._sdk.models import dna_model
30
+ from flax_model.alphagenome._sdk.models import dna_output
31
+ from flax_model.alphagenome._sdk.models import interval_scorers as interval_scorers_lib
32
+ from flax_model.alphagenome._sdk.models import junction_data_utils
33
+ from flax_model.alphagenome._sdk.models import track_data_utils
34
+ from flax_model.alphagenome._sdk.models import variant_scorers as variant_scorers_lib
35
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
36
+ from flax_model.alphagenome._sdk.protos import dna_model_service_pb2
37
+ from flax_model.alphagenome._sdk.protos import dna_model_service_pb2_grpc
38
+ from flax_model.alphagenome._sdk.protos import tensor_pb2
39
+ import anndata
40
+ import grpc
41
+ import numpy as np
42
+ import pandas as pd
43
+ import tqdm.auto
44
+
45
+
46
+ # Supported DNA sequence lengths.
47
+ SEQUENCE_LENGTH_16KB = 2**14 # 16_384
48
+ SEQUENCE_LENGTH_100KB = 2**17 # 131_072
49
+ SEQUENCE_LENGTH_500KB = 2**19 # 524_288
50
+ SEQUENCE_LENGTH_1MB = 2**20 # 1_048_576
51
+
52
+ SUPPORTED_SEQUENCE_LENGTHS = {
53
+ 'SEQUENCE_LENGTH_16KB': SEQUENCE_LENGTH_16KB,
54
+ 'SEQUENCE_LENGTH_100KB': SEQUENCE_LENGTH_100KB,
55
+ 'SEQUENCE_LENGTH_500KB': SEQUENCE_LENGTH_500KB,
56
+ 'SEQUENCE_LENGTH_1MB': SEQUENCE_LENGTH_1MB,
57
+ }
58
+
59
+ # Maximum width of an in silico mutagenesis (ISM) interval request.
60
+ # This controls the behavior of `score_ism_variants` in the client.
61
+ # When a user inputs an ISM interval that is wider than this value,
62
+ # the client will automatically split the interval into chunks of this width.
63
+ MAX_ISM_INTERVAL_WIDTH = 10
64
+
65
+ # Maximum number of variant scorers per request.
66
+ MAX_VARIANT_SCORERS_PER_REQUEST = 20
67
+
68
+ _VALID_SEQUENCE_CHARACTERS = frozenset('ACGTN')
69
+
70
+ RetryableFunction = TypeVar('RetryableFunction')
71
+
72
+
73
+ def retry_rpc(
74
+ function: RetryableFunction,
75
+ *,
76
+ max_attempts: int = 5,
77
+ initial_backoff: float = 1.25,
78
+ backoff_multiplier: float = 1.5,
79
+ retry_status_codes: Container[grpc.StatusCode] = frozenset(
80
+ [grpc.StatusCode.RESOURCE_EXHAUSTED, grpc.StatusCode.UNAVAILABLE]
81
+ ),
82
+ jitter: float = 0.2,
83
+ ) -> RetryableFunction:
84
+ """Decorator that retries when an RPC fails.
85
+
86
+ gRPC currently doesn't support retries for streaming RPCs. This decorator is
87
+ an implementation of the retry logic from https://grpc.io/docs/guides/retry.
88
+
89
+ Arguments:
90
+ function: Callable to retry.
91
+ max_attempts: Maximum number of attempts to make.
92
+ initial_backoff: Initial backoff time in seconds.
93
+ backoff_multiplier: Backoff multiplier to apply on each retry.
94
+ retry_status_codes: Set of status codes to retry on.
95
+ jitter: Jitter to apply to the backoff time. For example, a jitter of 0.2
96
+ and an initial backoff of 1.5 will result in a backoff time between 1.2
97
+ and 1.8 seconds.
98
+
99
+ Returns:
100
+ A decorator that retries the function on retryable RPC errors.
101
+ """
102
+ if backoff_multiplier < 1.0:
103
+ raise ValueError('Backoff multiplier must be >= 1.0.')
104
+
105
+ @functools.wraps(function)
106
+ def wrapper(*args, **kwargs):
107
+ attempt = 0
108
+ current_backoff = initial_backoff + (
109
+ random.uniform(-jitter, jitter) * initial_backoff
110
+ )
111
+ while True:
112
+ try:
113
+ attempt += 1
114
+ return function(*args, **kwargs)
115
+ except grpc.RpcError as e:
116
+ error_code = e.code() # pytype: disable=attribute-error
117
+ if error_code not in retry_status_codes or attempt >= max_attempts:
118
+ raise e
119
+ time.sleep(current_backoff)
120
+ current_backoff *= backoff_multiplier
121
+
122
+ return wrapper
123
+
124
+
125
+ ModelVersion = dna_model.ModelVersion
126
+ Output = dna_output.Output
127
+ OutputMetadata = dna_output.OutputMetadata
128
+ OutputType = dna_output.OutputType
129
+ VariantOutput = dna_output.VariantOutput
130
+ Organism = dna_model.Organism
131
+
132
+ PredictResponse = TypeVar(
133
+ 'PredictResponse',
134
+ dna_model_service_pb2.PredictSequenceResponse,
135
+ dna_model_service_pb2.PredictIntervalResponse,
136
+ )
137
+
138
+
139
+ def _read_tensor_chunks(
140
+ responses: Iterator[
141
+ PredictResponse
142
+ | dna_model_service_pb2.PredictVariantResponse
143
+ | dna_model_service_pb2.ScoreVariantResponse
144
+ | dna_model_service_pb2.ScoreIntervalResponse
145
+ ],
146
+ chunk_count: int,
147
+ ) -> Iterable[tensor_pb2.TensorChunk]:
148
+ """Helper to read a sequence of tensor chunks from a response iterator."""
149
+ for _ in range(chunk_count):
150
+ try:
151
+ response = next(responses)
152
+ except StopIteration as e:
153
+ raise ValueError('Expected tensor_chunk, got end of stream') from e
154
+ if response.WhichOneof('payload') != 'tensor_chunk':
155
+ raise ValueError(
156
+ f'Expected tensor_chunk, got "{response.WhichOneof("payload")}"'
157
+ ' payload'
158
+ )
159
+ yield response.tensor_chunk
160
+
161
+
162
+ def _make_output_data(
163
+ output: dna_model_pb2.Output,
164
+ responses: Iterator[
165
+ PredictResponse | dna_model_service_pb2.PredictVariantResponse
166
+ ],
167
+ interval: genome.Interval | None = None,
168
+ ) -> track_data.TrackData | np.ndarray | junction_data.JunctionData:
169
+ """Helper to create an output data object from an output proto."""
170
+ match output.WhichOneof('payload'):
171
+ case 'track_data':
172
+ return track_data_utils.from_protos(
173
+ output.track_data,
174
+ _read_tensor_chunks(responses, output.track_data.values.chunk_count),
175
+ interval=interval,
176
+ )
177
+ case 'data':
178
+ chunks = _read_tensor_chunks(responses, output.data.chunk_count)
179
+ values = tensor_utils.unpack_proto(output.data, chunks)
180
+ return tensor_utils.upcast_floating(values)
181
+ case 'junction_data':
182
+ return junction_data_utils.from_protos(
183
+ output.junction_data,
184
+ _read_tensor_chunks(
185
+ responses, output.junction_data.values.chunk_count
186
+ ),
187
+ interval=interval,
188
+ )
189
+ case _:
190
+ raise ValueError(
191
+ f'Unsupported output type: {output.WhichOneof("payload")}'
192
+ )
193
+
194
+
195
+ def construct_output_metadata(
196
+ responses: Iterator[dna_model_service_pb2.MetadataResponse],
197
+ ) -> OutputMetadata:
198
+ """Constructs an OutputMetadata from a stream of responses."""
199
+ metadata = {}
200
+ for response in responses:
201
+ for metadata_proto in response.output_metadata:
202
+ match metadata_proto.WhichOneof('payload'):
203
+ case 'tracks':
204
+ metadata[metadata_proto.output_type] = (
205
+ track_data_utils.metadata_from_proto(metadata_proto.tracks)
206
+ )
207
+ case 'junctions':
208
+ metadata[metadata_proto.output_type] = (
209
+ junction_data_utils.metadata_from_proto(metadata_proto.junctions)
210
+ )
211
+ case _:
212
+ raise ValueError(
213
+ 'Unsupported metadata type:'
214
+ f' {metadata_proto.WhichOneof("payload")}'
215
+ )
216
+
217
+ return OutputMetadata(
218
+ atac=metadata.get(dna_model_pb2.OUTPUT_TYPE_ATAC),
219
+ cage=metadata.get(dna_model_pb2.OUTPUT_TYPE_CAGE),
220
+ dnase=metadata.get(dna_model_pb2.OUTPUT_TYPE_DNASE),
221
+ rna_seq=metadata.get(dna_model_pb2.OUTPUT_TYPE_RNA_SEQ),
222
+ chip_histone=metadata.get(dna_model_pb2.OUTPUT_TYPE_CHIP_HISTONE),
223
+ chip_tf=metadata.get(dna_model_pb2.OUTPUT_TYPE_CHIP_TF),
224
+ splice_sites=metadata.get(dna_model_pb2.OUTPUT_TYPE_SPLICE_SITES),
225
+ splice_site_usage=metadata.get(
226
+ dna_model_pb2.OUTPUT_TYPE_SPLICE_SITE_USAGE
227
+ ),
228
+ splice_junctions=metadata.get(dna_model_pb2.OUTPUT_TYPE_SPLICE_JUNCTIONS),
229
+ contact_maps=metadata.get(dna_model_pb2.OUTPUT_TYPE_CONTACT_MAPS),
230
+ procap=metadata.get(dna_model_pb2.OUTPUT_TYPE_PROCAP),
231
+ )
232
+
233
+
234
+ def _construct_output(
235
+ output_dict: Mapping[
236
+ dna_model_pb2.OutputType,
237
+ track_data.TrackData | np.ndarray | junction_data.JunctionData,
238
+ ],
239
+ ):
240
+ """Helper to construct an Output dataclass from a mapping of output types."""
241
+ output = Output(
242
+ atac=output_dict.get(dna_model_pb2.OUTPUT_TYPE_ATAC),
243
+ cage=output_dict.get(dna_model_pb2.OUTPUT_TYPE_CAGE),
244
+ dnase=output_dict.get(dna_model_pb2.OUTPUT_TYPE_DNASE),
245
+ rna_seq=output_dict.get(dna_model_pb2.OUTPUT_TYPE_RNA_SEQ),
246
+ chip_histone=output_dict.get(dna_model_pb2.OUTPUT_TYPE_CHIP_HISTONE),
247
+ chip_tf=output_dict.get(dna_model_pb2.OUTPUT_TYPE_CHIP_TF),
248
+ splice_sites=output_dict.get(dna_model_pb2.OUTPUT_TYPE_SPLICE_SITES),
249
+ splice_site_usage=output_dict.get(
250
+ dna_model_pb2.OUTPUT_TYPE_SPLICE_SITE_USAGE
251
+ ),
252
+ splice_junctions=output_dict.get(
253
+ dna_model_pb2.OUTPUT_TYPE_SPLICE_JUNCTIONS
254
+ ),
255
+ contact_maps=output_dict.get(dna_model_pb2.OUTPUT_TYPE_CONTACT_MAPS),
256
+ procap=output_dict.get(dna_model_pb2.OUTPUT_TYPE_PROCAP),
257
+ )
258
+ return output
259
+
260
+
261
+ def _make_output(
262
+ responses: Iterator[PredictResponse],
263
+ *,
264
+ interval: genome.Interval | None = None,
265
+ ) -> Output:
266
+ """Helper to load an Output dataclass from a stream of response protos."""
267
+ outputs = {}
268
+
269
+ for response in responses:
270
+ match response.WhichOneof('payload'):
271
+ case 'output':
272
+ outputs[response.output.output_type] = _make_output_data(
273
+ response.output, responses, interval=interval
274
+ )
275
+ case 'tensor_chunk':
276
+ raise ValueError('Received tensor chunk before output proto.')
277
+ case _:
278
+ raise ValueError(
279
+ f'Unsupported response type: {response.WhichOneof("payload")}'
280
+ )
281
+
282
+ return _construct_output(outputs)
283
+
284
+
285
+ def _make_variant_output(
286
+ responses: Iterator[dna_model_service_pb2.PredictVariantResponse],
287
+ ) -> VariantOutput:
288
+ """Helper to load an Output dataclass from a stream of response protos."""
289
+ ref_outputs = {}
290
+ alt_outputs = {}
291
+
292
+ for response in responses:
293
+ match response.WhichOneof('payload'):
294
+ case 'reference_output':
295
+ ref_outputs[response.reference_output.output_type] = _make_output_data(
296
+ response.reference_output, responses
297
+ )
298
+ case 'alternate_output':
299
+ alt_outputs[response.alternate_output.output_type] = _make_output_data(
300
+ response.alternate_output, responses
301
+ )
302
+ case 'tensor_chunk':
303
+ raise ValueError('Received tensor chunk before output proto.')
304
+ case _:
305
+ raise ValueError(
306
+ f'Unsupported response type: {response.WhichOneof("payload")}'
307
+ )
308
+
309
+ return VariantOutput(
310
+ reference=_construct_output(ref_outputs),
311
+ alternate=_construct_output(alt_outputs),
312
+ )
313
+
314
+
315
+ def _construct_anndata_from_proto(
316
+ scorer_metadata: Sequence[dna_model_pb2.GeneScorerMetadata] | None,
317
+ track_metadata: Sequence[dna_model_pb2.TrackMetadata],
318
+ values: np.ndarray,
319
+ interval: genome.Interval,
320
+ variant: dna_model_pb2.Variant | None = None,
321
+ ) -> anndata.AnnData:
322
+ """Helper to construct `AnnData` from a scoring proto."""
323
+ obs = None
324
+ if scorer_metadata:
325
+ metadata = []
326
+ for gene_proto in scorer_metadata:
327
+ scorer_metadata = {
328
+ 'gene_id': gene_proto.gene_id,
329
+ }
330
+ if gene_proto.HasField('strand'):
331
+ scorer_metadata['strand'] = str(
332
+ genome.Strand.from_proto(gene_proto.strand)
333
+ )
334
+
335
+ if gene_proto.HasField('name'):
336
+ scorer_metadata['gene_name'] = gene_proto.name
337
+ if gene_proto.HasField('type'):
338
+ scorer_metadata['gene_type'] = gene_proto.type
339
+ if gene_proto.HasField('junction_start'):
340
+ scorer_metadata['junction_Start'] = gene_proto.junction_start
341
+ if gene_proto.HasField('junction_end'):
342
+ scorer_metadata['junction_End'] = gene_proto.junction_end
343
+
344
+ metadata.append(scorer_metadata)
345
+
346
+ obs = pd.DataFrame(metadata)
347
+ obs.index = obs.index.map(str)
348
+
349
+ var = track_data_utils.metadata_from_proto(
350
+ dna_model_pb2.TracksMetadata(metadata=track_metadata)
351
+ )
352
+ var.index = var.index.map(str)
353
+
354
+ uns = {'interval': interval}
355
+ if variant is not None:
356
+ uns['variant'] = genome.Variant.from_proto(variant)
357
+ layers = None
358
+ if values.shape[0] > 1:
359
+ layers = {'quantiles': values[1]}
360
+
361
+ return anndata.AnnData(
362
+ X=values[0],
363
+ obs=obs,
364
+ var=var,
365
+ uns=uns,
366
+ layers=layers,
367
+ )
368
+
369
+
370
+ def _construct_score_variant(
371
+ output: dna_model_pb2.ScoreVariantOutput,
372
+ responses: Iterator[
373
+ dna_model_service_pb2.ScoreVariantResponse
374
+ | dna_model_service_pb2.ScoreIsmVariantResponse
375
+ ],
376
+ interval: genome.Interval,
377
+ ) -> anndata.AnnData:
378
+ """Returns `AnnData` of variant scores from a ScoreVariantOutput proto."""
379
+ # dna_model_pb2.ScoreVariantOutput currently has ONLY VariantData with values
380
+ # and metadata.
381
+ chunks = _read_tensor_chunks(
382
+ responses, output.variant_data.values.chunk_count
383
+ )
384
+ values = tensor_utils.unpack_proto(output.variant_data.values, chunks)
385
+ values = tensor_utils.upcast_floating(values)
386
+ anndata_scores = _construct_anndata_from_proto(
387
+ output.variant_data.metadata.gene_metadata,
388
+ output.variant_data.metadata.track_metadata,
389
+ values,
390
+ interval=interval,
391
+ variant=output.variant_data.metadata.variant,
392
+ )
393
+ return anndata_scores
394
+
395
+
396
+ def _make_score_variant_output(
397
+ responses: Iterator[
398
+ dna_model_service_pb2.ScoreVariantResponse
399
+ | dna_model_service_pb2.ScoreIsmVariantResponse
400
+ ],
401
+ interval: genome.Interval,
402
+ ) -> list[anndata.AnnData]:
403
+ """Returns a list of `AnnData` variant scores from a stream of responses."""
404
+
405
+ anndata_scores = []
406
+ for response in responses:
407
+ match response.WhichOneof('payload'):
408
+ case 'output':
409
+ anndata_scores.append(
410
+ _construct_score_variant(
411
+ response.output, responses, interval=interval
412
+ )
413
+ )
414
+ case 'tensor_chunk':
415
+ raise ValueError('Received tensor chunk before output proto.')
416
+ case _:
417
+ raise ValueError(
418
+ f'Unsupported response type: {response.WhichOneof("payload")}'
419
+ )
420
+ return anndata_scores
421
+
422
+
423
+ def _construct_score_interval(
424
+ output: dna_model_pb2.ScoreIntervalOutput,
425
+ responses: Iterator[dna_model_service_pb2.ScoreIntervalResponse],
426
+ interval: genome.Interval,
427
+ ) -> anndata.AnnData:
428
+ """Returns `AnnData` of interval scores from a ScoreIntervalOutput proto."""
429
+ chunks = _read_tensor_chunks(
430
+ responses, output.interval_data.values.chunk_count
431
+ )
432
+ values = tensor_utils.unpack_proto(output.interval_data.values, chunks)
433
+ values = tensor_utils.upcast_floating(values)
434
+ anndata_scores = _construct_anndata_from_proto(
435
+ output.interval_data.metadata.gene_metadata,
436
+ output.interval_data.metadata.track_metadata,
437
+ values,
438
+ interval=interval,
439
+ )
440
+ return anndata_scores
441
+
442
+
443
+ def _make_interval_output(
444
+ responses: Iterator[dna_model_service_pb2.ScoreIntervalResponse],
445
+ interval: genome.Interval,
446
+ ) -> list[anndata.AnnData]:
447
+ """Returns a list of `AnnData` interval scores from a stream of responses."""
448
+
449
+ anndata_scores = []
450
+ for response in responses:
451
+ match response.WhichOneof('payload'):
452
+ case 'output':
453
+ anndata_scores.append(
454
+ _construct_score_interval(
455
+ response.output,
456
+ responses,
457
+ interval=interval,
458
+ )
459
+ )
460
+ case 'tensor_chunk':
461
+ raise ValueError('Received tensor chunk before output proto.')
462
+ case _:
463
+ raise ValueError(
464
+ f'Unsupported response type: {response.WhichOneof("payload")}'
465
+ )
466
+ return anndata_scores
467
+
468
+
469
+ def _convert_ontologies_to_protos(
470
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
471
+ ) -> Sequence[dna_model_pb2.OntologyTerm] | None:
472
+ """Convert ontology terms or curies to unique list of OntologyTerm protos."""
473
+ if ontology_terms is None:
474
+ return None
475
+ else:
476
+ protos = []
477
+ for term in dict.fromkeys(ontology_terms):
478
+ if isinstance(term, ontology.OntologyTerm):
479
+ protos.append(term.to_proto())
480
+ else:
481
+ protos.append(ontology.from_curie(term).to_proto())
482
+ return protos
483
+
484
+
485
+ def validate_sequence_length(length: int):
486
+ """Validate that the input sequence length is supported by the model."""
487
+ if length not in SUPPORTED_SEQUENCE_LENGTHS.values():
488
+ raise ValueError(
489
+ f'Sequence length {length} not supported by the model.'
490
+ f' Supported lengths: {[*SUPPORTED_SEQUENCE_LENGTHS.values()]}'
491
+ )
492
+
493
+
494
+ class DnaClient(dna_model.DnaModel):
495
+ """Client for interacting with a DNA model server.
496
+
497
+ Attributes:
498
+ channel: gRPC channel to the DNA model server.
499
+ metadata: Metadata to send with each request.
500
+ model_version: Optional model version to use for the DNA model server. If
501
+ none provided, the default model version will be used.
502
+ """
503
+
504
+ def __init__(
505
+ self,
506
+ *,
507
+ channel: grpc.Channel,
508
+ model_version: ModelVersion | None = None,
509
+ metadata: Sequence[tuple[str, str]] = (),
510
+ ):
511
+ self._channel = channel
512
+ self._metadata = metadata
513
+ self._model_version = (
514
+ model_version.name if model_version is not None else None
515
+ )
516
+
517
+ @retry_rpc
518
+ def predict_sequence(
519
+ self,
520
+ sequence: str,
521
+ *,
522
+ organism: Organism = Organism.HOMO_SAPIENS,
523
+ requested_outputs: Iterable[OutputType],
524
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
525
+ interval: genome.Interval | None = None,
526
+ ) -> Output:
527
+ """Generate predictions for a given DNA sequence.
528
+
529
+ Args:
530
+ sequence: DNA sequence to make prediction for.
531
+ organism: Organism to use for the prediction.
532
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
533
+ predictions to return.
534
+ ontology_terms: Iterable of ontology terms or curies to generate
535
+ predictions for. If None returns all ontologies.
536
+ interval: Optional interval from which the sequence was derived. This is
537
+ used as the interval in the output TrackData.
538
+
539
+ Returns:
540
+ Output for the provided DNA sequence.
541
+ """
542
+ if not (unique_values := set(sequence)).issubset(
543
+ _VALID_SEQUENCE_CHARACTERS
544
+ ):
545
+ invalid_characters = ','.join(unique_values - _VALID_SEQUENCE_CHARACTERS)
546
+ raise ValueError(
547
+ 'Invalid sequence, must only contain the characters "ACGTN". Found'
548
+ f' invalid characters: "{invalid_characters}"'
549
+ )
550
+ validate_sequence_length(len(sequence))
551
+ requested_outputs = [o.to_proto() for o in dict.fromkeys(requested_outputs)]
552
+ request = dna_model_service_pb2.PredictSequenceRequest(
553
+ sequence=sequence,
554
+ organism=organism.to_proto(),
555
+ ontology_terms=_convert_ontologies_to_protos(ontology_terms),
556
+ requested_outputs=requested_outputs,
557
+ model_version=self._model_version,
558
+ )
559
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
560
+ self._channel
561
+ ).PredictSequence(iter([request]), metadata=self._metadata)
562
+ return _make_output(responses, interval=interval)
563
+
564
+ @retry_rpc
565
+ def predict_interval(
566
+ self,
567
+ interval: genome.Interval,
568
+ *,
569
+ organism: Organism = Organism.HOMO_SAPIENS,
570
+ requested_outputs: Iterable[OutputType],
571
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
572
+ ) -> Output:
573
+ """Generate predictions for a given DNA interval.
574
+
575
+ Args:
576
+ interval: DNA interval to make prediction for.
577
+ organism: Organism to use for the prediction.
578
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
579
+ predictions to return.
580
+ ontology_terms: Iterable of ontology terms or curies to generate
581
+ predictions for. If None returns all ontologies.
582
+
583
+ Returns:
584
+ Output for the provided DNA interval.
585
+ """
586
+ validate_sequence_length(interval.width)
587
+ requested_outputs = [o.to_proto() for o in dict.fromkeys(requested_outputs)]
588
+ request = dna_model_service_pb2.PredictIntervalRequest(
589
+ interval=interval.to_proto(),
590
+ organism=organism.to_proto(),
591
+ ontology_terms=_convert_ontologies_to_protos(ontology_terms),
592
+ requested_outputs=requested_outputs,
593
+ model_version=self._model_version,
594
+ )
595
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
596
+ self._channel
597
+ ).PredictInterval(iter([request]), metadata=self._metadata)
598
+ return _make_output(responses)
599
+
600
+ @retry_rpc
601
+ def predict_variant(
602
+ self,
603
+ interval: genome.Interval,
604
+ variant: genome.Variant,
605
+ *,
606
+ organism: Organism = Organism.HOMO_SAPIENS,
607
+ requested_outputs: Iterable[OutputType],
608
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
609
+ ) -> VariantOutput:
610
+ """Generate predictions for a given DNA variant.
611
+
612
+ Args:
613
+ interval: DNA interval to make prediction for.
614
+ variant: DNA variant to make prediction for.
615
+ organism: Organism to use for the prediction.
616
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
617
+ predictions to return.
618
+ ontology_terms: Iterable of ontology terms or curies to generate
619
+ predictions for. If None returns all ontologies.
620
+
621
+ Returns:
622
+ Variant output for the provided DNA interval and variant.
623
+ """
624
+ validate_sequence_length(interval.width)
625
+ requested_outputs = [o.to_proto() for o in dict.fromkeys(requested_outputs)]
626
+ request = dna_model_service_pb2.PredictVariantRequest(
627
+ interval=interval.to_proto(),
628
+ variant=variant.to_proto(),
629
+ organism=organism.to_proto(),
630
+ ontology_terms=_convert_ontologies_to_protos(ontology_terms),
631
+ requested_outputs=requested_outputs,
632
+ model_version=self._model_version,
633
+ )
634
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
635
+ self._channel
636
+ ).PredictVariant(iter([request]), metadata=self._metadata)
637
+ return _make_variant_output(responses)
638
+
639
+ @retry_rpc
640
+ def score_interval(
641
+ self,
642
+ interval: genome.Interval,
643
+ interval_scorers: Sequence[interval_scorers_lib.IntervalScorerTypes] = (),
644
+ *,
645
+ organism: Organism = Organism.HOMO_SAPIENS,
646
+ ) -> list[anndata.AnnData]:
647
+ """Generate interval scores for a single given interval.
648
+
649
+ Args:
650
+ interval: Interval to make prediction for.
651
+ interval_scorers: Sequence of interval scorers to use for scoring. If no
652
+ interval scorers are provided, the recommended interval scorers for the
653
+ organism will be used.
654
+ organism: Organism to use for the prediction.
655
+
656
+ Returns:
657
+ List of `AnnData` interval scores.
658
+ """
659
+ if not interval_scorers:
660
+ interval_scorers = list(
661
+ interval_scorers_lib.RECOMMENDED_INTERVAL_SCORERS.values()
662
+ )
663
+
664
+ validate_sequence_length(interval.width)
665
+ if len(interval_scorers) != len(set(interval_scorers)):
666
+ raise ValueError(
667
+ f'Duplicate interval scorers requested: {interval_scorers}.'
668
+ )
669
+ if len(interval_scorers) > MAX_VARIANT_SCORERS_PER_REQUEST:
670
+ raise ValueError(
671
+ 'Too many interval scorers requested, '
672
+ f'maximum allowed: {MAX_VARIANT_SCORERS_PER_REQUEST}.'
673
+ )
674
+ if any(
675
+ scorer.width is not None and scorer.width > interval.width
676
+ for scorer in interval_scorers
677
+ ):
678
+ raise ValueError('Interval scorers must have widths <= interval width.')
679
+ request = dna_model_service_pb2.ScoreIntervalRequest(
680
+ interval=interval.to_proto(),
681
+ interval_scorers=[scorer.to_proto() for scorer in interval_scorers],
682
+ organism=organism.to_proto(),
683
+ model_version=self._model_version,
684
+ )
685
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
686
+ self._channel
687
+ ).ScoreInterval(iter([request]), metadata=self._metadata)
688
+ interval_outputs = _make_interval_output(responses, interval)
689
+ for ix in range(len(interval_scorers)):
690
+ interval_outputs[ix].uns['interval_scorer'] = interval_scorers[ix]
691
+ return interval_outputs
692
+
693
+ @retry_rpc
694
+ def score_variant(
695
+ self,
696
+ interval: genome.Interval,
697
+ variant: genome.Variant,
698
+ variant_scorers: Sequence[variant_scorers_lib.VariantScorerTypes] = (),
699
+ *,
700
+ organism: Organism = Organism.HOMO_SAPIENS,
701
+ ) -> list[anndata.AnnData]:
702
+ """Generate variant scores for a single given DNA variant.
703
+
704
+ Args:
705
+ interval: DNA interval to make prediction for.
706
+ variant: DNA variant to make prediction for.
707
+ variant_scorers: Sequence of variant scorers to use for scoring. If no
708
+ variant scorers are provided, the recommended variant scorers for the
709
+ organism will be used.
710
+ organism: Organism to use for the prediction.
711
+
712
+ Returns:
713
+ List of `AnnData` variant scores.
714
+ """
715
+ if not variant_scorers:
716
+ variant_scorers = variant_scorers_lib.get_recommended_scorers(
717
+ organism.to_proto()
718
+ )
719
+
720
+ validate_sequence_length(interval.width)
721
+ for variant_scorer in variant_scorers:
722
+ supported_organisms = variant_scorers_lib.SUPPORTED_ORGANISMS[
723
+ variant_scorer.base_variant_scorer
724
+ ]
725
+ if organism.to_proto() not in supported_organisms:
726
+ supported_organisms = [
727
+ dna_model_pb2.Organism.Name(o).removeprefix('ORGANISM_')
728
+ for o in supported_organisms
729
+ ]
730
+ raise ValueError(
731
+ f'Unsupported organism: {organism.name} for scorer'
732
+ f' {variant_scorer}. Supported organisms:'
733
+ f' {supported_organisms}'
734
+ )
735
+ if len(variant_scorers) != len(set(variant_scorers)):
736
+ raise ValueError(
737
+ f'Duplicate variant scorers requested: {variant_scorers}.'
738
+ )
739
+ if len(variant_scorers) > MAX_VARIANT_SCORERS_PER_REQUEST:
740
+ raise ValueError(
741
+ 'Too many variant scorers requested, '
742
+ f'maximum allowed: {MAX_VARIANT_SCORERS_PER_REQUEST}.'
743
+ )
744
+ request = dna_model_service_pb2.ScoreVariantRequest(
745
+ interval=interval.to_proto(),
746
+ variant=variant.to_proto(),
747
+ organism=organism.to_proto(),
748
+ variant_scorers=[vs.to_proto() for vs in variant_scorers],
749
+ model_version=self._model_version,
750
+ )
751
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
752
+ self._channel
753
+ ).ScoreVariant(iter([request]), metadata=self._metadata)
754
+ variant_outputs = _make_score_variant_output(responses, interval)
755
+ for ix in range(len(variant_scorers)):
756
+ variant_outputs[ix].uns['variant_scorer'] = variant_scorers[ix]
757
+ return variant_outputs
758
+
759
+ def score_ism_variants(
760
+ self,
761
+ interval: genome.Interval,
762
+ ism_interval: genome.Interval,
763
+ variant_scorers: Sequence[variant_scorers_lib.VariantScorerTypes] = (),
764
+ *,
765
+ organism: Organism = Organism.HOMO_SAPIENS,
766
+ interval_variant: genome.Variant | None = None,
767
+ progress_bar: bool = True,
768
+ max_workers: int = dna_model.DEFAULT_MAX_WORKERS,
769
+ ) -> list[list[anndata.AnnData]]:
770
+ """Generate in-silico mutagenesis (ISM) variant scores for a given interval.
771
+
772
+ Args:
773
+ interval: DNA interval to make the prediction for.
774
+ ism_interval: Interval to perform ISM.
775
+ variant_scorers: Sequence of variant scorers to use for scoring each
776
+ variant. If no variant scorers are provided, the recommended variant
777
+ scorers for the organism will be used.
778
+ organism: Organism to use for the prediction.
779
+ interval_variant: Optional variant to apply to the sequence. If provided,
780
+ the alternate allele is used for in-silico mutagenesis, otherwise the
781
+ unaltered reference sequence is used.
782
+ progress_bar: If True, show a progress bar.
783
+ max_workers: Number of parallel workers to use.
784
+
785
+ Returns:
786
+ List of variant scores for each variant in the ISM interval.
787
+ """
788
+ if not variant_scorers:
789
+ variant_scorers = variant_scorers_lib.get_recommended_scorers(
790
+ organism.to_proto()
791
+ )
792
+
793
+ validate_sequence_length(interval.width)
794
+ if ism_interval.negative_strand:
795
+ raise ValueError('ISM interval must be on the positive strand.')
796
+
797
+ ism_intervals = []
798
+ for position in range(0, ism_interval.width, MAX_ISM_INTERVAL_WIDTH):
799
+ ism_intervals.append(
800
+ genome.Interval(
801
+ ism_interval.chromosome,
802
+ ism_interval.start + position,
803
+ min(
804
+ ism_interval.start + position + MAX_ISM_INTERVAL_WIDTH,
805
+ ism_interval.end,
806
+ ),
807
+ )
808
+ )
809
+
810
+ @retry_rpc
811
+ def _score_ism_variant(
812
+ ism_interval: genome.Interval,
813
+ ) -> list[anndata.AnnData]:
814
+ request = dna_model_service_pb2.ScoreIsmVariantRequest(
815
+ interval=interval.to_proto(),
816
+ ism_interval=ism_interval.to_proto(),
817
+ organism=organism.to_proto(),
818
+ variant_scorers=[vs.to_proto() for vs in variant_scorers],
819
+ model_version=self._model_version,
820
+ interval_variant=interval_variant.to_proto()
821
+ if interval_variant is not None
822
+ else None,
823
+ )
824
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
825
+ self._channel
826
+ ).ScoreIsmVariant(iter([request]), metadata=self._metadata)
827
+
828
+ return _make_score_variant_output(responses, interval)
829
+
830
+ with concurrent.futures.ThreadPoolExecutor(
831
+ max_workers=max_workers
832
+ ) as executor:
833
+ futures = [
834
+ executor.submit(_score_ism_variant, ism_interval)
835
+ for ism_interval in ism_intervals
836
+ ]
837
+ ism_scores = []
838
+ for future in tqdm.auto.tqdm(
839
+ concurrent.futures.as_completed(futures),
840
+ total=len(futures),
841
+ disable=not progress_bar,
842
+ ):
843
+ ism_scores.extend(future.result())
844
+
845
+ # Group scores by variant.
846
+ ism_scores_by_variant: dict[str, list[anndata.AnnData]] = {}
847
+ for variant_score in ism_scores:
848
+ ism_scores_by_variant.setdefault(
849
+ str(variant_score.uns['variant']), []
850
+ ).append(variant_score)
851
+
852
+ return list(ism_scores_by_variant.values())
853
+
854
+ def output_metadata(
855
+ self, organism: Organism = Organism.HOMO_SAPIENS
856
+ ) -> OutputMetadata:
857
+ """Get the metadata for a given organism.
858
+
859
+ Args:
860
+ organism: Organism to get metadata for.
861
+
862
+ Returns:
863
+ OutputMetadata for the provided organism.
864
+ """
865
+ request = dna_model_service_pb2.MetadataRequest(
866
+ organism=organism.to_proto()
867
+ )
868
+ responses = dna_model_service_pb2_grpc.DnaModelServiceStub(
869
+ self._channel
870
+ ).GetMetadata(request, metadata=self._metadata)
871
+ return construct_output_metadata(responses)
872
+
873
+
874
+ def create(
875
+ api_key: str,
876
+ *,
877
+ model_version: ModelVersion | None = None,
878
+ timeout: float | None = None,
879
+ address: str | None = None,
880
+ ) -> DnaClient:
881
+ """Creates a model client for a given API key.
882
+
883
+ Args:
884
+ api_key: API key to use for authentication.
885
+ model_version: Optional model version to use for the DNA model server. If
886
+ none is provided, the default model will be used.
887
+ timeout: Optional timeout for waiting for the channel to be ready.
888
+ address: Optional server address to connect to.
889
+
890
+ Returns:
891
+ `DnaClient` instance.
892
+ """
893
+ options = [
894
+ ('grpc.max_send_message_length', -1),
895
+ ('grpc.max_receive_message_length', -1),
896
+ ]
897
+ address = address or 'dns:///gdmscience.googleapis.com:443'
898
+ channel = grpc.secure_channel(
899
+ address, grpc.ssl_channel_credentials(), options=options
900
+ )
901
+ grpc.channel_ready_future(channel).result(timeout)
902
+
903
+ return DnaClient(
904
+ channel=channel,
905
+ model_version=model_version,
906
+ metadata=[('x-goog-api-key', api_key)],
907
+ )
flax_model/alphagenome/_sdk/models/dna_model.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Abstract base class for AlphaGenome DNA models."""
16
+
17
+ import abc
18
+ from collections.abc import Iterable, Sequence
19
+ import concurrent.futures
20
+ import enum
21
+
22
+ from flax_model.alphagenome._sdk.data import genome
23
+ from flax_model.alphagenome._sdk.data import ontology
24
+ from flax_model.alphagenome._sdk.models import dna_output
25
+ from flax_model.alphagenome._sdk.models import interval_scorers as interval_scorers_lib
26
+ from flax_model.alphagenome._sdk.models import variant_scorers as variant_scorers_lib
27
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
28
+ import anndata
29
+ import tqdm.auto
30
+
31
+ # Default maximum number of workers to use for parallel requests.
32
+ DEFAULT_MAX_WORKERS = 5
33
+
34
+
35
+ @enum.unique
36
+ class ModelVersion(enum.Enum):
37
+ """Enumeration of all available model versions.
38
+
39
+ A fold is a part of the genome that is held out from training, and is used for
40
+ model validation.
41
+
42
+ Folds are numbered from 0 to 3, and represent disjoint subsets of the genome.
43
+
44
+ A model version that is designated FOLD_#, where # is an integer from 0 to 3,
45
+ indicates that the model was trained with that particular fold held out.
46
+
47
+ The ALL_FOLDS version refers to the distilled model that was trained on
48
+ an ensemble of teacher models trained on all folds.
49
+ """
50
+
51
+ ALL_FOLDS = enum.auto() # Default model version if None explicitly set.
52
+ FOLD_0 = enum.auto()
53
+ FOLD_1 = enum.auto()
54
+ FOLD_2 = enum.auto()
55
+ FOLD_3 = enum.auto()
56
+
57
+
58
+ class Organism(enum.Enum):
59
+ """Enumeration of all the available organisms."""
60
+
61
+ HOMO_SAPIENS = dna_model_pb2.ORGANISM_HOMO_SAPIENS
62
+ MUS_MUSCULUS = dna_model_pb2.ORGANISM_MUS_MUSCULUS
63
+
64
+ def to_proto(self) -> dna_model_pb2.Organism:
65
+ return self.value
66
+
67
+ def __lt__(self, other: 'Organism') -> bool:
68
+ if not isinstance(other, Organism):
69
+ return NotImplemented
70
+ return self.value < other.value
71
+
72
+
73
+ class DnaModel(metaclass=abc.ABCMeta):
74
+ """Abstract base class for AlphaGenome DNA models."""
75
+
76
+ @abc.abstractmethod
77
+ def predict_sequence(
78
+ self,
79
+ sequence: str,
80
+ *,
81
+ organism: Organism = Organism.HOMO_SAPIENS,
82
+ requested_outputs: Iterable[dna_output.OutputType],
83
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
84
+ interval: genome.Interval | None = None,
85
+ ) -> dna_output.Output:
86
+ """Generate predictions for a given DNA sequence.
87
+
88
+ Args:
89
+ sequence: DNA sequence to make prediction for.
90
+ organism: Organism to use for the prediction.
91
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
92
+ predictions to return.
93
+ ontology_terms: Iterable of ontology terms or curies to generate
94
+ predictions for. If None returns all ontologies.
95
+ interval: Optional interval from which the sequence was derived. This is
96
+ used as the interval in the output TrackData.
97
+
98
+ Returns:
99
+ Output for the provided DNA sequence.
100
+ """
101
+
102
+ def predict_sequences(
103
+ self,
104
+ sequences: Sequence[str],
105
+ *,
106
+ organism: Organism = Organism.HOMO_SAPIENS,
107
+ requested_outputs: Iterable[dna_output.OutputType],
108
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
109
+ progress_bar: bool = True,
110
+ max_workers: int = DEFAULT_MAX_WORKERS,
111
+ intervals: Sequence[genome.Interval] | None = None,
112
+ ) -> list[dna_output.Output]:
113
+ """Generate predictions for a given DNA sequence.
114
+
115
+ Args:
116
+ sequences: DNA sequences to make predictions for.
117
+ organism: Organism to use for the prediction.
118
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
119
+ predictions to return.
120
+ ontology_terms: Iterable of ontology terms or curies to generate
121
+ predictions for. If None returns all ontologies.
122
+ progress_bar: If True, show a progress bar.
123
+ max_workers: Number of parallel workers to use.
124
+ intervals: Optional intervals from which the sequences were derived. This
125
+ is used as the interval in the output TrackData. Must be the same length
126
+ as `sequences` if provided.
127
+
128
+ Returns:
129
+ Outputs for the provided DNA sequences.
130
+ """
131
+ with concurrent.futures.ThreadPoolExecutor(
132
+ max_workers=max_workers
133
+ ) as executor:
134
+ futures = [
135
+ executor.submit(
136
+ self.predict_sequence,
137
+ sequence=sequence,
138
+ organism=organism,
139
+ ontology_terms=ontology_terms,
140
+ requested_outputs=requested_outputs,
141
+ interval=interval,
142
+ )
143
+ for sequence, interval in zip(
144
+ sequences, intervals or [None] * len(sequences), strict=True
145
+ )
146
+ ]
147
+ futures_as_completed = tqdm.auto.tqdm(
148
+ concurrent.futures.as_completed(futures),
149
+ total=len(futures),
150
+ disable=not progress_bar,
151
+ )
152
+ for future in futures_as_completed:
153
+ if (exception := future.exception()) is not None:
154
+ executor.shutdown(wait=False, cancel_futures=True)
155
+ raise exception
156
+
157
+ return [future.result() for future in futures]
158
+
159
+ @abc.abstractmethod
160
+ def predict_interval(
161
+ self,
162
+ interval: genome.Interval,
163
+ *,
164
+ organism: Organism = Organism.HOMO_SAPIENS,
165
+ requested_outputs: Iterable[dna_output.OutputType],
166
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
167
+ ) -> dna_output.Output:
168
+ """Generate predictions for a given DNA interval.
169
+
170
+ Args:
171
+ interval: DNA interval to make prediction for.
172
+ organism: Organism to use for the prediction.
173
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
174
+ predictions to return.
175
+ ontology_terms: Iterable of ontology terms or curies to generate
176
+ predictions for. If None returns all ontologies.
177
+
178
+ Returns:
179
+ Output for the provided DNA interval.
180
+ """
181
+
182
+ def predict_intervals(
183
+ self,
184
+ intervals: Sequence[genome.Interval],
185
+ *,
186
+ organism: Organism = Organism.HOMO_SAPIENS,
187
+ requested_outputs: Iterable[dna_output.OutputType],
188
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
189
+ progress_bar: bool = True,
190
+ max_workers: int = DEFAULT_MAX_WORKERS,
191
+ ) -> list[dna_output.Output]:
192
+ """Generate predictions for a sequence of DNA intervals.
193
+
194
+ Args:
195
+ intervals: DNA intervals to make predictions for.
196
+ organism: Organism to use for the predictions.
197
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
198
+ predictions to return.
199
+ ontology_terms: Iterable of ontology terms or curies to generate
200
+ predictions for. If None returns all ontologies.
201
+ progress_bar: If True, show a progress bar.
202
+ max_workers: Number of parallel workers to use.
203
+
204
+ Returns:
205
+ Outputs for the provided DNA intervals.
206
+ """
207
+ with concurrent.futures.ThreadPoolExecutor(
208
+ max_workers=max_workers
209
+ ) as executor:
210
+ futures = [
211
+ executor.submit(
212
+ self.predict_interval,
213
+ interval=interval,
214
+ organism=organism,
215
+ ontology_terms=ontology_terms,
216
+ requested_outputs=requested_outputs,
217
+ )
218
+ for interval in intervals
219
+ ]
220
+ futures_as_completed = tqdm.auto.tqdm(
221
+ concurrent.futures.as_completed(futures),
222
+ total=len(futures),
223
+ disable=not progress_bar,
224
+ )
225
+ for future in futures_as_completed:
226
+ if (exception := future.exception()) is not None:
227
+ executor.shutdown(wait=False, cancel_futures=True)
228
+ raise exception
229
+
230
+ return [future.result() for future in futures]
231
+
232
+ @abc.abstractmethod
233
+ def predict_variant(
234
+ self,
235
+ interval: genome.Interval,
236
+ variant: genome.Variant,
237
+ *,
238
+ organism: Organism = Organism.HOMO_SAPIENS,
239
+ requested_outputs: Iterable[dna_output.OutputType],
240
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
241
+ ) -> dna_output.VariantOutput:
242
+ """Generate predictions for a given DNA variant.
243
+
244
+ Args:
245
+ interval: DNA interval to make prediction for.
246
+ variant: DNA variant to make prediction for.
247
+ organism: Organism to use for the prediction.
248
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
249
+ predictions to return.
250
+ ontology_terms: Iterable of ontology terms or curies to generate
251
+ predictions for. If None returns all ontologies.
252
+
253
+ Returns:
254
+ Variant output for the provided DNA interval and variant.
255
+ """
256
+
257
+ def predict_variants(
258
+ self,
259
+ intervals: genome.Interval | Sequence[genome.Interval],
260
+ variants: Sequence[genome.Variant],
261
+ *,
262
+ organism: Organism = Organism.HOMO_SAPIENS,
263
+ requested_outputs: Iterable[dna_output.OutputType],
264
+ ontology_terms: Iterable[ontology.OntologyTerm | str] | None,
265
+ progress_bar: bool = True,
266
+ max_workers: int = DEFAULT_MAX_WORKERS,
267
+ ) -> list[dna_output.VariantOutput]:
268
+ """Generate predictions for a given DNA variant.
269
+
270
+ Args:
271
+ intervals: DNA interval(s) to make predictions for.
272
+ variants: DNA variants to make prediction for.
273
+ organism: Organism to use for the prediction.
274
+ requested_outputs: Iterable of OutputTypes indicating which subsets of
275
+ predictions to return.
276
+ ontology_terms: Iterable of ontology terms or curies to generate
277
+ predictions for. If None returns all ontologies.
278
+ progress_bar: If True, show a progress bar.
279
+ max_workers: Number of parallel workers to use.
280
+
281
+ Returns:
282
+ Variant outputs for each DNA interval and variant pair.
283
+ """
284
+ if not isinstance(intervals, Sequence):
285
+ intervals = [intervals] * len(variants)
286
+ elif len(intervals) != len(variants):
287
+ raise ValueError(
288
+ 'Intervals and variants must have the same length.'
289
+ f'Got {len(intervals)} intervals and {len(variants)} variants.'
290
+ )
291
+ with concurrent.futures.ThreadPoolExecutor(
292
+ max_workers=max_workers
293
+ ) as executor:
294
+ futures = [
295
+ executor.submit(
296
+ self.predict_variant,
297
+ interval=interval,
298
+ variant=variant,
299
+ organism=organism,
300
+ ontology_terms=ontology_terms,
301
+ requested_outputs=requested_outputs,
302
+ )
303
+ for interval, variant in zip(intervals, variants, strict=True)
304
+ ]
305
+ futures_as_completed = tqdm.auto.tqdm(
306
+ concurrent.futures.as_completed(futures),
307
+ total=len(futures),
308
+ disable=not progress_bar,
309
+ )
310
+ for future in futures_as_completed:
311
+ if (exception := future.exception()) is not None:
312
+ executor.shutdown(wait=False, cancel_futures=True)
313
+ raise exception
314
+
315
+ return [future.result() for future in futures]
316
+
317
+ @abc.abstractmethod
318
+ def score_interval(
319
+ self,
320
+ interval: genome.Interval,
321
+ interval_scorers: Sequence[interval_scorers_lib.IntervalScorerTypes] = (),
322
+ *,
323
+ organism: Organism = Organism.HOMO_SAPIENS,
324
+ ) -> list[anndata.AnnData]:
325
+ """Generate interval scores for a single given interval.
326
+
327
+ Args:
328
+ interval: Interval to make prediction for.
329
+ interval_scorers: Sequence of interval scorers to use for scoring. If no
330
+ interval scorers are provided, the recommended interval scorers for the
331
+ organism will be used.
332
+ organism: Organism to use for the prediction.
333
+
334
+ Returns:
335
+ List of `AnnData` interval scores.
336
+ """
337
+
338
+ def score_intervals(
339
+ self,
340
+ intervals: Sequence[genome.Interval],
341
+ interval_scorers: Sequence[interval_scorers_lib.IntervalScorerTypes] = (),
342
+ *,
343
+ organism: Organism = Organism.HOMO_SAPIENS,
344
+ progress_bar: bool = True,
345
+ max_workers: int = DEFAULT_MAX_WORKERS,
346
+ ) -> list[list[anndata.AnnData]]:
347
+ """Generate interval scores for a sequence of intervals.
348
+
349
+ Args:
350
+ intervals: Sequence of DNA intervals to make prediction for.
351
+ interval_scorers: Sequence of interval scorers to use for scoring. If no
352
+ interval scorers are provided, the recommended interval scorers for the
353
+ organism will be used.
354
+ organism: Organism to use for the prediction.
355
+ progress_bar: If True, show a progress bar.
356
+ max_workers: Number of parallel workers to use.
357
+
358
+ Returns:
359
+ List of `AnnData` lists corresponding to score_interval
360
+ outputs for each interval.
361
+ """
362
+ with concurrent.futures.ThreadPoolExecutor(
363
+ max_workers=max_workers
364
+ ) as executor:
365
+ futures = [
366
+ executor.submit(
367
+ self.score_interval,
368
+ interval=interval,
369
+ interval_scorers=interval_scorers,
370
+ organism=organism,
371
+ )
372
+ for interval in intervals
373
+ ]
374
+ futures_as_completed = tqdm.auto.tqdm(
375
+ concurrent.futures.as_completed(futures),
376
+ total=len(futures),
377
+ disable=not progress_bar,
378
+ )
379
+ for future in futures_as_completed:
380
+ if (exception := future.exception()) is not None:
381
+ executor.shutdown(wait=False, cancel_futures=True)
382
+ raise exception
383
+
384
+ results = [future.result() for future in futures]
385
+ return results
386
+
387
+ @abc.abstractmethod
388
+ def score_variant(
389
+ self,
390
+ interval: genome.Interval,
391
+ variant: genome.Variant,
392
+ variant_scorers: Sequence[variant_scorers_lib.VariantScorerTypes] = (),
393
+ *,
394
+ organism: Organism = Organism.HOMO_SAPIENS,
395
+ ) -> list[anndata.AnnData]:
396
+ """Generate variant scores for a single given DNA variant.
397
+
398
+ Args:
399
+ interval: DNA interval to make prediction for.
400
+ variant: DNA variant to make prediction for.
401
+ variant_scorers: Sequence of variant scorers to use for scoring. If no
402
+ variant scorers are provided, the recommended variant scorers for the
403
+ organism will be used.
404
+ organism: Organism to use for the prediction.
405
+
406
+ Returns:
407
+ List of `AnnData` variant scores.
408
+ """
409
+
410
+ def score_variants(
411
+ self,
412
+ intervals: genome.Interval | Sequence[genome.Interval],
413
+ variants: Sequence[genome.Variant],
414
+ variant_scorers: Sequence[variant_scorers_lib.VariantScorerTypes] = (),
415
+ *,
416
+ organism: Organism = Organism.HOMO_SAPIENS,
417
+ progress_bar: bool = True,
418
+ max_workers: int = DEFAULT_MAX_WORKERS,
419
+ ) -> list[list[anndata.AnnData]]:
420
+ """Generate variant scores for a sequence of variants.
421
+
422
+ Args:
423
+ intervals: DNA interval(s) to make prediction for. If a single interval is
424
+ provided, then the same interval will be used for all variants.
425
+ variants: Sequence of DNA variants to make prediction for.
426
+ variant_scorers: Sequence of variant scorers to use for scoring. If no
427
+ variant scorers are provided, the recommended variant scorers for the
428
+ organism will be used.
429
+ organism: Organism to use for the prediction.
430
+ progress_bar: If True, show a progress bar.
431
+ max_workers: Number of parallel workers to use.
432
+
433
+ Returns:
434
+ List of `AnnData` lists corresponding to score_variant outputs for each
435
+ variant.
436
+ """
437
+ if not isinstance(intervals, Sequence):
438
+ intervals = [intervals] * len(variants)
439
+ if len(intervals) != len(variants):
440
+ raise ValueError(
441
+ 'Intervals and variants must have the same length.'
442
+ f'Got {len(intervals)} intervals and {len(variants)} variants.'
443
+ )
444
+ with concurrent.futures.ThreadPoolExecutor(
445
+ max_workers=max_workers
446
+ ) as executor:
447
+ futures = [
448
+ executor.submit(
449
+ self.score_variant,
450
+ interval=interval,
451
+ variant=variant,
452
+ variant_scorers=variant_scorers,
453
+ organism=organism,
454
+ )
455
+ for interval, variant in zip(intervals, variants, strict=True)
456
+ ]
457
+ futures_as_completed = tqdm.auto.tqdm(
458
+ concurrent.futures.as_completed(futures),
459
+ total=len(futures),
460
+ disable=not progress_bar,
461
+ )
462
+ for future in futures_as_completed:
463
+ if (exception := future.exception()) is not None:
464
+ executor.shutdown(wait=False, cancel_futures=True)
465
+ raise exception
466
+
467
+ return [future.result() for future in futures]
468
+
469
+ @abc.abstractmethod
470
+ def score_ism_variants(
471
+ self,
472
+ interval: genome.Interval,
473
+ ism_interval: genome.Interval,
474
+ variant_scorers: Sequence[variant_scorers_lib.VariantScorerTypes] = (),
475
+ *,
476
+ interval_variant: genome.Variant | None = None,
477
+ organism: Organism = Organism.HOMO_SAPIENS,
478
+ ) -> list[list[anndata.AnnData]]:
479
+ """Generate in-silico mutagenesis (ISM) variant scores for a given interval.
480
+
481
+ Args:
482
+ interval: DNA interval to make the prediction for.
483
+ ism_interval: Interval to perform ISM.
484
+ variant_scorers: Sequence of variant scorers to use for scoring each
485
+ variant. If no variant scorers are provided, the recommended variant
486
+ scorers for the organism will be used.
487
+ interval_variant: Optional variant to apply to the sequence. If provided,
488
+ the alternate allele is used for in-silico mutagenesis, otherwise the
489
+ unaltered reference sequence is used.
490
+ organism: Organism to use for the prediction.
491
+
492
+ Returns:
493
+ List of variant scores for each variant in the ISM interval.
494
+ """
495
+
496
+ @abc.abstractmethod
497
+ def output_metadata(
498
+ self, organism: Organism = Organism.HOMO_SAPIENS
499
+ ) -> dna_output.OutputMetadata:
500
+ """Get the metadata for a given organism.
501
+
502
+ Args:
503
+ organism: Organism to get metadata for.
504
+
505
+ Returns:
506
+ OutputMetadata for the provided organism.
507
+ """
flax_model/alphagenome/_sdk/models/dna_output.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Module for AlphaGenome model outputs."""
15
+
16
+ from collections.abc import Callable, Iterable, Mapping
17
+ import dataclasses
18
+ import enum
19
+ from typing import Literal
20
+
21
+ from flax_model.alphagenome._sdk import typing
22
+ from flax_model.alphagenome._sdk.data import junction_data
23
+ from flax_model.alphagenome._sdk.data import ontology
24
+ from flax_model.alphagenome._sdk.data import track_data
25
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
26
+ import pandas as pd
27
+
28
+
29
+ class OutputType(enum.Enum):
30
+ """Enumeration of all the available types of outputs.
31
+
32
+ Attributes:
33
+ ATAC: ATAC-seq tracks capturing chromatin accessibility.
34
+ CAGE: CAGE (Cap Analysis of Gene Expression) tracks capturing gene
35
+ expression.
36
+ DNASE: DNase I hypersensitive site tracks capturing chromatin accessibility.
37
+ RNA_SEQ: RNA sequencing tracks capturing gene expression.
38
+ CHIP_HISTONE: ChIP-seq tracks capturing histone modifications.
39
+ CHIP_TF: ChIP-seq tracks capturing transcription factor binding.
40
+ SPLICE_SITES: Splice site tracks capturing donor and acceptor splice sites.
41
+ SPLICE_SITE_USAGE: Splice site usage tracks capturing the fraction of the
42
+ time that each splice site is used.
43
+ SPLICE_JUNCTIONS: Splice junction tracks capturing split read RNA-seq counts
44
+ for each junction.
45
+ CONTACT_MAPS: Contact map tracks capturing 3D DNA-DNA contact probabilities.
46
+ PROCAP: Precision Run-On sequencing and capping, used to measure gene
47
+ expression.
48
+ """
49
+
50
+ ATAC = dna_model_pb2.OUTPUT_TYPE_ATAC
51
+ CAGE = dna_model_pb2.OUTPUT_TYPE_CAGE
52
+ DNASE = dna_model_pb2.OUTPUT_TYPE_DNASE
53
+ RNA_SEQ = dna_model_pb2.OUTPUT_TYPE_RNA_SEQ
54
+ CHIP_HISTONE = dna_model_pb2.OUTPUT_TYPE_CHIP_HISTONE
55
+ CHIP_TF = dna_model_pb2.OUTPUT_TYPE_CHIP_TF
56
+ SPLICE_SITES = dna_model_pb2.OUTPUT_TYPE_SPLICE_SITES
57
+ SPLICE_SITE_USAGE = dna_model_pb2.OUTPUT_TYPE_SPLICE_SITE_USAGE
58
+ SPLICE_JUNCTIONS = dna_model_pb2.OUTPUT_TYPE_SPLICE_JUNCTIONS
59
+ CONTACT_MAPS = dna_model_pb2.OUTPUT_TYPE_CONTACT_MAPS
60
+ PROCAP = dna_model_pb2.OUTPUT_TYPE_PROCAP
61
+
62
+ def __lt__(self, other: 'OutputType'):
63
+ """Compares if an other `OutputType` enum value is less than this one."""
64
+ return self.value < other.value
65
+
66
+ def to_proto(self) -> dna_model_pb2.OutputType:
67
+ """Converts the `OutputType` enum to a protobuf enum."""
68
+ return self.value
69
+
70
+ def __repr__(self) -> str:
71
+ """Returns name of the `OutputType` enum as the string representation."""
72
+ return self.name
73
+
74
+
75
+ @typing.jaxtyped
76
+ @dataclasses.dataclass(frozen=True)
77
+ class Output:
78
+ """Model outputs for a single prediction.
79
+
80
+ Attributes:
81
+ atac: TrackData of type OutputType.ATAC.
82
+ cage: TrackData of type OutputType.CAGE.
83
+ dnase: TrackData of type OutputType.DNASE.
84
+ rna_seq: TrackData of type OutputType.RNA_SEQ.
85
+ chip_histone: TrackData of type OutputType.CHIP_HISTONE.
86
+ chip_tf: TrackData of type OutputType.CHIP_TF.
87
+ splice_sites: TrackData of type OutputType.SPLICE_SITES.
88
+ splice_site_usage: TrackData of type OutputType.SPLICE_SITE_USAGE.
89
+ splice_junctions: TrackData of type OutputType.SPLICE_JUNCTIONS.
90
+ contact_maps: TrackData of type OutputType.CONTACT_MAPS.
91
+ procap: TrackData of type OutputType.PROCAP.
92
+ """
93
+
94
+ atac: track_data.TrackData | None = dataclasses.field(
95
+ default=None, metadata={'output_type': OutputType.ATAC}
96
+ )
97
+ cage: track_data.TrackData | None = dataclasses.field(
98
+ default=None, metadata={'output_type': OutputType.CAGE}
99
+ )
100
+ dnase: track_data.TrackData | None = dataclasses.field(
101
+ default=None, metadata={'output_type': OutputType.DNASE}
102
+ )
103
+ rna_seq: track_data.TrackData | None = dataclasses.field(
104
+ default=None, metadata={'output_type': OutputType.RNA_SEQ}
105
+ )
106
+ chip_histone: track_data.TrackData | None = dataclasses.field(
107
+ default=None,
108
+ metadata={'output_type': OutputType.CHIP_HISTONE},
109
+ )
110
+ chip_tf: track_data.TrackData | None = dataclasses.field(
111
+ default=None, metadata={'output_type': OutputType.CHIP_TF}
112
+ )
113
+
114
+ splice_sites: track_data.TrackData | None = dataclasses.field(
115
+ default=None,
116
+ metadata={'output_type': OutputType.SPLICE_SITES},
117
+ )
118
+ splice_site_usage: track_data.TrackData | None = dataclasses.field(
119
+ default=None,
120
+ metadata={'output_type': OutputType.SPLICE_SITE_USAGE},
121
+ )
122
+ splice_junctions: junction_data.JunctionData | None = dataclasses.field(
123
+ default=None,
124
+ metadata={'output_type': OutputType.SPLICE_JUNCTIONS},
125
+ )
126
+
127
+ contact_maps: track_data.TrackData | None = dataclasses.field(
128
+ default=None,
129
+ metadata={'output_type': OutputType.CONTACT_MAPS},
130
+ )
131
+ procap: track_data.TrackData | None = dataclasses.field(
132
+ default=None,
133
+ metadata={'output_type': OutputType.PROCAP},
134
+ )
135
+
136
+ def get(
137
+ self, output_type: OutputType
138
+ ) -> track_data.TrackData | junction_data.JunctionData | None:
139
+ """Gets the track data for the specified output type.
140
+
141
+ Args:
142
+ output_type: The type of output to retrieve.
143
+
144
+ Returns:
145
+ The track data for the specified output type, or None if no such data
146
+ exists.
147
+ """
148
+ for field in dataclasses.fields(self):
149
+ if field.metadata['output_type'] == output_type:
150
+ return getattr(self, field.name)
151
+
152
+ def map_track_data(
153
+ self,
154
+ fn: Callable[
155
+ [track_data.TrackData, OutputType],
156
+ track_data.TrackData | None,
157
+ ],
158
+ ) -> 'Output':
159
+ """Applies a transformation function to each `TrackData`.
160
+
161
+ Args:
162
+ fn: The function to apply to each `TrackData`. It should take a
163
+ `TrackData` object and an `OutputType` enum as input and return a
164
+ `TrackData` object or None.
165
+
166
+ Returns:
167
+ A new `Output` object with the transformed track data.
168
+ """
169
+ output_dict = {}
170
+ for field in dataclasses.fields(self):
171
+ output_type = field.metadata['output_type']
172
+ value = self.get(output_type)
173
+ if isinstance(value, track_data.TrackData):
174
+ output_dict[field.name] = fn(value, output_type)
175
+ else:
176
+ output_dict[field.name] = value
177
+ return Output(**output_dict)
178
+
179
+ def filter_to_strand(self, strand: Literal['+', '-', '.']) -> 'Output':
180
+ """Filters tracks by DNA strand.
181
+
182
+ Args:
183
+ strand: The strand to filter by ('+', '-', or '.').
184
+
185
+ Returns:
186
+ A new `Output` object with only the tracks on the specified strand.
187
+ """
188
+
189
+ def _filter_to_strand(
190
+ tdata: track_data.TrackData, output_type: OutputType
191
+ ) -> track_data.TrackData | None:
192
+ del output_type # Unused.
193
+ return tdata.filter_tracks(tdata.strands == strand)
194
+
195
+ output = self.map_track_data(_filter_to_strand)
196
+ if output.splice_junctions is not None:
197
+ output = dataclasses.replace(
198
+ output,
199
+ splice_junctions=output.splice_junctions.filter_to_strand(strand),
200
+ )
201
+ return output
202
+
203
+ def filter_ontology_terms(
204
+ self, ontology_terms: Iterable[ontology.OntologyTerm]
205
+ ) -> 'Output':
206
+ """Filters tracks to specific ontology terms.
207
+
208
+ Args:
209
+ ontology_terms: An iterable of `OntologyTerm` objects to filter to.
210
+
211
+ Returns:
212
+ A new `Output` object with only the tracks associated with the specified
213
+ ontology terms.
214
+ """
215
+
216
+ def _filter_ontology(
217
+ tdata: track_data.TrackData, output_type: OutputType
218
+ ) -> track_data.TrackData | None:
219
+ del output_type # Unused.
220
+ if track_ontologies := tdata.ontology_terms:
221
+ return tdata.filter_tracks(
222
+ [o in ontology_terms for o in track_ontologies]
223
+ )
224
+ else:
225
+ return tdata
226
+
227
+ return self.map_track_data(_filter_ontology)
228
+
229
+ def filter_output_type(self, output_types: Iterable[OutputType]) -> 'Output':
230
+ """Filters tracks to specific output type.
231
+
232
+ Args:
233
+ output_types: An iterable of `OutputType` enums to filter by.
234
+
235
+ Returns:
236
+ A new `Output` object with only the tracks of the specified output types.
237
+ """
238
+ output_dict = {}
239
+ for field in dataclasses.fields(self):
240
+ output_type = field.metadata['output_type']
241
+ if output_type in output_types:
242
+ output_dict[field.name] = self.get(output_type)
243
+ else:
244
+ output_dict[field.name] = None
245
+
246
+ return Output(**output_dict)
247
+
248
+ def resize(self, width: int) -> 'Output':
249
+ """Resizes all track data to a specified width.
250
+
251
+ Args:
252
+ width: The desired width in base pairs.
253
+
254
+ Returns:
255
+ A new `Output` object with resized track data.
256
+ """
257
+ return self.map_track_data(lambda tdata, _: tdata.resize(width))
258
+
259
+ def __add__(self, other: 'Output') -> 'Output':
260
+ """Adds the values of two `Output` objects element-wise.
261
+
262
+ Args:
263
+ other: The `Output` object to add.
264
+
265
+ Returns:
266
+ A new `Output` object with the summed values.
267
+ """
268
+
269
+ def add_track_data(
270
+ track_data1,
271
+ output_type: OutputType,
272
+ ):
273
+ """Adds two `TrackData` objects for a specific output type."""
274
+ track_data2 = other.get(output_type)
275
+ if track_data1 is None or track_data2 is None:
276
+ return None
277
+ return track_data1 + track_data2
278
+
279
+ return self.map_track_data(add_track_data)
280
+
281
+ def __sub__(self, other: 'Output') -> 'Output':
282
+ """Subtracts the values of two `Output` objects element-wise.
283
+
284
+ Args:
285
+ other: The `Output` object to subtract.
286
+
287
+ Returns:
288
+ A new `Output` object with the difference of the values.
289
+ """
290
+
291
+ def sub_track_data(track_data1, output_type: OutputType):
292
+ """Subtracts two `TrackData` objects for a specific output type."""
293
+ track_data2 = other.get(output_type)
294
+ if track_data1 is None or track_data2 is None:
295
+ return None
296
+ return track_data1 - track_data2
297
+
298
+ return self.map_track_data(sub_track_data)
299
+
300
+
301
+ @dataclasses.dataclass(frozen=True, kw_only=True)
302
+ class OutputMetadata:
303
+ """Metadata detailing the content of model output.
304
+
305
+ Attributes:
306
+ atac: Metadata for ATAC-seq tracks.
307
+ cage: Metadata for CAGE tracks.
308
+ dnase: Metadata for DNase I hypersensitive site tracks.
309
+ rna_seq: Metadata for RNA sequencing tracks.
310
+ chip_histone: Metadata for ChIP-seq tracks capturing histone modifications.
311
+ chip_tf: Metadata for ChIP-seq tracks capturing transcription factor
312
+ binding.
313
+ splice_sites: Metadata for splice site tracks.
314
+ splice_site_usage: Metadata for splice site usage tracks.
315
+ splice_junctions: Metadata for splice junction tracks.
316
+ contact_maps: Metadata for contact map tracks.
317
+ procap: Metadata for procap tracks.
318
+ """
319
+
320
+ atac: track_data.TrackMetadata | None = dataclasses.field(
321
+ default=None, metadata={'output_type': OutputType.ATAC}
322
+ )
323
+ cage: track_data.TrackMetadata | None = dataclasses.field(
324
+ default=None, metadata={'output_type': OutputType.CAGE}
325
+ )
326
+ dnase: track_data.TrackMetadata | None = dataclasses.field(
327
+ default=None, metadata={'output_type': OutputType.DNASE}
328
+ )
329
+ rna_seq: track_data.TrackMetadata | None = dataclasses.field(
330
+ default=None, metadata={'output_type': OutputType.RNA_SEQ}
331
+ )
332
+ chip_histone: track_data.TrackMetadata | None = dataclasses.field(
333
+ default=None, metadata={'output_type': OutputType.CHIP_HISTONE}
334
+ )
335
+ chip_tf: track_data.TrackMetadata | None = dataclasses.field(
336
+ default=None, metadata={'output_type': OutputType.CHIP_TF}
337
+ )
338
+ splice_sites: track_data.TrackMetadata | None = dataclasses.field(
339
+ default=None, metadata={'output_type': OutputType.SPLICE_SITES}
340
+ )
341
+ splice_site_usage: track_data.TrackMetadata | None = dataclasses.field(
342
+ default=None, metadata={'output_type': OutputType.SPLICE_SITE_USAGE}
343
+ )
344
+ splice_junctions: junction_data.JunctionMetadata | None = dataclasses.field(
345
+ default=None, metadata={'output_type': OutputType.SPLICE_JUNCTIONS}
346
+ )
347
+ contact_maps: track_data.TrackMetadata | None = dataclasses.field(
348
+ default=None, metadata={'output_type': OutputType.CONTACT_MAPS}
349
+ )
350
+ procap: track_data.TrackMetadata | None = dataclasses.field(
351
+ default=None, metadata={'output_type': OutputType.PROCAP}
352
+ )
353
+
354
+ def get(self, output: OutputType) -> track_data.TrackMetadata | None:
355
+ """Gets the track metadata for a given output type.
356
+
357
+ Args:
358
+ output: The `OutputType` enum value.
359
+
360
+ Returns:
361
+ The corresponding track metadata, or None if it doesn't exist.
362
+ """
363
+ match output:
364
+ case OutputType.ATAC:
365
+ return self.atac
366
+ case OutputType.CAGE:
367
+ return self.cage
368
+ case OutputType.DNASE:
369
+ return self.dnase
370
+ case OutputType.RNA_SEQ:
371
+ return self.rna_seq
372
+ case OutputType.CHIP_HISTONE:
373
+ return self.chip_histone
374
+ case OutputType.CHIP_TF:
375
+ return self.chip_tf
376
+ case OutputType.SPLICE_SITES:
377
+ return self.splice_sites
378
+ case OutputType.SPLICE_JUNCTIONS:
379
+ return self.splice_junctions
380
+ case OutputType.SPLICE_SITE_USAGE:
381
+ return self.splice_site_usage
382
+ case OutputType.CONTACT_MAPS:
383
+ return self.contact_maps
384
+ case OutputType.PROCAP:
385
+ return self.procap
386
+
387
+ @classmethod
388
+ def from_outputs(
389
+ cls,
390
+ outputs: Mapping[
391
+ OutputType, track_data.TrackData | junction_data.JunctionData
392
+ ],
393
+ ) -> 'OutputMetadata':
394
+ """Creates an `OutputMetadata` from a mapping of output types to data.
395
+
396
+ Args:
397
+ outputs: A mapping from `OutputType` to `TrackData` or `JunctionData`.
398
+
399
+ Returns:
400
+ An `OutputMetadata` object.
401
+ """
402
+ kwargs = {}
403
+ for field in dataclasses.fields(cls):
404
+ output_type = field.metadata['output_type']
405
+ if data := outputs.get(output_type):
406
+ kwargs[field.name] = data.metadata
407
+ return cls(**kwargs)
408
+
409
+ def concatenate(self) -> track_data.TrackMetadata:
410
+ """Concatenates all metadata into a single DataFrame.
411
+
412
+ Returns:
413
+ A pandas DataFrame containing all the track metadata, with an additional
414
+ column 'output_type' specifying the type of each track.
415
+ """
416
+ df_list = []
417
+ for output_type in OutputType:
418
+ if (df := self.get(output_type)) is not None:
419
+ assert isinstance(df, pd.DataFrame)
420
+ df_list.append(df.assign(output_type=output_type))
421
+ return pd.concat(df_list)
422
+
423
+
424
+ @typing.jaxtyped
425
+ @dataclasses.dataclass(frozen=True)
426
+ class VariantOutput:
427
+ """Model outputs for a variant prediction.
428
+
429
+ Attributes:
430
+ reference: The model output for the reference sequence.
431
+ alternate: The model output for the alternate sequence.
432
+ """
433
+
434
+ reference: Output
435
+ alternate: Output
flax_model/alphagenome/_sdk/models/interval_scorers.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Module containing interval scorer dataclasses for interval scoring."""
16
+
17
+ import dataclasses
18
+ import enum
19
+ from typing import TypeVar
20
+
21
+ from flax_model.alphagenome._sdk.models import dna_output
22
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
23
+ import immutabledict
24
+
25
+
26
+ class IntervalAggregationType(enum.Enum):
27
+ """Enum indicating the type of interval scorer aggregation.
28
+
29
+ Attributes:
30
+ MEAN: Mean across positions.
31
+ SUM: Sum across positions.
32
+
33
+ Methods:
34
+ to_proto: Converts the aggregation type to its corresponding proto enum.
35
+ """
36
+
37
+ MEAN = dna_model_pb2.IntervalAggregationType.INTERVAL_AGGREGATION_TYPE_MEAN
38
+ SUM = dna_model_pb2.IntervalAggregationType.INTERVAL_AGGREGATION_TYPE_SUM
39
+
40
+ def to_proto(self) -> dna_model_pb2.IntervalAggregationType:
41
+ return self.value
42
+
43
+ def __repr__(self) -> str:
44
+ return self.name
45
+
46
+
47
+ class BaseIntervalScorer(enum.Enum):
48
+ """Enum indicating the type of interval scoring.
49
+
50
+ Attributes:
51
+ GENE_MASK: Gene-mask scoring (e.g., aggregation of predictions within genes)
52
+ """
53
+
54
+ GENE_MASK = enum.auto()
55
+
56
+
57
+ SUPPORTED_OUTPUT_TYPES = immutabledict.immutabledict({
58
+ BaseIntervalScorer.GENE_MASK: [
59
+ dna_output.OutputType.ATAC,
60
+ dna_output.OutputType.CAGE,
61
+ dna_output.OutputType.CHIP_HISTONE,
62
+ dna_output.OutputType.CHIP_TF,
63
+ dna_output.OutputType.DNASE,
64
+ dna_output.OutputType.RNA_SEQ,
65
+ dna_output.OutputType.SPLICE_SITES,
66
+ dna_output.OutputType.SPLICE_SITE_USAGE,
67
+ ],
68
+ })
69
+
70
+ SUPPORTED_WIDTHS = immutabledict.immutabledict({
71
+ BaseIntervalScorer.GENE_MASK: [None, 501, 2001, 10_001, 100_001, 200_001],
72
+ })
73
+
74
+
75
+ @dataclasses.dataclass(frozen=True)
76
+ class GeneMaskScorer:
77
+ """Interval scorer for gene-mask scoring.
78
+
79
+ Attributes:
80
+ requested_output: The requested output type (e.g. ATAC, DNASE, etc.)
81
+ width: The width of the target interval to include in the aggregation.
82
+ aggregation_type: The type of aggregation to perform.
83
+ base_interval_scorer: The base interval scorer.
84
+ name: The name of the scorer (a composite of the above attributes that
85
+ uniquely identifies a scorer combination).
86
+
87
+ Methods:
88
+ to_proto: Converts the scorer to its corresponding proto message.
89
+
90
+ Raises:
91
+ ValueError: If the requested output is not supported.
92
+ """
93
+
94
+ requested_output: dna_output.OutputType
95
+ width: int | None
96
+ aggregation_type: IntervalAggregationType
97
+
98
+ @property
99
+ def base_interval_scorer(self) -> BaseIntervalScorer:
100
+ return BaseIntervalScorer.GENE_MASK
101
+
102
+ @property
103
+ def name(self) -> str:
104
+ return str(self)
105
+
106
+ def __post_init__(self):
107
+ if (
108
+ self.requested_output
109
+ not in SUPPORTED_OUTPUT_TYPES[self.base_interval_scorer]
110
+ ):
111
+ raise ValueError(
112
+ f'Unsupported requested output: {self.requested_output}. Supported'
113
+ f' output types: {SUPPORTED_OUTPUT_TYPES[self.base_interval_scorer]}'
114
+ )
115
+ if self.width not in SUPPORTED_WIDTHS[self.base_interval_scorer]:
116
+ raise ValueError(
117
+ f'Unsupported width: {self.width}. Supported widths:'
118
+ f' {SUPPORTED_WIDTHS[self.base_interval_scorer]}'
119
+ )
120
+
121
+ def to_proto(self) -> dna_model_pb2.IntervalScorer:
122
+ return dna_model_pb2.IntervalScorer(
123
+ gene_mask=dna_model_pb2.GeneMaskIntervalScorer(
124
+ requested_output=self.requested_output.to_proto(),
125
+ width=self.width,
126
+ aggregation_type=self.aggregation_type.to_proto(),
127
+ )
128
+ )
129
+
130
+
131
+ # TypeVar for all interval scorer types.
132
+ IntervalScorerTypes = TypeVar(
133
+ 'IntervalScorerTypes',
134
+ bound=GeneMaskScorer,
135
+ )
136
+
137
+ # A dict of interval scorers, with our recommended settings for a wide range
138
+ # of different use cases and settings.
139
+ RECOMMENDED_INTERVAL_SCORERS = {
140
+ 'RNA_SEQ': GeneMaskScorer(
141
+ requested_output=dna_output.OutputType.RNA_SEQ,
142
+ width=200_001,
143
+ aggregation_type=IntervalAggregationType.MEAN,
144
+ ),
145
+ }
flax_model/alphagenome/_sdk/models/junction_data_utils.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utils for converting JunctionData to/from protos."""
16
+
17
+ from collections.abc import Iterable, Sequence
18
+
19
+ from flax_model.alphagenome._sdk import tensor_utils
20
+ from flax_model.alphagenome._sdk.data import genome
21
+ from flax_model.alphagenome._sdk.data import junction_data
22
+ from flax_model.alphagenome._sdk.data import ontology
23
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
24
+ from flax_model.alphagenome._sdk.protos import tensor_pb2
25
+ import numpy as np
26
+ import pandas as pd
27
+
28
+
29
+ def to_protos(
30
+ data: junction_data.JunctionData,
31
+ *,
32
+ bytes_per_chunk: int = 0,
33
+ compression_type: tensor_pb2.CompressionType = (
34
+ tensor_pb2.CompressionType.COMPRESSION_TYPE_NONE
35
+ ),
36
+ ) -> tuple[dna_model_pb2.JunctionData, Sequence[tensor_pb2.TensorChunk]]:
37
+ """Converts the `JunctionData` to protobuf messages.
38
+
39
+ Args:
40
+ data: The `JunctionData` object to convert to protos.
41
+ bytes_per_chunk: The maximum number of bytes per tensor chunk.
42
+ compression_type: The compression type to use for the tensor chunks.
43
+
44
+ Returns:
45
+ A tuple containing the `JunctionData` protobuf message and a sequence of
46
+ `TensorChunk` protobuf messages.
47
+ """
48
+ tensor, chunks = tensor_utils.pack_tensor(
49
+ data.values,
50
+ bytes_per_chunk=bytes_per_chunk,
51
+ compression_type=compression_type,
52
+ )
53
+
54
+ return (
55
+ dna_model_pb2.JunctionData(
56
+ junctions=[j.to_proto() for j in data.junctions],
57
+ values=tensor,
58
+ metadata=metadata_to_proto(data.metadata).metadata,
59
+ interval=data.interval.to_proto() if data.interval else None,
60
+ ),
61
+ chunks,
62
+ )
63
+
64
+
65
+ def from_protos(
66
+ proto: dna_model_pb2.JunctionData,
67
+ chunks: Iterable[tensor_pb2.TensorChunk] = (),
68
+ *,
69
+ interval: genome.Interval | None = None,
70
+ ) -> junction_data.JunctionData:
71
+ """Converts a `JunctionData` protobuf to a `JunctionData` object.
72
+
73
+ Args:
74
+ proto: A `JunctionData` protobuf message.
75
+ chunks: A sequence of `TensorChunk` protobuf messages.
76
+ interval: Optional `Interval` object representing the genomic region
77
+ containing the junctions. Only used if the proto does not have an
78
+ interval.
79
+
80
+ Returns:
81
+ A `JunctionData` object.
82
+ """
83
+ values = tensor_utils.unpack_proto(proto.values, chunks)
84
+ values = tensor_utils.upcast_floating(values)
85
+
86
+ metadata = metadata_from_proto(
87
+ dna_model_pb2.JunctionsMetadata(metadata=proto.metadata)
88
+ )
89
+
90
+ if proto.HasField('interval'):
91
+ interval = genome.Interval.from_proto(proto.interval)
92
+
93
+ return junction_data.JunctionData(
94
+ junctions=np.array(
95
+ [genome.Interval.from_proto(j) for j in proto.junctions]
96
+ ),
97
+ values=values,
98
+ metadata=metadata,
99
+ interval=interval,
100
+ )
101
+
102
+
103
+ def metadata_to_proto(
104
+ metadata: junction_data.JunctionMetadata,
105
+ ) -> dna_model_pb2.JunctionsMetadata:
106
+ """Converts junction metadata to a JunctionsMetadata.
107
+
108
+ Args:
109
+ metadata: A pandas DataFrame containing junction metadata.
110
+
111
+ Returns:
112
+ A `JunctionsMetadata` protobuf message.
113
+ """
114
+ names = metadata['name']
115
+ default_values = [None] * len(names)
116
+
117
+ columns = zip(
118
+ metadata['name'],
119
+ metadata.get('ontology_curie', default_values),
120
+ metadata.get('biosample_type', default_values),
121
+ metadata.get('biosample_name', default_values),
122
+ metadata.get('biosample_life_stage', default_values),
123
+ metadata.get('gtex_tissue', default_values),
124
+ metadata.get('data_source', default_values),
125
+ metadata.get('Assay title', default_values),
126
+ strict=True,
127
+ )
128
+
129
+ metadata_protos = []
130
+
131
+ for (
132
+ name,
133
+ ontology_curie,
134
+ biosample_type,
135
+ biosample_name,
136
+ biosample_life_stage,
137
+ gtex_tissue,
138
+ data_source,
139
+ assay,
140
+ ) in columns:
141
+ if biosample_type is not None:
142
+ biosample_proto = dna_model_pb2.Biosample(
143
+ name=biosample_name,
144
+ type=dna_model_pb2.BiosampleType.Value(
145
+ f'BIOSAMPLE_TYPE_{biosample_type.upper()}'
146
+ ),
147
+ stage=biosample_life_stage,
148
+ )
149
+ else:
150
+ biosample_proto = None
151
+
152
+ metadata_protos.append(
153
+ dna_model_pb2.JunctionMetadata(
154
+ name=name,
155
+ ontology_term=ontology.from_curie(ontology_curie).to_proto()
156
+ if ontology_curie
157
+ else None,
158
+ biosample=biosample_proto,
159
+ gtex_tissue=gtex_tissue,
160
+ data_source=data_source,
161
+ assay=assay,
162
+ )
163
+ )
164
+
165
+ return dna_model_pb2.JunctionsMetadata(metadata=metadata_protos)
166
+
167
+
168
+ def metadata_from_proto(
169
+ proto: dna_model_pb2.JunctionsMetadata,
170
+ ) -> junction_data.JunctionMetadata:
171
+ """Create JunctionMetadata from a dna_model_pb2.JunctionsMetadata.
172
+
173
+ Args:
174
+ proto: A `JunctionsMetadata` protobuf message.
175
+
176
+ Returns:
177
+ A pandas DataFrame containing junction metadata.
178
+ """
179
+ metadata = []
180
+ for junction_proto in proto.metadata:
181
+ junction_metadata = {
182
+ 'name': junction_proto.name,
183
+ }
184
+
185
+ if junction_proto.HasField('ontology_term'):
186
+ junction_metadata['ontology_curie'] = ontology.from_proto(
187
+ junction_proto.ontology_term
188
+ ).ontology_curie
189
+
190
+ if junction_proto.HasField('biosample'):
191
+ junction_metadata['biosample_name'] = junction_proto.biosample.name
192
+ junction_metadata['biosample_type'] = (
193
+ dna_model_pb2.BiosampleType.Name(junction_proto.biosample.type)
194
+ .removeprefix('BIOSAMPLE_TYPE_')
195
+ .lower()
196
+ )
197
+ if junction_proto.biosample.HasField('stage'):
198
+ junction_metadata['biosample_life_stage'] = (
199
+ junction_proto.biosample.stage
200
+ )
201
+
202
+ if junction_proto.HasField('gtex_tissue'):
203
+ junction_metadata['gtex_tissue'] = junction_proto.gtex_tissue
204
+
205
+ if junction_proto.HasField('data_source'):
206
+ junction_metadata['data_source'] = junction_proto.data_source
207
+
208
+ if junction_proto.HasField('assay'):
209
+ junction_metadata['Assay title'] = junction_proto.assay
210
+
211
+ metadata.append(junction_metadata)
212
+
213
+ if metadata:
214
+ return pd.DataFrame(metadata)
215
+ else:
216
+ return pd.DataFrame(columns=['name'])
flax_model/alphagenome/_sdk/models/track_data_utils.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """Track data utils for converting to/from protos."""
17
+
18
+ from collections.abc import Iterable, Sequence
19
+
20
+ from flax_model.alphagenome._sdk import tensor_utils
21
+ from flax_model.alphagenome._sdk.data import genome
22
+ from flax_model.alphagenome._sdk.data import ontology
23
+ from flax_model.alphagenome._sdk.data import track_data
24
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
25
+ from flax_model.alphagenome._sdk.protos import tensor_pb2
26
+ import pandas as pd
27
+
28
+
29
+ def to_protos(
30
+ data: track_data.TrackData,
31
+ *,
32
+ bytes_per_chunk: int = 0,
33
+ compression_type: tensor_pb2.CompressionType = (
34
+ tensor_pb2.CompressionType.COMPRESSION_TYPE_NONE
35
+ ),
36
+ ) -> tuple[dna_model_pb2.TrackData, Sequence[tensor_pb2.TensorChunk]]:
37
+ """Serializes `TrackData` to protobuf messages.
38
+
39
+ Args:
40
+ data: The `TrackData` object to serialize.
41
+ bytes_per_chunk: The maximum number of bytes per tensor chunk.
42
+ compression_type: The compression type to use for the tensor chunks.
43
+
44
+ Returns:
45
+ A tuple containing the `TrackData` protobuf message and a sequence of
46
+ `TensorChunk` protobuf messages.
47
+ """
48
+ tensor, chunks = tensor_utils.pack_tensor(
49
+ data.values,
50
+ bytes_per_chunk=bytes_per_chunk,
51
+ compression_type=compression_type,
52
+ )
53
+ return (
54
+ dna_model_pb2.TrackData(
55
+ values=tensor,
56
+ metadata=metadata_to_proto(data.metadata).metadata,
57
+ resolution=data.resolution,
58
+ interval=data.interval.to_proto() if data.interval else None,
59
+ ),
60
+ chunks,
61
+ )
62
+
63
+
64
+ def from_protos(
65
+ proto: dna_model_pb2.TrackData,
66
+ chunks: Iterable[tensor_pb2.TensorChunk] = (),
67
+ *,
68
+ interval: genome.Interval | None = None,
69
+ ) -> track_data.TrackData:
70
+ """Creates a `TrackData` object from protobuf messages.
71
+
72
+ Args:
73
+ proto: A `TrackData` protobuf message.
74
+ chunks: A sequence of `TensorChunk` protobuf messages.
75
+ interval: Optional `Interval` object representing the genomic region
76
+ containing the tracks. Only used if the proto does not have an interval.
77
+
78
+ Returns:
79
+ A `TrackData` object.
80
+ """
81
+ metadata = metadata_from_proto(
82
+ dna_model_pb2.TracksMetadata(metadata=proto.metadata)
83
+ )
84
+
85
+ values = tensor_utils.unpack_proto(proto.values, chunks)
86
+ values = tensor_utils.upcast_floating(values)
87
+ resolution = proto.resolution if proto.HasField('resolution') else 1
88
+ if proto.HasField('interval'):
89
+ interval = genome.Interval.from_proto(proto.interval)
90
+
91
+ return track_data.TrackData(
92
+ values, metadata, resolution=resolution, interval=interval
93
+ )
94
+
95
+
96
+ def metadata_to_proto(
97
+ metadata: track_data.TrackMetadata,
98
+ ) -> dna_model_pb2.TracksMetadata:
99
+ """Converts track metadata to a `TracksMetadata` protobuf message.
100
+
101
+ Args:
102
+ metadata: A pandas DataFrame containing track metadata, with the following
103
+ required columns: name, strand.
104
+
105
+ Returns:
106
+ A `TracksMetadata` protobuf message.
107
+ """
108
+ names = metadata['name']
109
+ default_values = [None] * len(names)
110
+
111
+ columns = zip(
112
+ metadata['name'],
113
+ metadata['strand'],
114
+ metadata.get('ontology_curie', default_values),
115
+ metadata.get('biosample_type', default_values),
116
+ metadata.get('biosample_name', default_values),
117
+ metadata.get('biosample_life_stage', default_values),
118
+ metadata.get('transcription_factor', default_values),
119
+ metadata.get('histone_mark', default_values),
120
+ metadata.get('gtex_tissue', default_values),
121
+ metadata.get('Assay title', default_values),
122
+ metadata.get('data_source', default_values),
123
+ metadata.get('genetically_modified', default_values),
124
+ metadata.get('endedness', default_values),
125
+ metadata.get('nonzero_mean', default_values),
126
+ strict=True,
127
+ )
128
+
129
+ metadata_protos = []
130
+ for (
131
+ name,
132
+ strand,
133
+ ontology_curie,
134
+ biosample_type,
135
+ biosample_name,
136
+ biosample_life_stage,
137
+ transcription_factor,
138
+ histone_mark,
139
+ gtex_tissue,
140
+ assay,
141
+ data_source,
142
+ genetically_modified,
143
+ endedness,
144
+ nonzero_mean,
145
+ ) in columns:
146
+ if biosample_type:
147
+ biosample = dna_model_pb2.Biosample(
148
+ name=biosample_name,
149
+ type=dna_model_pb2.BiosampleType.Value(
150
+ f'BIOSAMPLE_TYPE_{biosample_type.upper()}'
151
+ ),
152
+ stage=biosample_life_stage,
153
+ )
154
+ else:
155
+ biosample = None
156
+ if endedness is not None:
157
+ match endedness:
158
+ case 'paired':
159
+ endedness = dna_model_pb2.Endedness.ENDEDNESS_PAIRED
160
+ case 'single':
161
+ endedness = dna_model_pb2.Endedness.ENDEDNESS_SINGLE
162
+ case _:
163
+ raise ValueError(f'Unknown endedness: {endedness}')
164
+
165
+ metadata_protos.append(
166
+ dna_model_pb2.TrackMetadata(
167
+ name=name,
168
+ strand=genome.Strand.from_str(strand).to_proto()
169
+ if strand
170
+ else None,
171
+ ontology_term=ontology.from_curie(ontology_curie).to_proto()
172
+ if ontology_curie
173
+ else None,
174
+ biosample=biosample,
175
+ transcription_factor_code=transcription_factor
176
+ if isinstance(transcription_factor, str)
177
+ else None,
178
+ histone_mark_code=histone_mark
179
+ if isinstance(histone_mark, str)
180
+ else None,
181
+ gtex_tissue=gtex_tissue,
182
+ assay=assay,
183
+ data_source=data_source,
184
+ genetically_modified=genetically_modified,
185
+ endedness=endedness,
186
+ nonzero_mean=nonzero_mean,
187
+ )
188
+ )
189
+
190
+ return dna_model_pb2.TracksMetadata(metadata=metadata_protos)
191
+
192
+
193
+ def metadata_from_proto(
194
+ proto: dna_model_pb2.TracksMetadata,
195
+ ) -> track_data.TrackMetadata:
196
+ """Creates track metadata from a `TracksMetadata` protobuf message.
197
+
198
+ Args:
199
+ proto: A `TracksMetadata` protobuf message.
200
+
201
+ Returns:
202
+ A pandas DataFrame containing track metadata.
203
+ """
204
+ metadata = []
205
+ for track_proto in proto.metadata:
206
+ track_metadata = {
207
+ 'name': track_proto.name,
208
+ 'strand': str(genome.Strand.from_proto(track_proto.strand)),
209
+ }
210
+
211
+ if track_proto.HasField('assay'):
212
+ track_metadata['Assay title'] = track_proto.assay
213
+
214
+ if track_proto.HasField('ontology_term'):
215
+ track_metadata['ontology_curie'] = ontology.from_proto(
216
+ track_proto.ontology_term
217
+ ).ontology_curie
218
+
219
+ if track_proto.HasField('biosample'):
220
+ track_metadata['biosample_name'] = track_proto.biosample.name
221
+ track_metadata['biosample_type'] = (
222
+ dna_model_pb2.BiosampleType.Name(track_proto.biosample.type)
223
+ .removeprefix('BIOSAMPLE_TYPE_')
224
+ .lower()
225
+ )
226
+ if track_proto.biosample.HasField('stage'):
227
+ track_metadata['biosample_life_stage'] = track_proto.biosample.stage
228
+
229
+ if track_proto.HasField('transcription_factor_code'):
230
+ track_metadata['transcription_factor'] = (
231
+ track_proto.transcription_factor_code
232
+ )
233
+
234
+ if track_proto.HasField('histone_mark_code'):
235
+ track_metadata['histone_mark'] = track_proto.histone_mark_code
236
+
237
+ if track_proto.HasField('gtex_tissue'):
238
+ track_metadata['gtex_tissue'] = track_proto.gtex_tissue
239
+
240
+ if track_proto.HasField('data_source'):
241
+ track_metadata['data_source'] = track_proto.data_source
242
+
243
+ if track_proto.HasField('endedness'):
244
+ track_metadata['endedness'] = (
245
+ dna_model_pb2.Endedness.Name(track_proto.endedness)
246
+ .removeprefix('ENDEDNESS_')
247
+ .lower()
248
+ )
249
+
250
+ if track_proto.HasField('genetically_modified'):
251
+ track_metadata['genetically_modified'] = track_proto.genetically_modified
252
+
253
+ if track_proto.HasField('nonzero_mean'):
254
+ track_metadata['nonzero_mean'] = track_proto.nonzero_mean
255
+
256
+ metadata.append(track_metadata)
257
+ if metadata:
258
+ return pd.DataFrame(metadata)
259
+ else:
260
+ return pd.DataFrame(columns=['name', 'strand'])
flax_model/alphagenome/_sdk/models/variant_scorers.py ADDED
@@ -0,0 +1,878 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Module containing variant scorer dataclasses for variant scoring."""
16
+
17
+ from collections.abc import Sequence
18
+ import dataclasses
19
+ import enum
20
+ import itertools
21
+ import math
22
+ from typing import TypeVar
23
+
24
+ from flax_model.alphagenome._sdk.models import dna_output
25
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2
26
+ import anndata
27
+ import immutabledict
28
+ import pandas as pd
29
+
30
+
31
+ class AggregationType(enum.Enum):
32
+ """Enum indicating the type of variant scorer aggregation.
33
+
34
+ Attributes:
35
+ DIFF_MEAN: Difference of means, i.e., mean(`ALT`) - mean(`REF`).
36
+ DIFF_SUM: Difference of sums, i.e., sum(`ALT`) - sum(`REF`).
37
+ DIFF_SUM_LOG2: Log scales predictions, then takes the sum and then the
38
+ difference, i.e., sum(log2(`ALT`)) - sum(log2(`REF`)).
39
+ DIFF_LOG2_SUM: Takes the sum of predictions, applies a log transform, then
40
+ takes the difference between predictions, i.e., log2(sum(`ALT`)) -
41
+ log2(sum(`REF`)).
42
+ L2_DIFF: Takes the difference of `ALT` and `REF` predictions, then computes
43
+ the L2 norm, i.e., l2_norm(`ALT` - `REF`).
44
+ L2_DIFF_LOG1P: Log scales the predictions + 1, takes the difference of `ALT`
45
+ and `REF` predictions, then computes the L2 norm, i.e.,
46
+ l2_norm(log1p(`ALT`) - log1p(`REF`)).
47
+ ACTIVE_MEAN: Maximum of means, i.e., max(mean(`ALT`), mean(`REF`)).
48
+ ACTIVE_SUM: Maximum of sums, i.e., max(sum(`ALT), sum(`REF`)).
49
+
50
+ Methods:
51
+ to_proto: Converts the aggregation type to its corresponding proto enum.
52
+ """
53
+
54
+ DIFF_MEAN = dna_model_pb2.AggregationType.AGGREGATION_TYPE_DIFF_MEAN
55
+ DIFF_SUM = dna_model_pb2.AggregationType.AGGREGATION_TYPE_DIFF_SUM
56
+ DIFF_SUM_LOG2 = dna_model_pb2.AggregationType.AGGREGATION_TYPE_DIFF_SUM_LOG2
57
+ DIFF_LOG2_SUM = dna_model_pb2.AggregationType.AGGREGATION_TYPE_DIFF_LOG2_SUM
58
+ L2_DIFF = dna_model_pb2.AggregationType.AGGREGATION_TYPE_L2_DIFF
59
+ L2_DIFF_LOG1P = dna_model_pb2.AggregationType.AGGREGATION_TYPE_L2_DIFF_LOG1P
60
+ ACTIVE_MEAN = dna_model_pb2.AggregationType.AGGREGATION_TYPE_ACTIVE_MEAN
61
+ ACTIVE_SUM = dna_model_pb2.AggregationType.AGGREGATION_TYPE_ACTIVE_SUM
62
+
63
+ def to_proto(self) -> dna_model_pb2.AggregationType:
64
+ return self.value
65
+
66
+ def __repr__(self) -> str:
67
+ return self.name
68
+
69
+
70
+ class BaseVariantScorer(enum.Enum):
71
+ """Enum indicating the type of variant scoring.
72
+
73
+ Attributes:
74
+ CENTER_MASK: Center mask scorer.
75
+ CONTACT_MAP: Contact map scorer, a center mask scorer that handles 2D
76
+ tensors.
77
+ GENE_MASK_LFC: Gene-mask scoring based on log fold change.
78
+ GENE_MASK_ACTIVE: Gene-mask scoring based on active allele.
79
+ GENE_MASK_SPLICING: Gene-mask scoring for splicing.
80
+ PA_QTL: Polyadenylation QTL scoring.
81
+ SPLICE_JUNCTION: Splice junction scoring.
82
+ """
83
+
84
+ CENTER_MASK = enum.auto()
85
+ CONTACT_MAP = enum.auto()
86
+ GENE_MASK_LFC = enum.auto()
87
+ GENE_MASK_ACTIVE = enum.auto()
88
+ GENE_MASK_SPLICING = enum.auto()
89
+ PA_QTL = enum.auto()
90
+ SPLICE_JUNCTION = enum.auto()
91
+
92
+
93
+ SUPPORTED_ORGANISMS = immutabledict.immutabledict({
94
+ BaseVariantScorer.CENTER_MASK: dna_model_pb2.Organism.values(),
95
+ BaseVariantScorer.CONTACT_MAP: dna_model_pb2.Organism.values(),
96
+ BaseVariantScorer.GENE_MASK_LFC: dna_model_pb2.Organism.values(),
97
+ BaseVariantScorer.GENE_MASK_ACTIVE: dna_model_pb2.Organism.values(),
98
+ BaseVariantScorer.GENE_MASK_SPLICING: dna_model_pb2.Organism.values(),
99
+ BaseVariantScorer.PA_QTL: [dna_model_pb2.Organism.ORGANISM_HOMO_SAPIENS],
100
+ BaseVariantScorer.SPLICE_JUNCTION: dna_model_pb2.Organism.values(),
101
+ })
102
+
103
+ SUPPORTED_OUTPUT_TYPES = immutabledict.immutabledict({
104
+ BaseVariantScorer.CENTER_MASK: [
105
+ dna_output.OutputType.ATAC,
106
+ dna_output.OutputType.CAGE,
107
+ dna_output.OutputType.DNASE,
108
+ dna_output.OutputType.PROCAP,
109
+ dna_output.OutputType.RNA_SEQ,
110
+ dna_output.OutputType.CHIP_HISTONE,
111
+ dna_output.OutputType.CHIP_TF,
112
+ dna_output.OutputType.SPLICE_SITES,
113
+ dna_output.OutputType.SPLICE_SITE_USAGE,
114
+ ],
115
+ BaseVariantScorer.CONTACT_MAP: [dna_output.OutputType.CONTACT_MAPS],
116
+ BaseVariantScorer.GENE_MASK_LFC: [
117
+ dna_output.OutputType.ATAC,
118
+ dna_output.OutputType.CAGE,
119
+ dna_output.OutputType.DNASE,
120
+ dna_output.OutputType.PROCAP,
121
+ dna_output.OutputType.RNA_SEQ,
122
+ dna_output.OutputType.SPLICE_SITES,
123
+ dna_output.OutputType.SPLICE_SITE_USAGE,
124
+ ],
125
+ BaseVariantScorer.GENE_MASK_ACTIVE: [
126
+ dna_output.OutputType.ATAC,
127
+ dna_output.OutputType.CAGE,
128
+ dna_output.OutputType.DNASE,
129
+ dna_output.OutputType.PROCAP,
130
+ dna_output.OutputType.RNA_SEQ,
131
+ dna_output.OutputType.SPLICE_SITES,
132
+ dna_output.OutputType.SPLICE_SITE_USAGE,
133
+ ],
134
+ BaseVariantScorer.GENE_MASK_SPLICING: [
135
+ dna_output.OutputType.SPLICE_SITES,
136
+ dna_output.OutputType.SPLICE_SITE_USAGE,
137
+ ],
138
+ })
139
+
140
+ SUPPORTED_WIDTHS = immutabledict.immutabledict({
141
+ BaseVariantScorer.CENTER_MASK: [None, 501, 2001, 10_001, 100_001, 200_001],
142
+ BaseVariantScorer.GENE_MASK_SPLICING: [None, 101, 1_001, 10_001],
143
+ })
144
+
145
+ SUPPORTED_AGGREGATIONS = immutabledict.immutabledict({
146
+ BaseVariantScorer.CENTER_MASK: [
147
+ AggregationType.DIFF_MEAN,
148
+ AggregationType.DIFF_SUM,
149
+ AggregationType.DIFF_SUM_LOG2,
150
+ AggregationType.DIFF_LOG2_SUM,
151
+ AggregationType.L2_DIFF,
152
+ AggregationType.L2_DIFF_LOG1P,
153
+ AggregationType.ACTIVE_MEAN,
154
+ AggregationType.ACTIVE_SUM,
155
+ ],
156
+ })
157
+
158
+
159
+ @dataclasses.dataclass(frozen=True)
160
+ class CenterMaskScorer:
161
+ """Variant scorer for center mask scorers.
162
+
163
+ Variant scorer that aggregates ALT and REF values using a spatial mask
164
+ centered around the variant before computing the difference. This scorer
165
+ aggregates over the spatial axis and returns one score per output track.
166
+
167
+ Attributes:
168
+ requested_output: The requested output type (e.g., ATAC, DNASE, etc.)
169
+ width: The width of the mask around the variant. If None, the score is
170
+ computed over the entire sequence.
171
+ aggregation_type: The aggregation type.
172
+ base_variant_scorer: The base variant scorer.
173
+ name: The name of the scorer (a composite of the above attributes that
174
+ uniquely identifies a scorer combination).
175
+ is_signed: Whether this variant scorer is directional (such that scores may
176
+ be negative or positive) or non-directional (scores are always positive).
177
+
178
+ Methods:
179
+ to_proto: Converts the scorer to its corresponding proto message.
180
+
181
+ Raises:
182
+ ValueError: If the requested output, width, or aggregation type is not
183
+ supported.
184
+ """
185
+
186
+ requested_output: dna_output.OutputType
187
+ width: int | None
188
+ aggregation_type: AggregationType
189
+
190
+ @property
191
+ def base_variant_scorer(self) -> BaseVariantScorer:
192
+ return BaseVariantScorer.CENTER_MASK
193
+
194
+ @property
195
+ def name(self) -> str:
196
+ return str(self)
197
+
198
+ @property
199
+ def is_signed(self) -> bool:
200
+ # The 'active' scorers track the maximum of the ALT and REF, and are
201
+ # non-directional. All other scorers track ALT - REF, with L2_DIFF
202
+ # nondirectional and the remaining directional.
203
+ return self.aggregation_type in [
204
+ AggregationType.DIFF_MEAN,
205
+ AggregationType.DIFF_SUM,
206
+ AggregationType.DIFF_SUM_LOG2,
207
+ AggregationType.DIFF_LOG2_SUM,
208
+ ]
209
+
210
+ def __post_init__(self):
211
+ if (
212
+ self.requested_output
213
+ not in SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]
214
+ ):
215
+ raise ValueError(
216
+ f'Unsupported requested output: {self.requested_output}. Supported'
217
+ f' output types: {SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]}'
218
+ )
219
+ if self.width not in SUPPORTED_WIDTHS[self.base_variant_scorer]:
220
+ raise ValueError(
221
+ f'Unsupported width: {self.width}. Supported widths:'
222
+ f' {SUPPORTED_WIDTHS[self.base_variant_scorer]}'
223
+ )
224
+ if (
225
+ self.aggregation_type
226
+ not in SUPPORTED_AGGREGATIONS[self.base_variant_scorer]
227
+ ):
228
+ raise ValueError(
229
+ f'Unsupported aggregation type: {self.aggregation_type}. Supported'
230
+ ' aggregation types:'
231
+ f' {SUPPORTED_AGGREGATIONS[self.base_variant_scorer]}'
232
+ )
233
+
234
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
235
+ return dna_model_pb2.VariantScorer(
236
+ center_mask=dna_model_pb2.CenterMaskScorer(
237
+ requested_output=self.requested_output.to_proto(),
238
+ width=self.width,
239
+ aggregation_type=self.aggregation_type.to_proto(),
240
+ )
241
+ )
242
+
243
+
244
+ @dataclasses.dataclass(frozen=True)
245
+ class ContactMapScorer:
246
+ """Variant scorer for contact map scorers.
247
+
248
+ Variant scorer that quantifies local contact disruption between ALT and REF
249
+ alleles. Uses a 1MB window centered around the variant to calculate the
250
+ mean absolute difference of contact frequencies, for all interactions
251
+ involving the variant-containing bin.
252
+
253
+ Attributes:
254
+ requested_output: The requested output type (e.g., CONTACT_MAPS).
255
+ base_variant_scorer: The base variant scorer.
256
+ name: The name of the scorer (a composite of the above attributes that
257
+ uniquely identifies a scorer combination).
258
+ is_signed: Whether this variant scorer is directional (such that scores may
259
+ be negative or positive) or non-directional (scores are always positive).
260
+
261
+ Methods:
262
+ to_proto: Converts the scorer to its corresponding proto message.
263
+ """
264
+
265
+ @property
266
+ def base_variant_scorer(self) -> BaseVariantScorer:
267
+ return BaseVariantScorer.CONTACT_MAP
268
+
269
+ @property
270
+ def name(self) -> str:
271
+ return str(self)
272
+
273
+ @property
274
+ def is_signed(self) -> bool:
275
+ return False
276
+
277
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
278
+ return dna_model_pb2.VariantScorer(
279
+ contact_map=dna_model_pb2.ContactMapScorer()
280
+ )
281
+
282
+ @property
283
+ def requested_output(self) -> dna_output.OutputType:
284
+ return dna_output.OutputType.CONTACT_MAPS
285
+
286
+
287
+ @dataclasses.dataclass(frozen=True)
288
+ class GeneMaskLFCScorer:
289
+ """Variant scorer for gene-mask log fold change scoring.
290
+
291
+ Variant scorer that quantifies impact on overall gene transcript abundance.
292
+ Calculates the log fold change of gene expression level between ALT and REF
293
+ alleles using a gene exon mask.
294
+
295
+ Attributes:
296
+ requested_output: The requested output type (e.g. ATAC, DNASE, etc.)
297
+ base_variant_scorer: The base variant scorer.
298
+ name: The name of the scorer (a composite of the above attributes that
299
+ uniquely identifies a scorer combination).
300
+ is_signed: Whether this variant scorer is directional (such that scores may
301
+ be negative or positive) or non-directional (scores are always positive).
302
+
303
+ Methods:
304
+ to_proto: Converts the scorer to its corresponding proto message.
305
+
306
+ Raises:
307
+ ValueError: If the requested output is not supported.
308
+ """
309
+
310
+ requested_output: dna_output.OutputType
311
+
312
+ @property
313
+ def base_variant_scorer(self) -> BaseVariantScorer:
314
+ return BaseVariantScorer.GENE_MASK_LFC
315
+
316
+ @property
317
+ def name(self) -> str:
318
+ return str(self)
319
+
320
+ @property
321
+ def is_signed(self) -> bool:
322
+ return True
323
+
324
+ def __post_init__(self):
325
+ if (
326
+ self.requested_output
327
+ not in SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]
328
+ ):
329
+ raise ValueError(
330
+ f'Unsupported requested output: {self.requested_output}. Supported'
331
+ f' output types: {SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]}'
332
+ )
333
+
334
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
335
+ return dna_model_pb2.VariantScorer(
336
+ gene_mask=dna_model_pb2.GeneMaskLFCScorer(
337
+ requested_output=self.requested_output.to_proto(),
338
+ )
339
+ )
340
+
341
+
342
+ @dataclasses.dataclass(frozen=True)
343
+ class GeneMaskActiveScorer:
344
+ """Variant scorer for gene-mask active allele scoring.
345
+
346
+ Variant scorer that captures absolute activity level associated with one of
347
+ the alleles, rather than quantifying the difference between the ALT and REF.
348
+ Active allele gene scores are calculated by taking the maximum of the
349
+ aggregated ALT and REF signals across exons for a gene of interest.
350
+
351
+ Attributes:
352
+ requested_output: The requested output type (e.g. ATAC, DNASE, etc.)
353
+ base_variant_scorer: The base variant scorer.
354
+ name: The name of the scorer (a composite of the above attributes that
355
+ uniquely identifies a scorer combination).
356
+ is_signed: Whether this variant scorer is directional (such that scores may
357
+ be negative or positive) or non-directional (scores are always positive).
358
+
359
+ Methods:
360
+ to_proto: Converts the scorer to its corresponding proto message.
361
+
362
+ Raises:
363
+ ValueError: If the requested output is not supported.
364
+ """
365
+
366
+ requested_output: dna_output.OutputType
367
+
368
+ @property
369
+ def base_variant_scorer(self) -> BaseVariantScorer:
370
+ return BaseVariantScorer.GENE_MASK_ACTIVE
371
+
372
+ @property
373
+ def name(self) -> str:
374
+ return str(self)
375
+
376
+ @property
377
+ def is_signed(self) -> bool:
378
+ return False
379
+
380
+ def __post_init__(self):
381
+ if (
382
+ self.requested_output
383
+ not in SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]
384
+ ):
385
+ raise ValueError(
386
+ f'Unsupported requested output: {self.requested_output}. Supported'
387
+ f' output types: {SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]}'
388
+ )
389
+
390
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
391
+ return dna_model_pb2.VariantScorer(
392
+ gene_mask_active=dna_model_pb2.GeneMaskActiveScorer(
393
+ requested_output=self.requested_output.to_proto(),
394
+ )
395
+ )
396
+
397
+
398
+ @dataclasses.dataclass(frozen=True)
399
+ class GeneMaskSplicingScorer:
400
+ """Variant scorer for gene-mask scoring for splicing.
401
+
402
+ Variant scorer that quantifies changes in class assignment probabilties
403
+ (dna_output.OutputType.SPLICE_SITES) or changes in the usage of splice sites
404
+ (dna_output.OutputType.SPLICE_SITE_USAGE) between ALT and REF alleles using
405
+ a gene exon mask.
406
+
407
+ Attributes:
408
+ requested_output: The requested output type (e.g. ATAC, DNASE, etc.)
409
+ width: The width of the mask around the variant.
410
+ base_variant_scorer: The base variant scorer.
411
+ name: The name of the scorer (a composite of the above attributes that
412
+ uniquely identifies a scorer combination).
413
+ is_signed: Whether this variant scorer is directional (such that scores may
414
+ be negative or positive) or non-directional (scores are always positive).
415
+
416
+ Methods:
417
+ to_proto: Converts the scorer to its corresponding proto message.
418
+
419
+ Raises:
420
+ ValueError: If the requested output or width is not supported.
421
+ """
422
+
423
+ requested_output: dna_output.OutputType
424
+ width: int | None
425
+
426
+ @property
427
+ def base_variant_scorer(self) -> BaseVariantScorer:
428
+ return BaseVariantScorer.GENE_MASK_SPLICING
429
+
430
+ @property
431
+ def name(self) -> str:
432
+ return str(self)
433
+
434
+ @property
435
+ def is_signed(self) -> bool:
436
+ return False
437
+
438
+ def __post_init__(self):
439
+ if (
440
+ self.requested_output
441
+ not in SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]
442
+ ):
443
+ raise ValueError(
444
+ f'Unsupported requested output: {self.requested_output}. Supported'
445
+ f' output types: {SUPPORTED_OUTPUT_TYPES[self.base_variant_scorer]}'
446
+ )
447
+ if self.width not in SUPPORTED_WIDTHS[self.base_variant_scorer]:
448
+ raise ValueError(
449
+ f'Unsupported width: {self.width}. Supported widths:'
450
+ f' {SUPPORTED_WIDTHS[self.base_variant_scorer]}'
451
+ )
452
+
453
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
454
+ return dna_model_pb2.VariantScorer(
455
+ gene_mask_splicing=dna_model_pb2.GeneMaskSplicingScorer(
456
+ requested_output=self.requested_output.to_proto(),
457
+ width=self.width,
458
+ )
459
+ )
460
+
461
+
462
+ @dataclasses.dataclass(frozen=True)
463
+ class PolyadenylationScorer:
464
+ """Variant scorer for polyadenylation QTLs (paQTLs).
465
+
466
+ Variant scorer for polyadenylation quantitative trait loci (paQTLs) to capture
467
+ a variant's impact on RNA isoform production.
468
+
469
+ The scoring approach quantifies the maximum log fold change in expression
470
+ between the set of proximal polyadenylation sites (PAS)s vs. the set of
471
+ distal PASs, where the sets of proximal and distal PASs are the PASs upstream
472
+ or downstream of any given 3' cleavage site respectively.
473
+
474
+ Attributes:
475
+ base_variant_scorer: The base variant scorer.
476
+ name: The name of the scorer (a composite of the above attributes that
477
+ uniquely identifies a scorer combination).
478
+ is_signed: Whether this variant scorer is directional (such that scores may
479
+ be negative or positive) or non-directional (scores are always positive).
480
+
481
+ Methods:
482
+ to_proto: Converts the scorer to its corresponding proto message.
483
+ """
484
+
485
+ @property
486
+ def requested_output(self) -> dna_output.OutputType:
487
+ """Returns the scorer's underlying output type."""
488
+ return dna_output.OutputType.RNA_SEQ
489
+
490
+ @property
491
+ def base_variant_scorer(self) -> BaseVariantScorer:
492
+ return BaseVariantScorer.PA_QTL
493
+
494
+ @property
495
+ def name(self) -> str:
496
+ return str(self)
497
+
498
+ @property
499
+ def is_signed(self) -> bool:
500
+ return False
501
+
502
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
503
+ return dna_model_pb2.VariantScorer(
504
+ pa_qtl=dna_model_pb2.PolyadenylationScorer()
505
+ )
506
+
507
+
508
+ @dataclasses.dataclass(frozen=True)
509
+ class SpliceJunctionScorer:
510
+ """Variant scorer for splice junction scoring.
511
+
512
+ Attributes:
513
+ base_variant_scorer: The base variant scorer.
514
+ name: The name of the scorer (a composite of the above attributes that
515
+ uniquely identifies a scorer combination).
516
+ is_signed: Whether this variant scorer is directional (such that scores may
517
+ be negative or positive) or non-directional (scores are always positive).
518
+
519
+ Methods:
520
+ to_proto: Converts the scorer to its corresponding proto message.
521
+ """
522
+
523
+ @property
524
+ def requested_output(self) -> dna_output.OutputType:
525
+ """Returns the scorer's underlying output type."""
526
+ return dna_output.OutputType.SPLICE_JUNCTIONS
527
+
528
+ @property
529
+ def base_variant_scorer(self) -> BaseVariantScorer:
530
+ return BaseVariantScorer.SPLICE_JUNCTION
531
+
532
+ @property
533
+ def name(self) -> str:
534
+ return str(self)
535
+
536
+ @property
537
+ def is_signed(self) -> bool:
538
+ return False
539
+
540
+ def to_proto(self) -> dna_model_pb2.VariantScorer:
541
+ return dna_model_pb2.VariantScorer(
542
+ splice_junction=dna_model_pb2.SpliceJunctionScorer()
543
+ )
544
+
545
+
546
+ # TypeVar for all variant scorer types.
547
+ VariantScorerTypes = TypeVar(
548
+ 'VariantScorerTypes',
549
+ CenterMaskScorer,
550
+ ContactMapScorer,
551
+ GeneMaskLFCScorer,
552
+ GeneMaskActiveScorer,
553
+ GeneMaskSplicingScorer,
554
+ PolyadenylationScorer,
555
+ SpliceJunctionScorer,
556
+ )
557
+
558
+ # A dict of variant scorers, with our recommended settings for a wide range
559
+ # of different use cases and settings.
560
+ RECOMMENDED_VARIANT_SCORERS = immutabledict.immutabledict({
561
+ 'ATAC': CenterMaskScorer(
562
+ requested_output=dna_output.OutputType.ATAC,
563
+ width=501,
564
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
565
+ ),
566
+ 'CONTACT_MAPS': ContactMapScorer(),
567
+ 'DNASE': CenterMaskScorer(
568
+ requested_output=dna_output.OutputType.DNASE,
569
+ width=501,
570
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
571
+ ),
572
+ 'CHIP_TF': CenterMaskScorer(
573
+ requested_output=dna_output.OutputType.CHIP_TF,
574
+ width=501,
575
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
576
+ ),
577
+ 'CHIP_HISTONE': CenterMaskScorer(
578
+ requested_output=dna_output.OutputType.CHIP_HISTONE,
579
+ width=2001,
580
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
581
+ ),
582
+ 'CAGE': CenterMaskScorer(
583
+ requested_output=dna_output.OutputType.CAGE,
584
+ width=501,
585
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
586
+ ),
587
+ 'PROCAP': CenterMaskScorer(
588
+ requested_output=dna_output.OutputType.PROCAP,
589
+ width=501,
590
+ aggregation_type=AggregationType.DIFF_LOG2_SUM,
591
+ ),
592
+ # Expression log fold change.
593
+ 'RNA_SEQ': GeneMaskLFCScorer(
594
+ requested_output=dna_output.OutputType.RNA_SEQ
595
+ ),
596
+ 'RNA_SEQ_ACTIVE': GeneMaskActiveScorer(
597
+ requested_output=dna_output.OutputType.RNA_SEQ
598
+ ),
599
+ 'SPLICE_SITES': GeneMaskSplicingScorer(
600
+ requested_output=dna_output.OutputType.SPLICE_SITES,
601
+ width=None,
602
+ ),
603
+ 'SPLICE_SITE_USAGE': GeneMaskSplicingScorer(
604
+ requested_output=dna_output.OutputType.SPLICE_SITE_USAGE,
605
+ width=None,
606
+ ),
607
+ 'SPLICE_JUNCTIONS': SpliceJunctionScorer(),
608
+ 'POLYADENYLATION': PolyadenylationScorer(),
609
+ 'ATAC_ACTIVE': CenterMaskScorer(
610
+ requested_output=dna_output.OutputType.ATAC,
611
+ width=501,
612
+ aggregation_type=AggregationType.ACTIVE_SUM,
613
+ ),
614
+ 'DNASE_ACTIVE': CenterMaskScorer(
615
+ requested_output=dna_output.OutputType.DNASE,
616
+ width=501,
617
+ aggregation_type=AggregationType.ACTIVE_SUM,
618
+ ),
619
+ 'CHIP_TF_ACTIVE': CenterMaskScorer(
620
+ requested_output=dna_output.OutputType.CHIP_TF,
621
+ width=501,
622
+ aggregation_type=AggregationType.ACTIVE_SUM,
623
+ ),
624
+ 'CHIP_HISTONE_ACTIVE': CenterMaskScorer(
625
+ requested_output=dna_output.OutputType.CHIP_HISTONE,
626
+ width=2001,
627
+ aggregation_type=AggregationType.ACTIVE_SUM,
628
+ ),
629
+ 'CAGE_ACTIVE': CenterMaskScorer(
630
+ requested_output=dna_output.OutputType.CAGE,
631
+ width=501,
632
+ aggregation_type=AggregationType.ACTIVE_SUM,
633
+ ),
634
+ 'PROCAP_ACTIVE': CenterMaskScorer(
635
+ requested_output=dna_output.OutputType.PROCAP,
636
+ width=501,
637
+ aggregation_type=AggregationType.ACTIVE_SUM,
638
+ ),
639
+ })
640
+
641
+
642
+ def get_recommended_scorers(
643
+ organism: dna_model_pb2.Organism,
644
+ ) -> list[VariantScorerTypes]:
645
+ """Returns the recommended variant scorers for a given organism."""
646
+ return [
647
+ scorer
648
+ for scorer in RECOMMENDED_VARIANT_SCORERS.values()
649
+ if organism in SUPPORTED_ORGANISMS[scorer.base_variant_scorer]
650
+ ]
651
+
652
+
653
+ def tidy_anndata(
654
+ adata: anndata.AnnData,
655
+ match_gene_strand: bool = True,
656
+ include_extended_metadata: bool = True,
657
+ ) -> pd.DataFrame:
658
+ """Formats an :class:`~anndata.AnnData` score as a tidy DataFrame.
659
+
660
+ This function converts the score output from an AnnData object into a
661
+ long-format pandas DataFrame, where each row represents:
662
+
663
+ - For non-gene-centric variant scorers: Score for a variant-track pair.
664
+ - For non-gene-centric interval scorers: Score for an interval-track pair.
665
+ - For gene-centric variant/interval scoring: Score for a
666
+ variant/interval-gene-track combination.
667
+
668
+ Args:
669
+ adata: An AnnData object containing scores.
670
+ match_gene_strand: If True (and using gene-centric scoring), rows with
671
+ mismatched gene and track strands are removed.
672
+ include_extended_metadata: If True, includes additional columns derived from
673
+ metadata specific to the output type, such as biosample name and type,
674
+ gtex tissue, transcription factor, and histone mark, if available. If
675
+ False, only includes minimal metadata columns required to unique identify
676
+ a track withing a given output type: track_name and track_strand.
677
+
678
+ Returns:
679
+ A pandas DataFrame with one score per row. The DataFrame includes
680
+ columns for variant ID (if applicable), scored interval, gene information
681
+ (if applicable), output type, variant/interval scorer, track name,
682
+ ontology term, assay type, track strand, and raw score. Additional metadata
683
+ such as biosample name and type, gtex tissue are also returned (where
684
+ available). See :func:`full_path_to.tidy_scores` for more details on the
685
+ returned columns.
686
+
687
+ Raises:
688
+ ValueError: If the input is not an AnnData object.
689
+ """
690
+ if not isinstance(adata, anndata.AnnData):
691
+ raise ValueError('Invalid input type. Must be an AnnData object.')
692
+
693
+ # Columns to include from the gene metadata. If the column did not
694
+ # exist in the original metadata (or if the scores do not have a concept
695
+ # of a gene), a column of all Nones is added.
696
+ gene_columns = [
697
+ 'gene_id',
698
+ 'gene_name',
699
+ 'gene_type',
700
+ 'gene_strand',
701
+ 'junction_Start',
702
+ 'junction_End',
703
+ ]
704
+ if math.prod(adata.X.shape) == 0:
705
+ # Scores are empty, so we return an empty dataframe.
706
+ return pd.DataFrame()
707
+ elif 'gene_id' in adata.obs and 'strand' in adata.obs:
708
+ # Scores are for a gene-based scorer.
709
+ obs = adata.obs.rename({'strand': 'gene_strand'}, axis=1)
710
+
711
+ # Remove patch number from gene_id.
712
+ obs['gene_id'] = obs['gene_id'].str.split('.', expand=True).get(0)
713
+ for col in gene_columns:
714
+ if col not in obs:
715
+ obs[col] = None
716
+ elif adata.X.shape[0] == 1 and adata.obs.empty:
717
+ # Scores are for a non-gene-based scorer.
718
+ obs = pd.DataFrame([[None] * len(gene_columns)], columns=gene_columns)
719
+ else:
720
+ raise ValueError('Scores contain gene metadata but no gene_id/strand.')
721
+
722
+ # Columns to include from the track metadata. Only added if the column
723
+ # appears in the original metadata.
724
+ var = adata.var.rename(
725
+ {'strand': 'track_strand', 'name': 'track_name'}, axis=1
726
+ )
727
+ track_columns = ['track_name', 'track_strand']
728
+ if include_extended_metadata:
729
+ extended_metadata_columns = [
730
+ 'Assay title',
731
+ 'ontology_curie',
732
+ 'biosample_name',
733
+ 'biosample_type',
734
+ 'biosample_life_stage',
735
+ 'gtex_tissue',
736
+ 'data_source',
737
+ 'endedness',
738
+ 'genetically_modified',
739
+ 'transcription_factor',
740
+ 'histone_mark',
741
+ ]
742
+ track_columns.extend([c for c in extended_metadata_columns if c in var])
743
+
744
+ # Construct score dataframe.
745
+ df = pd.merge(var, obs, how='cross')
746
+ df['scored_interval'] = adata.uns['interval']
747
+
748
+ # Add id and scorer information depending on the type of score.
749
+ if 'variant_scorer' in adata.uns:
750
+ df['variant_id'] = adata.uns['variant']
751
+ df['output_type'] = adata.uns['variant_scorer'].requested_output.name
752
+ df['variant_scorer'] = str(adata.uns['variant_scorer'])
753
+ id_columns = ['variant_id', 'scored_interval']
754
+ scorer_columns = ['output_type', 'variant_scorer']
755
+ elif 'interval_scorer' in adata.uns:
756
+ df['output_type'] = adata.uns['interval_scorer'].requested_output.name
757
+ df['interval_scorer'] = str(adata.uns['interval_scorer'])
758
+ id_columns = ['scored_interval']
759
+ scorer_columns = ['output_type', 'interval_scorer']
760
+ else:
761
+ raise ValueError(
762
+ 'Invalid scores, expected either variant_scorer or interval_scorer.'
763
+ )
764
+
765
+ # Formatting final touches.
766
+ df = df.loc[:, id_columns + gene_columns + scorer_columns + track_columns]
767
+ df['raw_score'] = adata.X.T.reshape((-1,))
768
+ if 'quantiles' in adata.layers:
769
+ df['quantile_score'] = adata.layers['quantiles'].T.reshape((-1,))
770
+
771
+ # Remove entries where DNA strands are mismatched. Note that we still retain
772
+ # all tracks that have strand '.' which indicates the data is unstranded.
773
+ if match_gene_strand:
774
+ mismatched_strands = (df['gene_strand'] == '+') & (
775
+ df['track_strand'] == '-'
776
+ ) | (df['gene_strand'] == '-') & (df['track_strand'] == '+')
777
+ df = df[~mismatched_strands].reset_index(drop=True)
778
+ return df.reset_index(drop=True)
779
+
780
+
781
+ def tidy_scores(
782
+ scores: Sequence[anndata.AnnData] | Sequence[Sequence[anndata.AnnData]],
783
+ match_gene_strand: bool = True,
784
+ include_extended_metadata: bool = True,
785
+ ) -> pd.DataFrame | None:
786
+ """Formats scores into a tidy (long) pandas DataFrame.
787
+
788
+ This function reformats variant scores into a more readable DataFrame with one
789
+ score per row. This function supports both scores generated from variant
790
+ scorers (e.g., the output of `score_variant` or `score_variants`) or interval
791
+ scorers (e.g., the output of `score_interval` or `score_intervals`).
792
+
793
+ It handles both variant/interval-centric scoring (producing one score row per
794
+ variant/interval-track pair) and gene-centric scoring (one score row per
795
+ variant/interval-gene-track combination).
796
+
797
+ The function accepts these score input types:
798
+
799
+ - A sequence of AnnData objects (e.g., output of `score_variant` with one or
800
+ more scorers).
801
+ - A nested sequence of AnnData objects (e.g., output of `score_variants`
802
+ with multiple variants).
803
+
804
+ Scores from multiple scorers or multiple variants/intervals are concatenated
805
+ together into a single pandas DataFrame, containing the union of all
806
+ applicable columns across scorers.
807
+
808
+ Args:
809
+ scores: Scoring output as either a sequence of AnnData objects, or a nested
810
+ sequence of AnnData objects (for example, the outputs of `score_variant`
811
+ and `score_variants`, respectively).
812
+ match_gene_strand: If True (and using gene-centric scoring), rows with
813
+ mismatched gene and track strands are removed.
814
+ include_extended_metadata: Argument passed to `tidy_anndata` to include
815
+ additional metadata columns where available.
816
+
817
+ Returns:
818
+ pd.DataFrame with columns:
819
+
820
+ - variant_id (when applicable): Variant of interest (e.g.
821
+ chr22:36201698:A>C).
822
+ - scored_interval: Genomic interval scored (e.g. chr22:36100000-36300000).
823
+ - gene_id: ENSEMBL gene identifier without version number.
824
+ (e.g. ENSG00000100342), or None if not applicable.
825
+ - gene_name: HGNC gene symbol (e.g. APOL1), or None if not applicable.
826
+ - gene_type: Gene biotype (e.g. protein_coding, lncRNA), or None if not
827
+ applicable.
828
+ - gene_strand: Strand of the gene ('+', '-', '.'), or None if not
829
+ applicable.
830
+ - output_type: Type of the output from the model (e.g. RNA_SEQ, DNASE).
831
+ - interval_scorer (when applicable): Name of the interval scorer used.
832
+ - variant_scorer (when applicable): Name of the variant scorer used.
833
+ - track_name: Name of the output track (e.g. UBERON:0036149 total
834
+ RNA-seq).
835
+ - track_strand: Strand of the track ('+', '-', or '.').
836
+ - ontology_curie: Ontology term for the cell type or tissue of the track
837
+ (e.g. UBERON:0036149), or NaN if not applicable.
838
+ - gtex_tissue: Name of the gtex tissue (e.g. Liver), or NaN if not
839
+ applicable.
840
+ - Assay title: Subtype of the assay (e.g., total RNA-seq), or NaN if not
841
+ applicable.
842
+ - biosample_name: Name of the biosample (e.g. liver), or NaN if not
843
+ applicable.
844
+ - biosample_type: Type of biosample (e.g. 'tissue' or 'primary cell'), or
845
+ NaN if not applicable.
846
+ - transcription_factor: Name of the transcription factor (e.g. 'CTCF'), or
847
+ NaN if not applicable.
848
+ - histone_mark: Name of the histological mark (e.g. 'H3K4ME3'), or NaN if
849
+ not applicable.
850
+ - raw_score: Raw variant score.
851
+ - quantile_score (when applicable): Quantile score.
852
+
853
+ Raises:
854
+ ValueError: If the input is not a valid type (sequence of AnnData or nested
855
+ sequence of AnnDatas).
856
+ """
857
+ if isinstance(scores, Sequence):
858
+ tidied_anndata = [
859
+ tidy_anndata(adata, match_gene_strand, include_extended_metadata)
860
+ for adata in itertools.chain.from_iterable(scores)
861
+ ]
862
+ if not tidied_anndata:
863
+ return None
864
+
865
+ concatenated_df = pd.concat(tidied_anndata, axis=0, ignore_index=True)
866
+ # Put the raw_score (and quantile_score when applicable) columns last.
867
+ last_columns = ['raw_score']
868
+ if 'quantile_score' in concatenated_df.columns:
869
+ last_columns.append('quantile_score')
870
+ return concatenated_df[
871
+ [col for col in concatenated_df.columns if col not in last_columns]
872
+ + last_columns
873
+ ]
874
+ else:
875
+ raise ValueError(
876
+ 'Invalid input type. Must be sequence of AnnDatas or nested sequence of'
877
+ ' AnnDatas.'
878
+ )
flax_model/alphagenome/_sdk/protos/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Protocol buffers for interacting with genomic models."""
flax_model/alphagenome/_sdk/protos/dna_model.proto ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2024 Google LLC.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ syntax = "proto3";
16
+
17
+ package google.gdm.gdmscience.alphagenome.v1main;
18
+
19
+ import "alphagenome/protos/tensor.proto";
20
+
21
+ option go_package = "google.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome";
22
+ option java_multiple_files = true;
23
+ option java_outer_classname = "DnaModelProto";
24
+ option java_package = "com.google.gdm.gdmscience.alphagenome.v1main";
25
+
26
+ // Message that represents a genomic interval.
27
+ message Interval {
28
+ // The chromosome name, e.g., "chr1".
29
+ string chromosome = 1;
30
+
31
+ // 0-based start position.
32
+ int64 start = 2;
33
+
34
+ // 0-based end position. Must be greater than or equal to start.
35
+ int64 end = 3;
36
+
37
+ // The strand of the interval.
38
+ Strand strand = 4;
39
+ }
40
+
41
+ // Message that represents a genomic variant/mutation.
42
+ message Variant {
43
+ // The chromosome name, e.g., "chr1".
44
+ string chromosome = 1;
45
+
46
+ // The 1-based position of the variant on the chromosome.
47
+ int64 position = 2;
48
+
49
+ // The reference base(s) at the variant position. Normally this corresponds to
50
+ // the sequence in the reference genome. Must only contain characters "ACGTN".
51
+ string reference_bases = 3;
52
+
53
+ // The alternate base(s) that replace the reference. For example:
54
+ // If sequence="ACT", position=2, reference_bases="C", alternate_bases="TG",
55
+ // then the alternate sequence would be "ATGT'.
56
+ string alternate_bases = 4;
57
+ }
58
+
59
+ // A single biological ontology term.
60
+ message OntologyTerm {
61
+ // The type of ontology this term belongs to.
62
+ OntologyType ontology_type = 1;
63
+
64
+ // The ID of the term within the ontology.
65
+ int64 id = 2;
66
+ }
67
+
68
+ // Message that represents a biological sample that is used in an experiment.
69
+ message Biosample {
70
+ // Biosample type matching ENCODE conventions.
71
+ BiosampleType type = 1;
72
+
73
+ // The name of the biosample, e.g. "T-cell".
74
+ string name = 2;
75
+
76
+ // Optional stage of the biosample, e.g. "adult".
77
+ optional string stage = 3;
78
+ }
79
+
80
+ // Message containing metadata for a gene score.
81
+ message GeneScorerMetadata {
82
+ // ENSEMBL gene identifier without version number, e.g. "ENSG00000100342".
83
+ string gene_id = 1;
84
+
85
+ // Optional HGNC gene symbol, e.g. "APOL1".
86
+ optional string name = 3;
87
+
88
+ // Optional strand of the gene.
89
+ optional Strand strand = 2;
90
+
91
+ // Optional gene biotype, e.g. "protein_coding" or "lncRNA".
92
+ optional string type = 4;
93
+
94
+ // Optional start position for junction scores.
95
+ optional int64 junction_start = 5;
96
+
97
+ // Optional end position for junction scores.
98
+ optional int64 junction_end = 6;
99
+ }
100
+
101
+ // Message containing metadata for a track.
102
+ message TrackMetadata {
103
+ // Name of the track.
104
+ string name = 1;
105
+
106
+ // Strand for the track.
107
+ Strand strand = 2;
108
+
109
+ // Ontology term for the track.
110
+ OntologyTerm ontology_term = 3;
111
+
112
+ // Biosample for the track.
113
+ Biosample biosample = 4;
114
+
115
+ // Optional assay for the track. e.g. "polyA plus RNA-seq".
116
+ optional string assay = 5;
117
+
118
+ // Optional histone mark for the track.
119
+ optional string histone_mark_code = 9;
120
+
121
+ // Optional transcription factor for the track.
122
+ optional string transcription_factor_code = 10;
123
+
124
+ // Optional GTEx tissue for the track.
125
+ optional string gtex_tissue = 8;
126
+
127
+ // Optional data source for the track. e.g. "encode".
128
+ optional string data_source = 11;
129
+
130
+ // Optional sequence reading endedness.
131
+ optional Endedness endedness = 12;
132
+
133
+ // Optional boolean indicating whether the track is genetically modified.
134
+ optional bool genetically_modified = 13;
135
+
136
+ // Optional mean of the non-zero values in the track.
137
+ optional float nonzero_mean = 14;
138
+ }
139
+
140
+ // Container for TrackMetadata.
141
+ message TracksMetadata {
142
+ // Ordered list of metadata for each track.
143
+ repeated TrackMetadata metadata = 1;
144
+ }
145
+
146
+ // Message containing metadata for a splice junctions.
147
+ message JunctionMetadata {
148
+ // Name of the junction track, e.g. "Brain_Cerebellum".
149
+ string name = 1;
150
+
151
+ // Ontology term for the junction track.
152
+ OntologyTerm ontology_term = 2;
153
+
154
+ // Biosample for the junction track.
155
+ Biosample biosample = 3;
156
+
157
+ // Optional GTEx tissue for the junction track.
158
+ optional string gtex_tissue = 4;
159
+
160
+ // Optional data source for the track. e.g. "encode".
161
+ optional string data_source = 5;
162
+
163
+ // Optional assay for the track. e.g. "polyA plus RNA-seq".
164
+ optional string assay = 6;
165
+ }
166
+
167
+ // Container for list of JunctionMetadata.
168
+ message JunctionsMetadata {
169
+ // Ordered list of metadata for each junction.
170
+ repeated JunctionMetadata metadata = 1;
171
+ }
172
+
173
+ // Message for storing track values and metadata.
174
+ message TrackData {
175
+ // Values for the track.
176
+ Tensor values = 1;
177
+
178
+ // Metadata for the track. The number of metadata elements must match the last
179
+ // dimension of the values tensor.
180
+ repeated TrackMetadata metadata = 2;
181
+
182
+ // Resolution of the track data in base pairs.
183
+ optional int64 resolution = 3;
184
+
185
+ // Optional Interval representing the genomic region.
186
+ Interval interval = 4;
187
+ }
188
+
189
+ // Message for storing splice junction values and metadata.
190
+ message JunctionData {
191
+ // Values for the splice junction for each track.
192
+ Tensor values = 1;
193
+
194
+ // Metadata for the splice junction. The number of elements must match the
195
+ // last dimension of the values tensor.
196
+ repeated JunctionMetadata metadata = 2;
197
+
198
+ // Predicted splice junctions. The number of elements must match the first
199
+ // dimensions of the values tensor.
200
+ repeated Interval junctions = 3;
201
+
202
+ // Optional Interval representing the genomic region.
203
+ Interval interval = 4;
204
+ }
205
+
206
+ // Message containing metadata for an interval prediction.
207
+ message IntervalMetadata {
208
+ // Genomic interval.
209
+ Interval interval = 1;
210
+
211
+ // Track metadata for the interval prediction.
212
+ repeated TrackMetadata track_metadata = 2;
213
+
214
+ // Gene metadata for the interval prediction.
215
+ repeated GeneScorerMetadata gene_metadata = 3;
216
+ }
217
+
218
+ // Message for storing interval values and metadata.
219
+ message IntervalData {
220
+ // Values for the interval.
221
+ Tensor values = 1;
222
+
223
+ // Metadata for the interval.
224
+ IntervalMetadata metadata = 2;
225
+ }
226
+
227
+ // Message containing metadata for a variant prediction.
228
+ message VariantMetadata {
229
+ // Genomic variant.
230
+ Variant variant = 1;
231
+
232
+ // Track metadata for the variant prediction.
233
+ repeated TrackMetadata track_metadata = 2;
234
+
235
+ // Gene metadata for the variant prediction.
236
+ repeated GeneScorerMetadata gene_metadata = 3;
237
+ }
238
+
239
+ // Message for storing variant values and metadata.
240
+ message VariantData {
241
+ // Values for the variant.
242
+ Tensor values = 1;
243
+
244
+ // Metadata for the variant.
245
+ VariantMetadata metadata = 2;
246
+ }
247
+
248
+ // Message for storing model outputs for a single output type.
249
+ message Output {
250
+ // The type of output.
251
+ OutputType output_type = 1;
252
+
253
+ // The payload for the output.
254
+ oneof payload {
255
+ // Track data for the output. Used in all output types except
256
+ // OUTPUT_TYPE_SPLICE_JUNCTIONS.
257
+ TrackData track_data = 2;
258
+
259
+ // Raw predictions with no metadata.
260
+ Tensor data = 3;
261
+
262
+ // Junction data for the output. This is only set for
263
+ // OUTPUT_TYPE_SPLICE_JUNCTIONS.
264
+ JunctionData junction_data = 4;
265
+ }
266
+ }
267
+
268
+ // Message for storing score interval results.
269
+ message ScoreIntervalOutput {
270
+ // Score interval data.
271
+ IntervalData interval_data = 1;
272
+ }
273
+
274
+ // Message for storing score variant results.
275
+ message ScoreVariantOutput {
276
+ // Score variant data.
277
+ VariantData variant_data = 1;
278
+ }
279
+
280
+ // Interval scorer message for gene-mask scoring.
281
+ message GeneMaskIntervalScorer {
282
+ // Requested output type.
283
+ OutputType requested_output = 1;
284
+
285
+ // Requested width.
286
+ optional int64 width = 2;
287
+
288
+ // Requested aggregation type.
289
+ IntervalAggregationType aggregation_type = 3;
290
+ }
291
+
292
+ // Container message for all interval scorers.
293
+ message IntervalScorer {
294
+ // The interval scorer payload.
295
+ oneof scorer {
296
+ // Gene mask scorer.
297
+ GeneMaskIntervalScorer gene_mask = 1;
298
+ }
299
+ }
300
+
301
+ // Variant scorer message for center mask scoring.
302
+ message CenterMaskScorer {
303
+ // The width of the mask around the variant.
304
+ optional int64 width = 1;
305
+
306
+ // The aggregation type.
307
+ AggregationType aggregation_type = 2;
308
+
309
+ // Requested output type.
310
+ OutputType requested_output = 3;
311
+ }
312
+
313
+ // Variant scorer message for gene-mask scoring based on log fold change.
314
+ message GeneMaskLFCScorer {
315
+ // Requested output type.
316
+ OutputType requested_output = 1;
317
+ }
318
+
319
+ // Variant scorer message for gene-mask scoring based on active allele counts.
320
+ message GeneMaskActiveScorer {
321
+ // Requested output type.
322
+ OutputType requested_output = 1;
323
+ }
324
+
325
+ // Variant scorer message for gene-mask scoring for splicing.
326
+ message GeneMaskSplicingScorer {
327
+ // The width of the mask around the variant.
328
+ optional int64 width = 1;
329
+
330
+ // Requested output type.
331
+ OutputType requested_output = 2;
332
+ }
333
+
334
+ // Variant scorer message for polyadenylation QTLs (paQTLs).
335
+ message PolyadenylationScorer {}
336
+
337
+ // Variant scorer message for splice junction scoring.
338
+ message SpliceJunctionScorer {}
339
+
340
+ // Variant scorer message for contact map scoring.
341
+ message ContactMapScorer {}
342
+
343
+ // Container message for all variant scorers.
344
+ message VariantScorer {
345
+ // The variant scorer payload.
346
+ oneof scorer {
347
+ // Center mask scorer.
348
+ CenterMaskScorer center_mask = 1;
349
+
350
+ // Gene mask scorer based on log fold change.
351
+ GeneMaskLFCScorer gene_mask = 2;
352
+
353
+ // Gene mask scorer for splicing.
354
+ GeneMaskSplicingScorer gene_mask_splicing = 3;
355
+
356
+ // Polyadenylation scorer.
357
+ PolyadenylationScorer pa_qtl = 4;
358
+
359
+ // Splice junction scorer.
360
+ SpliceJunctionScorer splice_junction = 5;
361
+
362
+ // Contact map scorer.
363
+ ContactMapScorer contact_map = 6;
364
+
365
+ // Gene mask scorer based on active allele counts.
366
+ GeneMaskActiveScorer gene_mask_active = 7;
367
+ }
368
+ }
369
+
370
+ // Message containing metadata for an output type.
371
+ message OutputMetadata {
372
+ // The output type.
373
+ OutputType output_type = 1;
374
+
375
+ // The payload for the output metadata.
376
+ oneof payload {
377
+ // Track metadata for the output. Used in all output types except
378
+ // OUTPUT_TYPE_SPLICE_JUNCTIONS.
379
+ TracksMetadata tracks = 2;
380
+
381
+ // Junction metadata for the output. This is only set for
382
+ // OUTPUT_TYPE_SPLICE_JUNCTIONS.
383
+ JunctionsMetadata junctions = 3;
384
+ }
385
+ }
386
+
387
+ // Defines the possible strands for a DNA sequence.
388
+ enum Strand {
389
+ // Unspecified strand.
390
+ STRAND_UNSPECIFIED = 0;
391
+
392
+ // The forward strand (5' to 3')
393
+ STRAND_POSITIVE = 1;
394
+
395
+ // The reverse strand (3' to 5')
396
+ STRAND_NEGATIVE = 2;
397
+
398
+ // The strand is not specified.
399
+ STRAND_UNSTRANDED = 3;
400
+ }
401
+
402
+ // Supported ontology types.
403
+ enum OntologyType {
404
+ // Unspecified ontology type.
405
+ ONTOLOGY_TYPE_UNSPECIFIED = 0;
406
+
407
+ // Cell Line Ontology.
408
+ ONTOLOGY_TYPE_CLO = 1;
409
+
410
+ // Uber-anatomy ontology.
411
+ ONTOLOGY_TYPE_UBERON = 2;
412
+
413
+ // Cell Ontology.
414
+ ONTOLOGY_TYPE_CL = 3;
415
+
416
+ // Experimental Factor Ontology.
417
+ ONTOLOGY_TYPE_EFO = 4;
418
+
419
+ // New Term Requested.
420
+ ONTOLOGY_TYPE_NTR = 5;
421
+ }
422
+
423
+ // Biosample type matching ENCODE conventions. See
424
+ // https://www.encodeproject.org/profiles/biosample_type.
425
+ enum BiosampleType {
426
+ // Unspecified biosample type.
427
+ BIOSAMPLE_TYPE_UNSPECIFIED = 0;
428
+
429
+ // Primary cell sample.
430
+ BIOSAMPLE_TYPE_PRIMARY_CELL = 1;
431
+
432
+ // In-vitro differentiated cell sample.
433
+ BIOSAMPLE_TYPE_IN_VITRO_DIFFERENTIATED_CELLS = 2;
434
+
435
+ // Cell line sample.
436
+ BIOSAMPLE_TYPE_CELL_LINE = 3;
437
+
438
+ // Tissue sample.
439
+ BIOSAMPLE_TYPE_TISSUE = 4;
440
+
441
+ // Technical sample.
442
+ BIOSAMPLE_TYPE_TECHNICAL_SAMPLE = 5;
443
+
444
+ // Organoid sample.
445
+ BIOSAMPLE_TYPE_ORGANOID = 6;
446
+ }
447
+
448
+ // Enumeration of all the available types of outputs.
449
+ enum OutputType {
450
+ // Unspecified output type.
451
+ OUTPUT_TYPE_UNSPECIFIED = 0;
452
+
453
+ // ATAC-seq tracks capturing chromatin accessibility.
454
+ OUTPUT_TYPE_ATAC = 1;
455
+
456
+ // CAGE (Cap Analysis of Gene Expression) tracks capturing gene expression.
457
+ OUTPUT_TYPE_CAGE = 2;
458
+
459
+ // DNase I hypersensitive site tracks capturing chromatin accessibility.
460
+ OUTPUT_TYPE_DNASE = 3;
461
+
462
+ // RNA sequencing tracks capturing gene expression.
463
+ OUTPUT_TYPE_RNA_SEQ = 4;
464
+
465
+ // ChIP-seq tracks capturing histone modifications.
466
+ OUTPUT_TYPE_CHIP_HISTONE = 5;
467
+
468
+ // ChIP-seq tracks capturing transcription factor binding.
469
+ OUTPUT_TYPE_CHIP_TF = 6;
470
+
471
+ // Splice site tracks capturing donor and acceptor splice sites.
472
+ OUTPUT_TYPE_SPLICE_SITES = 7;
473
+
474
+ // Splice site usage tracks capturing the fraction of the time that each
475
+ // splice site is used.
476
+ OUTPUT_TYPE_SPLICE_SITE_USAGE = 8;
477
+
478
+ // Splice junction tracks capturing split read RNA-seq counts for each
479
+ // junction.
480
+ OUTPUT_TYPE_SPLICE_JUNCTIONS = 9;
481
+
482
+ // Contact map tracks capturing 3D chromatin contact probabilities.
483
+ OUTPUT_TYPE_CONTACT_MAPS = 11;
484
+
485
+ // Precision Run-On sequencing and capping, used to measure gene expression.
486
+ OUTPUT_TYPE_PROCAP = 12;
487
+ }
488
+
489
+ // Enumeration of all the available organisms. Values relate to the NCBI
490
+ // taxonomy ID.
491
+ enum Organism {
492
+ // Unspecified organism.
493
+ ORGANISM_UNSPECIFIED = 0;
494
+
495
+ // Human (Homo sapiens).
496
+ ORGANISM_HOMO_SAPIENS = 9606;
497
+
498
+ // Mouse (Mus musculus).
499
+ ORGANISM_MUS_MUSCULUS = 10090;
500
+ }
501
+
502
+ // Enum indicating the type of interval scorer aggregation.
503
+ enum IntervalAggregationType {
504
+ // Unspecified aggregation type.
505
+ INTERVAL_AGGREGATION_TYPE_UNSPECIFIED = 0;
506
+
507
+ // Mean across positions.
508
+ INTERVAL_AGGREGATION_TYPE_MEAN = 1;
509
+
510
+ // Sum across positions.
511
+ INTERVAL_AGGREGATION_TYPE_SUM = 2;
512
+ }
513
+
514
+ // Enum indicating the type of variant scorer aggregation.
515
+ enum AggregationType {
516
+ // Unspecified aggregation type.
517
+ AGGREGATION_TYPE_UNSPECIFIED = 0;
518
+
519
+ // Difference of means, i.e., mean(ALT) - mean(REF).
520
+ AGGREGATION_TYPE_DIFF_MEAN = 1;
521
+
522
+ // Difference of sums, i.e., sum(ALT) - sum(REF).
523
+ AGGREGATION_TYPE_DIFF_SUM = 2;
524
+
525
+ // Log scales predictions, then takes the sum and then the difference, i.e.
526
+ // sum(log2(ALT)) - sum(log2(REF)).
527
+ AGGREGATION_TYPE_DIFF_SUM_LOG2 = 3;
528
+
529
+ // Takes the difference of ALT and REF predictions, then computes the L2 norm,
530
+ // i.e., l2_norm(ALT - REF).
531
+ AGGREGATION_TYPE_L2_DIFF = 4;
532
+
533
+ // Log scales the predictions + 1, takes the difference of `ALT` and `REF`
534
+ // predictions, then computes the L2 norm, i.e., l2_norm(log1p(`ALT`) -
535
+ // log1p(`REF`)).
536
+ AGGREGATION_TYPE_L2_DIFF_LOG1P = 8;
537
+
538
+ // Takes the sum of predictions, applies a log transform, then takes the
539
+ // difference between predictions, i.e., log2(sum(ALT)) - log2(sum(REF)).
540
+ AGGREGATION_TYPE_DIFF_LOG2_SUM = 5;
541
+
542
+ // Maximum of means, i.e., max(mean(ALT), mean(REF)).
543
+ AGGREGATION_TYPE_ACTIVE_MEAN = 6;
544
+
545
+ // Maximum of sums, i.e., max(sum(ALT), sum(REF)).
546
+ AGGREGATION_TYPE_ACTIVE_SUM = 7;
547
+ }
548
+
549
+ // Enum indicating the endedness of a track experiment.
550
+ enum Endedness {
551
+ // Unspecified endedness.
552
+ ENDEDNESS_UNSPECIFIED = 0;
553
+
554
+ // Single end.
555
+ ENDEDNESS_SINGLE = 1;
556
+
557
+ // Paired end.
558
+ ENDEDNESS_PAIRED = 2;
559
+ }
flax_model/alphagenome/_sdk/protos/dna_model_pb2.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: alphagenome/protos/dna_model.proto
4
+ # Protobuf Python Version: 4.25.1
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+ from flax_model.alphagenome._sdk.protos import tensor_pb2 as alphagenome_dot_protos_dot_tensor__pb2
16
+
17
+
18
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"alphagenome/protos/dna_model.proto\x12(google.gdm.gdmscience.alphagenome.v1main\x1a\x1f\x61lphagenome/protos/tensor.proto\"|\n\x08Interval\x12\x12\n\nchromosome\x18\x01 \x01(\t\x12\r\n\x05start\x18\x02 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x03 \x01(\x03\x12@\n\x06strand\x18\x04 \x01(\x0e\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Strand\"a\n\x07Variant\x12\x12\n\nchromosome\x18\x01 \x01(\t\x12\x10\n\x08position\x18\x02 \x01(\x03\x12\x17\n\x0freference_bases\x18\x03 \x01(\t\x12\x17\n\x0f\x61lternate_bases\x18\x04 \x01(\t\"i\n\x0cOntologyTerm\x12M\n\rontology_type\x18\x01 \x01(\x0e\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyType\x12\n\n\x02id\x18\x02 \x01(\x03\"~\n\tBiosample\x12\x45\n\x04type\x18\x01 \x01(\x0e\x32\x37.google.gdm.gdmscience.alphagenome.v1main.BiosampleType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\x05stage\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_stage\"\x8b\x02\n\x12GeneScorerMetadata\x12\x0f\n\x07gene_id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x45\n\x06strand\x18\x02 \x01(\x0e\x32\x30.google.gdm.gdmscience.alphagenome.v1main.StrandH\x01\x88\x01\x01\x12\x11\n\x04type\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x1b\n\x0ejunction_start\x18\x05 \x01(\x03H\x03\x88\x01\x01\x12\x19\n\x0cjunction_end\x18\x06 \x01(\x03H\x04\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_strandB\x07\n\x05_typeB\x11\n\x0f_junction_startB\x0f\n\r_junction_end\"\xa7\x05\n\rTrackMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12@\n\x06strand\x18\x02 \x01(\x0e\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Strand\x12M\n\rontology_term\x18\x03 \x01(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyTerm\x12\x46\n\tbiosample\x18\x04 \x01(\x0b\x32\x33.google.gdm.gdmscience.alphagenome.v1main.Biosample\x12\x12\n\x05\x61ssay\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x1e\n\x11histone_mark_code\x18\t \x01(\tH\x01\x88\x01\x01\x12&\n\x19transcription_factor_code\x18\n \x01(\tH\x02\x88\x01\x01\x12\x18\n\x0bgtex_tissue\x18\x08 \x01(\tH\x03\x88\x01\x01\x12\x18\n\x0b\x64\x61ta_source\x18\x0b \x01(\tH\x04\x88\x01\x01\x12K\n\tendedness\x18\x0c \x01(\x0e\x32\x33.google.gdm.gdmscience.alphagenome.v1main.EndednessH\x05\x88\x01\x01\x12!\n\x14genetically_modified\x18\r \x01(\x08H\x06\x88\x01\x01\x12\x19\n\x0cnonzero_mean\x18\x0e \x01(\x02H\x07\x88\x01\x01\x42\x08\n\x06_assayB\x14\n\x12_histone_mark_codeB\x1c\n\x1a_transcription_factor_codeB\x0e\n\x0c_gtex_tissueB\x0e\n\x0c_data_sourceB\x0c\n\n_endednessB\x17\n\x15_genetically_modifiedB\x0f\n\r_nonzero_mean\"[\n\x0eTracksMetadata\x12I\n\x08metadata\x18\x01 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.TrackMetadata\"\xa9\x02\n\x10JunctionMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12M\n\rontology_term\x18\x02 \x01(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyTerm\x12\x46\n\tbiosample\x18\x03 \x01(\x0b\x32\x33.google.gdm.gdmscience.alphagenome.v1main.Biosample\x12\x18\n\x0bgtex_tissue\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x61ta_source\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x12\n\x05\x61ssay\x18\x06 \x01(\tH\x02\x88\x01\x01\x42\x0e\n\x0c_gtex_tissueB\x0e\n\x0c_data_sourceB\x08\n\x06_assay\"a\n\x11JunctionsMetadata\x12L\n\x08metadata\x18\x01 \x03(\x0b\x32:.google.gdm.gdmscience.alphagenome.v1main.JunctionMetadata\"\x86\x02\n\tTrackData\x12@\n\x06values\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Tensor\x12I\n\x08metadata\x18\x02 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.TrackMetadata\x12\x17\n\nresolution\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x44\n\x08interval\x18\x04 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.IntervalB\r\n\x0b_resolution\"\xab\x02\n\x0cJunctionData\x12@\n\x06values\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Tensor\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.google.gdm.gdmscience.alphagenome.v1main.JunctionMetadata\x12\x45\n\tjunctions\x18\x03 \x03(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x44\n\x08interval\x18\x04 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\"\xfe\x01\n\x10IntervalMetadata\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12O\n\x0etrack_metadata\x18\x02 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.TrackMetadata\x12S\n\rgene_metadata\x18\x03 \x03(\x0b\x32<.google.gdm.gdmscience.alphagenome.v1main.GeneScorerMetadata\"\x9e\x01\n\x0cIntervalData\x12@\n\x06values\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Tensor\x12L\n\x08metadata\x18\x02 \x01(\x0b\x32:.google.gdm.gdmscience.alphagenome.v1main.IntervalMetadata\"\xfb\x01\n\x0fVariantMetadata\x12\x42\n\x07variant\x18\x01 \x01(\x0b\x32\x31.google.gdm.gdmscience.alphagenome.v1main.Variant\x12O\n\x0etrack_metadata\x18\x02 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.TrackMetadata\x12S\n\rgene_metadata\x18\x03 \x03(\x0b\x32<.google.gdm.gdmscience.alphagenome.v1main.GeneScorerMetadata\"\x9c\x01\n\x0bVariantData\x12@\n\x06values\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.Tensor\x12K\n\x08metadata\x18\x02 \x01(\x0b\x32\x39.google.gdm.gdmscience.alphagenome.v1main.VariantMetadata\"\xbc\x02\n\x06Output\x12I\n\x0boutput_type\x18\x01 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12I\n\ntrack_data\x18\x02 \x01(\x0b\x32\x33.google.gdm.gdmscience.alphagenome.v1main.TrackDataH\x00\x12@\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.TensorH\x00\x12O\n\rjunction_data\x18\x04 \x01(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.JunctionDataH\x00\x42\t\n\x07payload\"d\n\x13ScoreIntervalOutput\x12M\n\rinterval_data\x18\x01 \x01(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.IntervalData\"a\n\x12ScoreVariantOutput\x12K\n\x0cvariant_data\x18\x01 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.VariantData\"\xe3\x01\n\x16GeneMaskIntervalScorer\x12N\n\x10requested_output\x18\x01 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12\x12\n\x05width\x18\x02 \x01(\x03H\x00\x88\x01\x01\x12[\n\x10\x61ggregation_type\x18\x03 \x01(\x0e\x32\x41.google.gdm.gdmscience.alphagenome.v1main.IntervalAggregationTypeB\x08\n\x06_width\"q\n\x0eIntervalScorer\x12U\n\tgene_mask\x18\x01 \x01(\x0b\x32@.google.gdm.gdmscience.alphagenome.v1main.GeneMaskIntervalScorerH\x00\x42\x08\n\x06scorer\"\xd5\x01\n\x10\x43\x65nterMaskScorer\x12\x12\n\x05width\x18\x01 \x01(\x03H\x00\x88\x01\x01\x12S\n\x10\x61ggregation_type\x18\x02 \x01(\x0e\x32\x39.google.gdm.gdmscience.alphagenome.v1main.AggregationType\x12N\n\x10requested_output\x18\x03 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputTypeB\x08\n\x06_width\"c\n\x11GeneMaskLFCScorer\x12N\n\x10requested_output\x18\x01 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\"f\n\x14GeneMaskActiveScorer\x12N\n\x10requested_output\x18\x01 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\"\x86\x01\n\x16GeneMaskSplicingScorer\x12\x12\n\x05width\x18\x01 \x01(\x03H\x00\x88\x01\x01\x12N\n\x10requested_output\x18\x02 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputTypeB\x08\n\x06_width\"\x17\n\x15PolyadenylationScorer\"\x16\n\x14SpliceJunctionScorer\"\x12\n\x10\x43ontactMapScorer\"\xfb\x04\n\rVariantScorer\x12Q\n\x0b\x63\x65nter_mask\x18\x01 \x01(\x0b\x32:.google.gdm.gdmscience.alphagenome.v1main.CenterMaskScorerH\x00\x12P\n\tgene_mask\x18\x02 \x01(\x0b\x32;.google.gdm.gdmscience.alphagenome.v1main.GeneMaskLFCScorerH\x00\x12^\n\x12gene_mask_splicing\x18\x03 \x01(\x0b\x32@.google.gdm.gdmscience.alphagenome.v1main.GeneMaskSplicingScorerH\x00\x12Q\n\x06pa_qtl\x18\x04 \x01(\x0b\x32?.google.gdm.gdmscience.alphagenome.v1main.PolyadenylationScorerH\x00\x12Y\n\x0fsplice_junction\x18\x05 \x01(\x0b\x32>.google.gdm.gdmscience.alphagenome.v1main.SpliceJunctionScorerH\x00\x12Q\n\x0b\x63ontact_map\x18\x06 \x01(\x0b\x32:.google.gdm.gdmscience.alphagenome.v1main.ContactMapScorerH\x00\x12Z\n\x10gene_mask_active\x18\x07 \x01(\x0b\x32>.google.gdm.gdmscience.alphagenome.v1main.GeneMaskActiveScorerH\x00\x42\x08\n\x06scorer\"\x84\x02\n\x0eOutputMetadata\x12I\n\x0boutput_type\x18\x01 \x01(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12J\n\x06tracks\x18\x02 \x01(\x0b\x32\x38.google.gdm.gdmscience.alphagenome.v1main.TracksMetadataH\x00\x12P\n\tjunctions\x18\x03 \x01(\x0b\x32;.google.gdm.gdmscience.alphagenome.v1main.JunctionsMetadataH\x00\x42\t\n\x07payload*a\n\x06Strand\x12\x16\n\x12STRAND_UNSPECIFIED\x10\x00\x12\x13\n\x0fSTRAND_POSITIVE\x10\x01\x12\x13\n\x0fSTRAND_NEGATIVE\x10\x02\x12\x15\n\x11STRAND_UNSTRANDED\x10\x03*\xa2\x01\n\x0cOntologyType\x12\x1d\n\x19ONTOLOGY_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11ONTOLOGY_TYPE_CLO\x10\x01\x12\x18\n\x14ONTOLOGY_TYPE_UBERON\x10\x02\x12\x14\n\x10ONTOLOGY_TYPE_CL\x10\x03\x12\x15\n\x11ONTOLOGY_TYPE_EFO\x10\x04\x12\x15\n\x11ONTOLOGY_TYPE_NTR\x10\x05*\xfd\x01\n\rBiosampleType\x12\x1e\n\x1a\x42IOSAMPLE_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x42IOSAMPLE_TYPE_PRIMARY_CELL\x10\x01\x12\x30\n,BIOSAMPLE_TYPE_IN_VITRO_DIFFERENTIATED_CELLS\x10\x02\x12\x1c\n\x18\x42IOSAMPLE_TYPE_CELL_LINE\x10\x03\x12\x19\n\x15\x42IOSAMPLE_TYPE_TISSUE\x10\x04\x12#\n\x1f\x42IOSAMPLE_TYPE_TECHNICAL_SAMPLE\x10\x05\x12\x1b\n\x17\x42IOSAMPLE_TYPE_ORGANOID\x10\x06*\xd5\x02\n\nOutputType\x12\x1b\n\x17OUTPUT_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10OUTPUT_TYPE_ATAC\x10\x01\x12\x14\n\x10OUTPUT_TYPE_CAGE\x10\x02\x12\x15\n\x11OUTPUT_TYPE_DNASE\x10\x03\x12\x17\n\x13OUTPUT_TYPE_RNA_SEQ\x10\x04\x12\x1c\n\x18OUTPUT_TYPE_CHIP_HISTONE\x10\x05\x12\x17\n\x13OUTPUT_TYPE_CHIP_TF\x10\x06\x12\x1c\n\x18OUTPUT_TYPE_SPLICE_SITES\x10\x07\x12!\n\x1dOUTPUT_TYPE_SPLICE_SITE_USAGE\x10\x08\x12 \n\x1cOUTPUT_TYPE_SPLICE_JUNCTIONS\x10\t\x12\x1c\n\x18OUTPUT_TYPE_CONTACT_MAPS\x10\x0b\x12\x16\n\x12OUTPUT_TYPE_PROCAP\x10\x0c*\\\n\x08Organism\x12\x18\n\x14ORGANISM_UNSPECIFIED\x10\x00\x12\x1a\n\x15ORGANISM_HOMO_SAPIENS\x10\x86K\x12\x1a\n\x15ORGANISM_MUS_MUSCULUS\x10\xeaN*\x8b\x01\n\x17IntervalAggregationType\x12)\n%INTERVAL_AGGREGATION_TYPE_UNSPECIFIED\x10\x00\x12\"\n\x1eINTERVAL_AGGREGATION_TYPE_MEAN\x10\x01\x12!\n\x1dINTERVAL_AGGREGATION_TYPE_SUM\x10\x02*\xbf\x02\n\x0f\x41ggregationType\x12 \n\x1c\x41GGREGATION_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x41GGREGATION_TYPE_DIFF_MEAN\x10\x01\x12\x1d\n\x19\x41GGREGATION_TYPE_DIFF_SUM\x10\x02\x12\"\n\x1e\x41GGREGATION_TYPE_DIFF_SUM_LOG2\x10\x03\x12\x1c\n\x18\x41GGREGATION_TYPE_L2_DIFF\x10\x04\x12\"\n\x1e\x41GGREGATION_TYPE_L2_DIFF_LOG1P\x10\x08\x12\"\n\x1e\x41GGREGATION_TYPE_DIFF_LOG2_SUM\x10\x05\x12 \n\x1c\x41GGREGATION_TYPE_ACTIVE_MEAN\x10\x06\x12\x1f\n\x1b\x41GGREGATION_TYPE_ACTIVE_SUM\x10\x07*R\n\tEndedness\x12\x19\n\x15\x45NDEDNESS_UNSPECIFIED\x10\x00\x12\x14\n\x10\x45NDEDNESS_SINGLE\x10\x01\x12\x14\n\x10\x45NDEDNESS_PAIRED\x10\x02\x42\x94\x01\n,com.google.gdm.gdmscience.alphagenome.v1mainB\rDnaModelProtoP\x01ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenomeb\x06proto3')
19
+
20
+ _globals = globals()
21
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
22
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flax_model.alphagenome._sdk.protos.dna_model_pb2', _globals)
23
+ if _descriptor._USE_C_DESCRIPTORS == False:
24
+ _globals['DESCRIPTOR']._options = None
25
+ _globals['DESCRIPTOR']._serialized_options = b'\n,com.google.gdm.gdmscience.alphagenome.v1mainB\rDnaModelProtoP\001ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome'
26
+ _globals['_STRAND']._serialized_start=5808
27
+ _globals['_STRAND']._serialized_end=5905
28
+ _globals['_ONTOLOGYTYPE']._serialized_start=5908
29
+ _globals['_ONTOLOGYTYPE']._serialized_end=6070
30
+ _globals['_BIOSAMPLETYPE']._serialized_start=6073
31
+ _globals['_BIOSAMPLETYPE']._serialized_end=6326
32
+ _globals['_OUTPUTTYPE']._serialized_start=6329
33
+ _globals['_OUTPUTTYPE']._serialized_end=6670
34
+ _globals['_ORGANISM']._serialized_start=6672
35
+ _globals['_ORGANISM']._serialized_end=6764
36
+ _globals['_INTERVALAGGREGATIONTYPE']._serialized_start=6767
37
+ _globals['_INTERVALAGGREGATIONTYPE']._serialized_end=6906
38
+ _globals['_AGGREGATIONTYPE']._serialized_start=6909
39
+ _globals['_AGGREGATIONTYPE']._serialized_end=7228
40
+ _globals['_ENDEDNESS']._serialized_start=7230
41
+ _globals['_ENDEDNESS']._serialized_end=7312
42
+ _globals['_INTERVAL']._serialized_start=113
43
+ _globals['_INTERVAL']._serialized_end=237
44
+ _globals['_VARIANT']._serialized_start=239
45
+ _globals['_VARIANT']._serialized_end=336
46
+ _globals['_ONTOLOGYTERM']._serialized_start=338
47
+ _globals['_ONTOLOGYTERM']._serialized_end=443
48
+ _globals['_BIOSAMPLE']._serialized_start=445
49
+ _globals['_BIOSAMPLE']._serialized_end=571
50
+ _globals['_GENESCORERMETADATA']._serialized_start=574
51
+ _globals['_GENESCORERMETADATA']._serialized_end=841
52
+ _globals['_TRACKMETADATA']._serialized_start=844
53
+ _globals['_TRACKMETADATA']._serialized_end=1523
54
+ _globals['_TRACKSMETADATA']._serialized_start=1525
55
+ _globals['_TRACKSMETADATA']._serialized_end=1616
56
+ _globals['_JUNCTIONMETADATA']._serialized_start=1619
57
+ _globals['_JUNCTIONMETADATA']._serialized_end=1916
58
+ _globals['_JUNCTIONSMETADATA']._serialized_start=1918
59
+ _globals['_JUNCTIONSMETADATA']._serialized_end=2015
60
+ _globals['_TRACKDATA']._serialized_start=2018
61
+ _globals['_TRACKDATA']._serialized_end=2280
62
+ _globals['_JUNCTIONDATA']._serialized_start=2283
63
+ _globals['_JUNCTIONDATA']._serialized_end=2582
64
+ _globals['_INTERVALMETADATA']._serialized_start=2585
65
+ _globals['_INTERVALMETADATA']._serialized_end=2839
66
+ _globals['_INTERVALDATA']._serialized_start=2842
67
+ _globals['_INTERVALDATA']._serialized_end=3000
68
+ _globals['_VARIANTMETADATA']._serialized_start=3003
69
+ _globals['_VARIANTMETADATA']._serialized_end=3254
70
+ _globals['_VARIANTDATA']._serialized_start=3257
71
+ _globals['_VARIANTDATA']._serialized_end=3413
72
+ _globals['_OUTPUT']._serialized_start=3416
73
+ _globals['_OUTPUT']._serialized_end=3732
74
+ _globals['_SCOREINTERVALOUTPUT']._serialized_start=3734
75
+ _globals['_SCOREINTERVALOUTPUT']._serialized_end=3834
76
+ _globals['_SCOREVARIANTOUTPUT']._serialized_start=3836
77
+ _globals['_SCOREVARIANTOUTPUT']._serialized_end=3933
78
+ _globals['_GENEMASKINTERVALSCORER']._serialized_start=3936
79
+ _globals['_GENEMASKINTERVALSCORER']._serialized_end=4163
80
+ _globals['_INTERVALSCORER']._serialized_start=4165
81
+ _globals['_INTERVALSCORER']._serialized_end=4278
82
+ _globals['_CENTERMASKSCORER']._serialized_start=4281
83
+ _globals['_CENTERMASKSCORER']._serialized_end=4494
84
+ _globals['_GENEMASKLFCSCORER']._serialized_start=4496
85
+ _globals['_GENEMASKLFCSCORER']._serialized_end=4595
86
+ _globals['_GENEMASKACTIVESCORER']._serialized_start=4597
87
+ _globals['_GENEMASKACTIVESCORER']._serialized_end=4699
88
+ _globals['_GENEMASKSPLICINGSCORER']._serialized_start=4702
89
+ _globals['_GENEMASKSPLICINGSCORER']._serialized_end=4836
90
+ _globals['_POLYADENYLATIONSCORER']._serialized_start=4838
91
+ _globals['_POLYADENYLATIONSCORER']._serialized_end=4861
92
+ _globals['_SPLICEJUNCTIONSCORER']._serialized_start=4863
93
+ _globals['_SPLICEJUNCTIONSCORER']._serialized_end=4885
94
+ _globals['_CONTACTMAPSCORER']._serialized_start=4887
95
+ _globals['_CONTACTMAPSCORER']._serialized_end=4905
96
+ _globals['_VARIANTSCORER']._serialized_start=4908
97
+ _globals['_VARIANTSCORER']._serialized_end=5543
98
+ _globals['_OUTPUTMETADATA']._serialized_start=5546
99
+ _globals['_OUTPUTMETADATA']._serialized_end=5806
100
+ # @@protoc_insertion_point(module_scope)
flax_model/alphagenome/_sdk/protos/dna_model_pb2_grpc.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+
flax_model/alphagenome/_sdk/protos/dna_model_service.proto ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2024 Google LLC.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ syntax = "proto3";
16
+
17
+ package google.gdm.gdmscience.alphagenome.v1main;
18
+
19
+ import "alphagenome/protos/dna_model.proto";
20
+ import "alphagenome/protos/tensor.proto";
21
+
22
+ option go_package = "google.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome";
23
+ option java_multiple_files = true;
24
+ option java_outer_classname = "DnaModelServiceProto";
25
+ option java_package = "com.google.gdm.gdmscience.alphagenome.v1main";
26
+
27
+ // Service for making predictions with DNA models.
28
+ service DnaModelService {
29
+ // Makes a prediction for DNA sequence.
30
+ rpc PredictSequence(stream PredictSequenceRequest)
31
+ returns (stream PredictSequenceResponse) {}
32
+
33
+ // Make prediction for a single genomic interval.
34
+ rpc PredictInterval(stream PredictIntervalRequest)
35
+ returns (stream PredictIntervalResponse) {}
36
+
37
+ // Make variant effect predictions for a genomic interval.
38
+ rpc PredictVariant(stream PredictVariantRequest)
39
+ returns (stream PredictVariantResponse) {}
40
+
41
+ // Score a genomic interval.
42
+ rpc ScoreInterval(stream ScoreIntervalRequest)
43
+ returns (stream ScoreIntervalResponse) {}
44
+
45
+ // Score a variant for a genomic interval.
46
+ rpc ScoreVariant(stream ScoreVariantRequest)
47
+ returns (stream ScoreVariantResponse) {}
48
+
49
+ // Score ISM variant effect predictions for a genomic interval.
50
+ rpc ScoreIsmVariant(stream ScoreIsmVariantRequest)
51
+ returns (stream ScoreIsmVariantResponse) {}
52
+
53
+ // Get metadata for the model.
54
+ rpc GetMetadata(MetadataRequest) returns (stream MetadataResponse) {}
55
+ }
56
+
57
+ // Request message for predicting a sequence.
58
+ message PredictSequenceRequest {
59
+ // DNA sequence to make prediction for. Must only contain characters "ACGTN".
60
+ string sequence = 1;
61
+
62
+ // Organism to use for the prediction.
63
+ Organism organism = 2;
64
+
65
+ // Ontology terms to generate predictions for. If empty returns all
66
+ // ontologies.
67
+ repeated OntologyTerm ontology_terms = 3;
68
+
69
+ // Output types to generate predictions for.
70
+ repeated OutputType requested_outputs = 4;
71
+
72
+ // Model version to use.
73
+ string model_version = 5;
74
+ }
75
+
76
+ // Response message for predicting a sequence.
77
+ message PredictSequenceResponse {
78
+ // The payload for the response.
79
+ oneof payload {
80
+ // Output for a single output type.
81
+ Output output = 1;
82
+
83
+ // Tensor chunk related to the last output message received. It is an error
84
+ // to receive a tensor chunk without a previous output message.
85
+ TensorChunk tensor_chunk = 2;
86
+ }
87
+ }
88
+
89
+ // Request message for predicting an interval.
90
+ message PredictIntervalRequest {
91
+ // DNA interval to make prediction for.
92
+ Interval interval = 1;
93
+
94
+ // Organism to use for the prediction.
95
+ Organism organism = 2;
96
+
97
+ // Output types to generate predictions for.
98
+ repeated OutputType requested_outputs = 3;
99
+
100
+ // Ontology terms to generate predictions for. If empty returns all
101
+ // ontologies.
102
+ repeated OntologyTerm ontology_terms = 4;
103
+
104
+ // Model version to use.
105
+ string model_version = 5;
106
+ }
107
+
108
+ // Response message for predicting an interval.
109
+ message PredictIntervalResponse {
110
+ // The payload for the response.
111
+ oneof payload {
112
+ // Output for a single output type.
113
+ Output output = 1;
114
+
115
+ // Tensor chunk related to the last output message received. It is an error
116
+ // to receive a tensor chunk without a previous output message.
117
+ TensorChunk tensor_chunk = 2;
118
+ }
119
+ }
120
+
121
+ // Request message for predicting a variant.
122
+ message PredictVariantRequest {
123
+ // DNA interval to make prediction for.
124
+ Interval interval = 1;
125
+
126
+ // DNA variant to make prediction for.
127
+ Variant variant = 2;
128
+
129
+ // Organism to use for the prediction.
130
+ Organism organism = 3;
131
+
132
+ // Output types to generate predictions for.
133
+ repeated OutputType requested_outputs = 4;
134
+
135
+ // Ontology terms to generate predictions for. If empty returns all
136
+ // ontologies.
137
+ repeated OntologyTerm ontology_terms = 5;
138
+
139
+ // Model version to use.
140
+ string model_version = 6;
141
+ }
142
+
143
+ // Response message for predicting a variant.
144
+ message PredictVariantResponse {
145
+ // The payload for the response.
146
+ oneof payload {
147
+ // Reference output for a single output type.
148
+ Output reference_output = 1;
149
+
150
+ // Alternate output for a single output type.
151
+ Output alternate_output = 2;
152
+
153
+ // Tensor chunk related to the last reference or alternate output message
154
+ // received. It is an error to receive a tensor chunk without a previous
155
+ // reference or alternate output message.
156
+ TensorChunk tensor_chunk = 3;
157
+ }
158
+ }
159
+
160
+ // Request message for scoring an interval.
161
+ message ScoreIntervalRequest {
162
+ // DNA interval to make prediction for.
163
+ Interval interval = 1;
164
+
165
+ // Organism to use for the prediction.
166
+ Organism organism = 2;
167
+
168
+ // Interval scorers to use for the prediction.
169
+ repeated IntervalScorer interval_scorers = 3;
170
+
171
+ // Model version to use.
172
+ string model_version = 4;
173
+ }
174
+
175
+ // Response message for scoring an interval.
176
+ message ScoreIntervalResponse {
177
+ // The payload for the response.
178
+ oneof payload {
179
+ // Score interval output.
180
+ ScoreIntervalOutput output = 1;
181
+
182
+ // Tensor chunk related to the last score interval output message received.
183
+ // It is an error to receice a tensor chunk without a previous output
184
+ // message.
185
+ TensorChunk tensor_chunk = 2;
186
+ }
187
+ }
188
+
189
+ // Request message for scoring a variant.
190
+ message ScoreVariantRequest {
191
+ // DNA interval to make prediction for.
192
+ Interval interval = 1;
193
+
194
+ // DNA variant to make prediction for.
195
+ Variant variant = 2;
196
+
197
+ // Organism to use for the prediction.
198
+ Organism organism = 3;
199
+
200
+ // Variant scorers to use for the prediction.
201
+ repeated VariantScorer variant_scorers = 4;
202
+
203
+ // Model version to use.
204
+ string model_version = 5;
205
+ }
206
+
207
+ // Response message for scoring a variant.
208
+ message ScoreVariantResponse {
209
+ // The payload for the response.
210
+ oneof payload {
211
+ // Score variant output.
212
+ ScoreVariantOutput output = 1;
213
+
214
+ // Tensor chunk related to the last score variant output message received.
215
+ // It is an error to receive a tensor chunk without a previous output
216
+ // message.
217
+ TensorChunk tensor_chunk = 2;
218
+ }
219
+ }
220
+
221
+ // Request message for scoring an in-silico mutagenesis (ISM) interval.
222
+ message ScoreIsmVariantRequest {
223
+ // DNA interval to make the prediction for.
224
+ Interval interval = 1;
225
+
226
+ // ISM interval to make the prediction for.
227
+ Interval ism_interval = 2;
228
+
229
+ // Organism to use for the prediction.
230
+ Organism organism = 3;
231
+
232
+ // Variant scorers to use for the prediction.
233
+ repeated VariantScorer variant_scorers = 4;
234
+
235
+ // Optional variant applied to the reference interval. If provided, the
236
+ // alternate allele is used for in-silico mutagenesis, otherwise the
237
+ // unaltered reference sequence is used.
238
+ optional Variant interval_variant = 6;
239
+
240
+ // Model version to use.
241
+ string model_version = 5;
242
+ }
243
+
244
+ // Response message for scoring an in-silico mutagenesis (ISM) interval.
245
+ message ScoreIsmVariantResponse {
246
+ // The payload for the response.
247
+ oneof payload {
248
+ // Score variant output.
249
+ ScoreVariantOutput output = 1;
250
+
251
+ // Tensor chunk related to the last score variant output message received.
252
+ // It is an error to receive a tensor chunk without a previous output
253
+ // message.
254
+ TensorChunk tensor_chunk = 2;
255
+ }
256
+ }
257
+
258
+ // Request message for getting metadata for an organism.
259
+ message MetadataRequest {
260
+ // The organism we should return metadata for.
261
+ Organism organism = 1;
262
+ }
263
+
264
+ // Response message for getting metadata for an organism.
265
+ message MetadataResponse {
266
+ // The metadata for each output type.
267
+ repeated OutputMetadata output_metadata = 1;
268
+ }
flax_model/alphagenome/_sdk/protos/dna_model_service_pb2.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: alphagenome/protos/dna_model_service.proto
4
+ # Protobuf Python Version: 4.25.1
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+ from flax_model.alphagenome._sdk.protos import dna_model_pb2 as alphagenome_dot_protos_dot_dna__model__pb2
16
+ from flax_model.alphagenome._sdk.protos import tensor_pb2 as alphagenome_dot_protos_dot_tensor__pb2
17
+
18
+
19
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*alphagenome/protos/dna_model_service.proto\x12(google.gdm.gdmscience.alphagenome.v1main\x1a\"alphagenome/protos/dna_model.proto\x1a\x1f\x61lphagenome/protos/tensor.proto\"\xa8\x02\n\x16PredictSequenceRequest\x12\x10\n\x08sequence\x18\x01 \x01(\t\x12\x44\n\x08organism\x18\x02 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12N\n\x0eontology_terms\x18\x03 \x03(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyTerm\x12O\n\x11requested_outputs\x18\x04 \x03(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12\x15\n\rmodel_version\x18\x05 \x01(\t\"\xb7\x01\n\x17PredictSequenceResponse\x12\x42\n\x06output\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.OutputH\x00\x12M\n\x0ctensor_chunk\x18\x02 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"\xdc\x02\n\x16PredictIntervalRequest\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x44\n\x08organism\x18\x02 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12O\n\x11requested_outputs\x18\x03 \x03(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12N\n\x0eontology_terms\x18\x04 \x03(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyTerm\x12\x15\n\rmodel_version\x18\x05 \x01(\t\"\xb7\x01\n\x17PredictIntervalResponse\x12\x42\n\x06output\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.OutputH\x00\x12M\n\x0ctensor_chunk\x18\x02 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"\x9f\x03\n\x15PredictVariantRequest\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x42\n\x07variant\x18\x02 \x01(\x0b\x32\x31.google.gdm.gdmscience.alphagenome.v1main.Variant\x12\x44\n\x08organism\x18\x03 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12O\n\x11requested_outputs\x18\x04 \x03(\x0e\x32\x34.google.gdm.gdmscience.alphagenome.v1main.OutputType\x12N\n\x0eontology_terms\x18\x05 \x03(\x0b\x32\x36.google.gdm.gdmscience.alphagenome.v1main.OntologyTerm\x12\x15\n\rmodel_version\x18\x06 \x01(\t\"\x8e\x02\n\x16PredictVariantResponse\x12L\n\x10reference_output\x18\x01 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.OutputH\x00\x12L\n\x10\x61lternate_output\x18\x02 \x01(\x0b\x32\x30.google.gdm.gdmscience.alphagenome.v1main.OutputH\x00\x12M\n\x0ctensor_chunk\x18\x03 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"\x8d\x02\n\x14ScoreIntervalRequest\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x44\n\x08organism\x18\x02 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12R\n\x10interval_scorers\x18\x03 \x03(\x0b\x32\x38.google.gdm.gdmscience.alphagenome.v1main.IntervalScorer\x12\x15\n\rmodel_version\x18\x04 \x01(\t\"\xc2\x01\n\x15ScoreIntervalResponse\x12O\n\x06output\x18\x01 \x01(\x0b\x32=.google.gdm.gdmscience.alphagenome.v1main.ScoreIntervalOutputH\x00\x12M\n\x0ctensor_chunk\x18\x02 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"\xce\x02\n\x13ScoreVariantRequest\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x42\n\x07variant\x18\x02 \x01(\x0b\x32\x31.google.gdm.gdmscience.alphagenome.v1main.Variant\x12\x44\n\x08organism\x18\x03 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12P\n\x0fvariant_scorers\x18\x04 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.VariantScorer\x12\x15\n\rmodel_version\x18\x05 \x01(\t\"\xc0\x01\n\x14ScoreVariantResponse\x12N\n\x06output\x18\x01 \x01(\x0b\x32<.google.gdm.gdmscience.alphagenome.v1main.ScoreVariantOutputH\x00\x12M\n\x0ctensor_chunk\x18\x02 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"\xbe\x03\n\x16ScoreIsmVariantRequest\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12H\n\x0cism_interval\x18\x02 \x01(\x0b\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Interval\x12\x44\n\x08organism\x18\x03 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\x12P\n\x0fvariant_scorers\x18\x04 \x03(\x0b\x32\x37.google.gdm.gdmscience.alphagenome.v1main.VariantScorer\x12P\n\x10interval_variant\x18\x06 \x01(\x0b\x32\x31.google.gdm.gdmscience.alphagenome.v1main.VariantH\x00\x88\x01\x01\x12\x15\n\rmodel_version\x18\x05 \x01(\tB\x13\n\x11_interval_variant\"\xc3\x01\n\x17ScoreIsmVariantResponse\x12N\n\x06output\x18\x01 \x01(\x0b\x32<.google.gdm.gdmscience.alphagenome.v1main.ScoreVariantOutputH\x00\x12M\n\x0ctensor_chunk\x18\x02 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x42\t\n\x07payload\"W\n\x0fMetadataRequest\x12\x44\n\x08organism\x18\x01 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.Organism\"e\n\x10MetadataResponse\x12Q\n\x0foutput_metadata\x18\x01 \x03(\x0b\x32\x38.google.gdm.gdmscience.alphagenome.v1main.OutputMetadata2\xc4\x08\n\x0f\x44naModelService\x12\x9c\x01\n\x0fPredictSequence\x12@.google.gdm.gdmscience.alphagenome.v1main.PredictSequenceRequest\x1a\x41.google.gdm.gdmscience.alphagenome.v1main.PredictSequenceResponse\"\x00(\x01\x30\x01\x12\x9c\x01\n\x0fPredictInterval\x12@.google.gdm.gdmscience.alphagenome.v1main.PredictIntervalRequest\x1a\x41.google.gdm.gdmscience.alphagenome.v1main.PredictIntervalResponse\"\x00(\x01\x30\x01\x12\x99\x01\n\x0ePredictVariant\x12?.google.gdm.gdmscience.alphagenome.v1main.PredictVariantRequest\x1a@.google.gdm.gdmscience.alphagenome.v1main.PredictVariantResponse\"\x00(\x01\x30\x01\x12\x96\x01\n\rScoreInterval\x12>.google.gdm.gdmscience.alphagenome.v1main.ScoreIntervalRequest\x1a?.google.gdm.gdmscience.alphagenome.v1main.ScoreIntervalResponse\"\x00(\x01\x30\x01\x12\x93\x01\n\x0cScoreVariant\x12=.google.gdm.gdmscience.alphagenome.v1main.ScoreVariantRequest\x1a>.google.gdm.gdmscience.alphagenome.v1main.ScoreVariantResponse\"\x00(\x01\x30\x01\x12\x9c\x01\n\x0fScoreIsmVariant\x12@.google.gdm.gdmscience.alphagenome.v1main.ScoreIsmVariantRequest\x1a\x41.google.gdm.gdmscience.alphagenome.v1main.ScoreIsmVariantResponse\"\x00(\x01\x30\x01\x12\x88\x01\n\x0bGetMetadata\x12\x39.google.gdm.gdmscience.alphagenome.v1main.MetadataRequest\x1a:.google.gdm.gdmscience.alphagenome.v1main.MetadataResponse\"\x00\x30\x01\x42\x9b\x01\n,com.google.gdm.gdmscience.alphagenome.v1mainB\x14\x44naModelServiceProtoP\x01ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenomeb\x06proto3')
20
+
21
+ _globals = globals()
22
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
23
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flax_model.alphagenome._sdk.protos.dna_model_service_pb2', _globals)
24
+ if _descriptor._USE_C_DESCRIPTORS == False:
25
+ _globals['DESCRIPTOR']._options = None
26
+ _globals['DESCRIPTOR']._serialized_options = b'\n,com.google.gdm.gdmscience.alphagenome.v1mainB\024DnaModelServiceProtoP\001ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome'
27
+ _globals['_PREDICTSEQUENCEREQUEST']._serialized_start=158
28
+ _globals['_PREDICTSEQUENCEREQUEST']._serialized_end=454
29
+ _globals['_PREDICTSEQUENCERESPONSE']._serialized_start=457
30
+ _globals['_PREDICTSEQUENCERESPONSE']._serialized_end=640
31
+ _globals['_PREDICTINTERVALREQUEST']._serialized_start=643
32
+ _globals['_PREDICTINTERVALREQUEST']._serialized_end=991
33
+ _globals['_PREDICTINTERVALRESPONSE']._serialized_start=994
34
+ _globals['_PREDICTINTERVALRESPONSE']._serialized_end=1177
35
+ _globals['_PREDICTVARIANTREQUEST']._serialized_start=1180
36
+ _globals['_PREDICTVARIANTREQUEST']._serialized_end=1595
37
+ _globals['_PREDICTVARIANTRESPONSE']._serialized_start=1598
38
+ _globals['_PREDICTVARIANTRESPONSE']._serialized_end=1868
39
+ _globals['_SCOREINTERVALREQUEST']._serialized_start=1871
40
+ _globals['_SCOREINTERVALREQUEST']._serialized_end=2140
41
+ _globals['_SCOREINTERVALRESPONSE']._serialized_start=2143
42
+ _globals['_SCOREINTERVALRESPONSE']._serialized_end=2337
43
+ _globals['_SCOREVARIANTREQUEST']._serialized_start=2340
44
+ _globals['_SCOREVARIANTREQUEST']._serialized_end=2674
45
+ _globals['_SCOREVARIANTRESPONSE']._serialized_start=2677
46
+ _globals['_SCOREVARIANTRESPONSE']._serialized_end=2869
47
+ _globals['_SCOREISMVARIANTREQUEST']._serialized_start=2872
48
+ _globals['_SCOREISMVARIANTREQUEST']._serialized_end=3318
49
+ _globals['_SCOREISMVARIANTRESPONSE']._serialized_start=3321
50
+ _globals['_SCOREISMVARIANTRESPONSE']._serialized_end=3516
51
+ _globals['_METADATAREQUEST']._serialized_start=3518
52
+ _globals['_METADATAREQUEST']._serialized_end=3605
53
+ _globals['_METADATARESPONSE']._serialized_start=3607
54
+ _globals['_METADATARESPONSE']._serialized_end=3708
55
+ _globals['_DNAMODELSERVICE']._serialized_start=3711
56
+ _globals['_DNAMODELSERVICE']._serialized_end=4803
57
+ # @@protoc_insertion_point(module_scope)
flax_model/alphagenome/_sdk/protos/dna_model_service_pb2_grpc.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+
5
+ from flax_model.alphagenome._sdk.protos import dna_model_service_pb2 as alphagenome_dot_protos_dot_dna__model__service__pb2
6
+
7
+
8
+ class DnaModelServiceStub(object):
9
+ """Service for making predictions with DNA models.
10
+ """
11
+
12
+ def __init__(self, channel):
13
+ """Constructor.
14
+
15
+ Args:
16
+ channel: A grpc.Channel.
17
+ """
18
+ self.PredictSequence = channel.stream_stream(
19
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictSequence',
20
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceRequest.SerializeToString,
21
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceResponse.FromString,
22
+ )
23
+ self.PredictInterval = channel.stream_stream(
24
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictInterval',
25
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalRequest.SerializeToString,
26
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalResponse.FromString,
27
+ )
28
+ self.PredictVariant = channel.stream_stream(
29
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictVariant',
30
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantRequest.SerializeToString,
31
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantResponse.FromString,
32
+ )
33
+ self.ScoreInterval = channel.stream_stream(
34
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreInterval',
35
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalRequest.SerializeToString,
36
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalResponse.FromString,
37
+ )
38
+ self.ScoreVariant = channel.stream_stream(
39
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreVariant',
40
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantRequest.SerializeToString,
41
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantResponse.FromString,
42
+ )
43
+ self.ScoreIsmVariant = channel.stream_stream(
44
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreIsmVariant',
45
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantRequest.SerializeToString,
46
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantResponse.FromString,
47
+ )
48
+ self.GetMetadata = channel.unary_stream(
49
+ '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/GetMetadata',
50
+ request_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataRequest.SerializeToString,
51
+ response_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataResponse.FromString,
52
+ )
53
+
54
+
55
+ class DnaModelServiceServicer(object):
56
+ """Service for making predictions with DNA models.
57
+ """
58
+
59
+ def PredictSequence(self, request_iterator, context):
60
+ """Makes a prediction for DNA sequence.
61
+ """
62
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
63
+ context.set_details('Method not implemented!')
64
+ raise NotImplementedError('Method not implemented!')
65
+
66
+ def PredictInterval(self, request_iterator, context):
67
+ """Make prediction for a single genomic interval.
68
+ """
69
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
70
+ context.set_details('Method not implemented!')
71
+ raise NotImplementedError('Method not implemented!')
72
+
73
+ def PredictVariant(self, request_iterator, context):
74
+ """Make variant effect predictions for a genomic interval.
75
+ """
76
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
77
+ context.set_details('Method not implemented!')
78
+ raise NotImplementedError('Method not implemented!')
79
+
80
+ def ScoreInterval(self, request_iterator, context):
81
+ """Score a genomic interval.
82
+ """
83
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
84
+ context.set_details('Method not implemented!')
85
+ raise NotImplementedError('Method not implemented!')
86
+
87
+ def ScoreVariant(self, request_iterator, context):
88
+ """Score a variant for a genomic interval.
89
+ """
90
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
91
+ context.set_details('Method not implemented!')
92
+ raise NotImplementedError('Method not implemented!')
93
+
94
+ def ScoreIsmVariant(self, request_iterator, context):
95
+ """Score ISM variant effect predictions for a genomic interval.
96
+ """
97
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
98
+ context.set_details('Method not implemented!')
99
+ raise NotImplementedError('Method not implemented!')
100
+
101
+ def GetMetadata(self, request, context):
102
+ """Get metadata for the model.
103
+ """
104
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
105
+ context.set_details('Method not implemented!')
106
+ raise NotImplementedError('Method not implemented!')
107
+
108
+
109
+ def add_DnaModelServiceServicer_to_server(servicer, server):
110
+ rpc_method_handlers = {
111
+ 'PredictSequence': grpc.stream_stream_rpc_method_handler(
112
+ servicer.PredictSequence,
113
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceRequest.FromString,
114
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceResponse.SerializeToString,
115
+ ),
116
+ 'PredictInterval': grpc.stream_stream_rpc_method_handler(
117
+ servicer.PredictInterval,
118
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalRequest.FromString,
119
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalResponse.SerializeToString,
120
+ ),
121
+ 'PredictVariant': grpc.stream_stream_rpc_method_handler(
122
+ servicer.PredictVariant,
123
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantRequest.FromString,
124
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantResponse.SerializeToString,
125
+ ),
126
+ 'ScoreInterval': grpc.stream_stream_rpc_method_handler(
127
+ servicer.ScoreInterval,
128
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalRequest.FromString,
129
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalResponse.SerializeToString,
130
+ ),
131
+ 'ScoreVariant': grpc.stream_stream_rpc_method_handler(
132
+ servicer.ScoreVariant,
133
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantRequest.FromString,
134
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantResponse.SerializeToString,
135
+ ),
136
+ 'ScoreIsmVariant': grpc.stream_stream_rpc_method_handler(
137
+ servicer.ScoreIsmVariant,
138
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantRequest.FromString,
139
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantResponse.SerializeToString,
140
+ ),
141
+ 'GetMetadata': grpc.unary_stream_rpc_method_handler(
142
+ servicer.GetMetadata,
143
+ request_deserializer=alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataRequest.FromString,
144
+ response_serializer=alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataResponse.SerializeToString,
145
+ ),
146
+ }
147
+ generic_handler = grpc.method_handlers_generic_handler(
148
+ 'google.gdm.gdmscience.alphagenome.v1main.DnaModelService', rpc_method_handlers)
149
+ server.add_generic_rpc_handlers((generic_handler,))
150
+
151
+
152
+ # This class is part of an EXPERIMENTAL API.
153
+ class DnaModelService(object):
154
+ """Service for making predictions with DNA models.
155
+ """
156
+
157
+ @staticmethod
158
+ def PredictSequence(request_iterator,
159
+ target,
160
+ options=(),
161
+ channel_credentials=None,
162
+ call_credentials=None,
163
+ insecure=False,
164
+ compression=None,
165
+ wait_for_ready=None,
166
+ timeout=None,
167
+ metadata=None):
168
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictSequence',
169
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceRequest.SerializeToString,
170
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictSequenceResponse.FromString,
171
+ options, channel_credentials,
172
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
173
+
174
+ @staticmethod
175
+ def PredictInterval(request_iterator,
176
+ target,
177
+ options=(),
178
+ channel_credentials=None,
179
+ call_credentials=None,
180
+ insecure=False,
181
+ compression=None,
182
+ wait_for_ready=None,
183
+ timeout=None,
184
+ metadata=None):
185
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictInterval',
186
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalRequest.SerializeToString,
187
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictIntervalResponse.FromString,
188
+ options, channel_credentials,
189
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
190
+
191
+ @staticmethod
192
+ def PredictVariant(request_iterator,
193
+ target,
194
+ options=(),
195
+ channel_credentials=None,
196
+ call_credentials=None,
197
+ insecure=False,
198
+ compression=None,
199
+ wait_for_ready=None,
200
+ timeout=None,
201
+ metadata=None):
202
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/PredictVariant',
203
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantRequest.SerializeToString,
204
+ alphagenome_dot_protos_dot_dna__model__service__pb2.PredictVariantResponse.FromString,
205
+ options, channel_credentials,
206
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
207
+
208
+ @staticmethod
209
+ def ScoreInterval(request_iterator,
210
+ target,
211
+ options=(),
212
+ channel_credentials=None,
213
+ call_credentials=None,
214
+ insecure=False,
215
+ compression=None,
216
+ wait_for_ready=None,
217
+ timeout=None,
218
+ metadata=None):
219
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreInterval',
220
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalRequest.SerializeToString,
221
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIntervalResponse.FromString,
222
+ options, channel_credentials,
223
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
224
+
225
+ @staticmethod
226
+ def ScoreVariant(request_iterator,
227
+ target,
228
+ options=(),
229
+ channel_credentials=None,
230
+ call_credentials=None,
231
+ insecure=False,
232
+ compression=None,
233
+ wait_for_ready=None,
234
+ timeout=None,
235
+ metadata=None):
236
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreVariant',
237
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantRequest.SerializeToString,
238
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreVariantResponse.FromString,
239
+ options, channel_credentials,
240
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
241
+
242
+ @staticmethod
243
+ def ScoreIsmVariant(request_iterator,
244
+ target,
245
+ options=(),
246
+ channel_credentials=None,
247
+ call_credentials=None,
248
+ insecure=False,
249
+ compression=None,
250
+ wait_for_ready=None,
251
+ timeout=None,
252
+ metadata=None):
253
+ return grpc.experimental.stream_stream(request_iterator, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/ScoreIsmVariant',
254
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantRequest.SerializeToString,
255
+ alphagenome_dot_protos_dot_dna__model__service__pb2.ScoreIsmVariantResponse.FromString,
256
+ options, channel_credentials,
257
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
258
+
259
+ @staticmethod
260
+ def GetMetadata(request,
261
+ target,
262
+ options=(),
263
+ channel_credentials=None,
264
+ call_credentials=None,
265
+ insecure=False,
266
+ compression=None,
267
+ wait_for_ready=None,
268
+ timeout=None,
269
+ metadata=None):
270
+ return grpc.experimental.unary_stream(request, target, '/google.gdm.gdmscience.alphagenome.v1main.DnaModelService/GetMetadata',
271
+ alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataRequest.SerializeToString,
272
+ alphagenome_dot_protos_dot_dna__model__service__pb2.MetadataResponse.FromString,
273
+ options, channel_credentials,
274
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
flax_model/alphagenome/_sdk/protos/tensor.proto ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2024 Google LLC.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ syntax = "proto3";
16
+
17
+ package google.gdm.gdmscience.alphagenome.v1main;
18
+
19
+ option go_package = "google.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome";
20
+ option java_multiple_files = true;
21
+ option java_outer_classname = "TensorProto";
22
+ option java_package = "com.google.gdm.gdmscience.alphagenome.v1main";
23
+
24
+ // Protocol buffer representing an arbitrarily large multi-dimensional array of
25
+ // data, laid out in row-major format. To support tensors larger than the
26
+ // maximum protocol buffer message size, the tensor payload may be split into
27
+ // multiple chunks.
28
+ message Tensor {
29
+ // The shape of the tensor. If empty, the data will be treated as a scalar.
30
+ repeated int32 shape = 1;
31
+
32
+ // Data type for the elements in this tensor.
33
+ DataType data_type = 2;
34
+
35
+ // The payload for the tensor.
36
+ oneof payload {
37
+ // The raw data for the tensor. If present, the tensor is not split into
38
+ // chunks.
39
+ TensorChunk array = 3;
40
+
41
+ // The number of chunks the tensor is split into.
42
+ int64 chunk_count = 4;
43
+ }
44
+ }
45
+
46
+ // A single chunk of a tensor.
47
+ message TensorChunk {
48
+ // Flattened tensor data. Data is laid out in row-major order.
49
+ bytes data = 1;
50
+
51
+ // How the data is compressed. Compression is applied to each chunk
52
+ // independently.
53
+ CompressionType compression_type = 2;
54
+ }
55
+
56
+ // The data type of the tensor.
57
+ enum DataType {
58
+ // Unspecified data type.
59
+ DATA_TYPE_UNSPECIFIED = 0;
60
+
61
+ // 16-bit "Brain Floating Point". See
62
+ // https://en.wikipedia.org/wiki/Bfloat16_floating-point_format for more
63
+ // details.
64
+ DATA_TYPE_BFLOAT16 = 1;
65
+
66
+ // 16-bit floating point.
67
+ DATA_TYPE_FLOAT16 = 11;
68
+
69
+ // 32-bit floating point.
70
+ DATA_TYPE_FLOAT32 = 2;
71
+
72
+ // 64-bit floating point.
73
+ DATA_TYPE_FLOAT64 = 3;
74
+
75
+ // 8-bit signed integer.
76
+ DATA_TYPE_INT8 = 4;
77
+
78
+ // 32-bit signed integer.
79
+ DATA_TYPE_INT32 = 5;
80
+
81
+ // 64-bit signed integer.
82
+ DATA_TYPE_INT64 = 6;
83
+
84
+ // 8-bit unsigned integer.
85
+ DATA_TYPE_UINT8 = 7;
86
+
87
+ // 32-bit unsigned integer.
88
+ DATA_TYPE_UINT32 = 8;
89
+
90
+ // 64-bit unsigned integer.
91
+ DATA_TYPE_UINT64 = 9;
92
+
93
+ // 8-bit boolean.
94
+ DATA_TYPE_BOOL = 10;
95
+ }
96
+
97
+ // Compression type for the tensor data.
98
+ enum CompressionType {
99
+ // No compression.
100
+ COMPRESSION_TYPE_NONE = 0;
101
+
102
+ // ZSTD compression.
103
+ COMPRESSION_TYPE_ZSTD = 1;
104
+ }
flax_model/alphagenome/_sdk/protos/tensor_pb2.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: alphagenome/protos/tensor.proto
4
+ # Protobuf Python Version: 4.25.1
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+
16
+
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61lphagenome/protos/tensor.proto\x12(google.gdm.gdmscience.alphagenome.v1main\"\xc8\x01\n\x06Tensor\x12\r\n\x05shape\x18\x01 \x03(\x05\x12\x45\n\tdata_type\x18\x02 \x01(\x0e\x32\x32.google.gdm.gdmscience.alphagenome.v1main.DataType\x12\x46\n\x05\x61rray\x18\x03 \x01(\x0b\x32\x35.google.gdm.gdmscience.alphagenome.v1main.TensorChunkH\x00\x12\x15\n\x0b\x63hunk_count\x18\x04 \x01(\x03H\x00\x42\t\n\x07payload\"p\n\x0bTensorChunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12S\n\x10\x63ompression_type\x18\x02 \x01(\x0e\x32\x39.google.gdm.gdmscience.alphagenome.v1main.CompressionType*\x95\x02\n\x08\x44\x61taType\x12\x19\n\x15\x44\x41TA_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12\x44\x41TA_TYPE_BFLOAT16\x10\x01\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT16\x10\x0b\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT32\x10\x02\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT64\x10\x03\x12\x12\n\x0e\x44\x41TA_TYPE_INT8\x10\x04\x12\x13\n\x0f\x44\x41TA_TYPE_INT32\x10\x05\x12\x13\n\x0f\x44\x41TA_TYPE_INT64\x10\x06\x12\x13\n\x0f\x44\x41TA_TYPE_UINT8\x10\x07\x12\x14\n\x10\x44\x41TA_TYPE_UINT32\x10\x08\x12\x14\n\x10\x44\x41TA_TYPE_UINT64\x10\t\x12\x12\n\x0e\x44\x41TA_TYPE_BOOL\x10\n*G\n\x0f\x43ompressionType\x12\x19\n\x15\x43OMPRESSION_TYPE_NONE\x10\x00\x12\x19\n\x15\x43OMPRESSION_TYPE_ZSTD\x10\x01\x42\x92\x01\n,com.google.gdm.gdmscience.alphagenome.v1mainB\x0bTensorProtoP\x01ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenomeb\x06proto3')
18
+
19
+ _globals = globals()
20
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flax_model.alphagenome._sdk.protos.tensor_pb2', _globals)
22
+ if _descriptor._USE_C_DESCRIPTORS == False:
23
+ _globals['DESCRIPTOR']._options = None
24
+ _globals['DESCRIPTOR']._serialized_options = b'\n,com.google.gdm.gdmscience.alphagenome.v1mainB\013TensorProtoP\001ZSgoogle.golang.org/genproto/googleapis/gdm/gdmscience/alphagenome/v1main;alphagenome'
25
+ _globals['_DATATYPE']._serialized_start=395
26
+ _globals['_DATATYPE']._serialized_end=672
27
+ _globals['_COMPRESSIONTYPE']._serialized_start=674
28
+ _globals['_COMPRESSIONTYPE']._serialized_end=745
29
+ _globals['_TENSOR']._serialized_start=78
30
+ _globals['_TENSOR']._serialized_end=278
31
+ _globals['_TENSORCHUNK']._serialized_start=280
32
+ _globals['_TENSORCHUNK']._serialized_end=392
33
+ # @@protoc_insertion_point(module_scope)
flax_model/alphagenome/_sdk/protos/tensor_pb2_grpc.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+
flax_model/alphagenome/_sdk/tensor_utils.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utility functions for converting NumPy arrays to Tensor protocol buffers."""
16
+
17
+ from collections.abc import Iterable, Sequence
18
+
19
+ from flax_model.alphagenome._sdk.protos import tensor_pb2
20
+ import immutabledict
21
+ import ml_dtypes
22
+ import numpy as np
23
+ import zstandard
24
+
25
+
26
+ _TENSOR_DTYPE_TO_NUMPY_DTYPE = immutabledict.immutabledict({
27
+ tensor_pb2.DataType.DATA_TYPE_BFLOAT16: np.dtype(ml_dtypes.bfloat16),
28
+ tensor_pb2.DataType.DATA_TYPE_FLOAT16: np.dtype(np.float16),
29
+ tensor_pb2.DataType.DATA_TYPE_FLOAT32: np.dtype(np.float32),
30
+ tensor_pb2.DataType.DATA_TYPE_FLOAT64: np.dtype(np.float64),
31
+ tensor_pb2.DataType.DATA_TYPE_INT8: np.dtype(np.int8),
32
+ tensor_pb2.DataType.DATA_TYPE_INT32: np.dtype(np.int32),
33
+ tensor_pb2.DataType.DATA_TYPE_INT64: np.dtype(np.int64),
34
+ tensor_pb2.DataType.DATA_TYPE_UINT8: np.dtype(np.uint8),
35
+ tensor_pb2.DataType.DATA_TYPE_UINT32: np.dtype(np.uint32),
36
+ tensor_pb2.DataType.DATA_TYPE_UINT64: np.dtype(np.uint64),
37
+ tensor_pb2.DataType.DATA_TYPE_BOOL: np.dtype(bool),
38
+ })
39
+
40
+ _NUMPY_DTYPE_TO_TENSOR_DTYPE = immutabledict.immutabledict(
41
+ {value: key for key, value in _TENSOR_DTYPE_TO_NUMPY_DTYPE.items()}
42
+ )
43
+
44
+
45
+ def _compress_bytes(
46
+ array: np.ndarray, compression_type: tensor_pb2.CompressionType
47
+ ):
48
+ """Compresses a c-contiguous array to the specified compression type."""
49
+ assert array.flags.c_contiguous
50
+ array = array.view(np.uint8)
51
+ match compression_type:
52
+ case tensor_pb2.CompressionType.COMPRESSION_TYPE_ZSTD:
53
+ return zstandard.compress(array.data)
54
+ case tensor_pb2.CompressionType.COMPRESSION_TYPE_NONE:
55
+ return bytes(array.data)
56
+
57
+
58
+ def _decompress_bytes(
59
+ data: bytes, compression_type: tensor_pb2.CompressionType
60
+ ):
61
+ """Decompress bytes using the specified compression type."""
62
+ match compression_type:
63
+ case tensor_pb2.CompressionType.COMPRESSION_TYPE_ZSTD:
64
+ return zstandard.decompress(data)
65
+ case tensor_pb2.CompressionType.COMPRESSION_TYPE_NONE:
66
+ return data
67
+
68
+
69
+ def pack_tensor(
70
+ value: ...,
71
+ *,
72
+ bytes_per_chunk: int = 0,
73
+ compression_type: tensor_pb2.CompressionType = (
74
+ tensor_pb2.CompressionType.COMPRESSION_TYPE_NONE
75
+ ),
76
+ ) -> tuple[tensor_pb2.Tensor, Sequence[tensor_pb2.TensorChunk]]:
77
+ """Encodes the value as a Tensor and optional sequence of chunks.
78
+
79
+ Args:
80
+ value: An array-like object to pack. For example, scalar (float, int, bool,
81
+ etc.), NumPy array, or nested lists of scalars.
82
+ bytes_per_chunk: The number of bytes to include in each chunk. If 0, the
83
+ entire value will be packed into the Tensor proto, otherwise the value
84
+ will be split into chunks of this size.
85
+ compression_type: The type of compression to apply to the data. This is
86
+ applied to each chunk separately.
87
+
88
+ Returns:
89
+ Tuple of Tensor protocol buffer and, if items_per_chunk is greater than 0, a
90
+ sequence of TensorChunk protos.
91
+ """
92
+ packed = tensor_pb2.Tensor()
93
+ value = np.ascontiguousarray(value)
94
+
95
+ packed.shape[:] = value.shape
96
+ packed.data_type = _NUMPY_DTYPE_TO_TENSOR_DTYPE[value.dtype]
97
+
98
+ chunks = []
99
+ if bytes_per_chunk > 0:
100
+ items_per_chunk = bytes_per_chunk // value.itemsize
101
+ if bytes_per_chunk < value.itemsize:
102
+ raise ValueError(f'{bytes_per_chunk=} must be >= {value.itemsize=}.')
103
+ for chunk in np.split(
104
+ value.ravel(), range(items_per_chunk, value.size, items_per_chunk)
105
+ ):
106
+ chunks.append(
107
+ tensor_pb2.TensorChunk(
108
+ data=_compress_bytes(chunk, compression_type),
109
+ compression_type=compression_type,
110
+ )
111
+ )
112
+ packed.chunk_count = len(chunks)
113
+ else:
114
+ packed.array.data = _compress_bytes(value, compression_type)
115
+ packed.array.compression_type = compression_type
116
+
117
+ return packed, chunks
118
+
119
+
120
+ def unpack_proto(
121
+ proto: tensor_pb2.Tensor,
122
+ chunks: Iterable[tensor_pb2.TensorChunk] = (),
123
+ ) -> np.ndarray:
124
+ """Converts a Tensor proto and any chunks into a NumPy array.
125
+
126
+ Args:
127
+ proto: Tensor proto to unpack.
128
+ chunks: Optional sequence of TensorChunk protos to unpack.
129
+
130
+ Returns:
131
+ NumPy array of the unpacked data.
132
+ """
133
+ dtype = _TENSOR_DTYPE_TO_NUMPY_DTYPE[proto.data_type]
134
+ match proto.WhichOneof('payload'):
135
+ case 'array':
136
+ data = _decompress_bytes(proto.array.data, proto.array.compression_type)
137
+ array = np.frombuffer(data, dtype=dtype).reshape(proto.shape)
138
+ case 'chunk_count':
139
+ array = np.empty(np.prod(proto.shape) * dtype.itemsize, dtype=np.uint8)
140
+ bytes_received = 0
141
+ for chunk in chunks:
142
+ chunk_data = np.frombuffer(
143
+ _decompress_bytes(chunk.data, chunk.compression_type),
144
+ dtype=np.uint8,
145
+ )
146
+ array[bytes_received : bytes_received + chunk_data.nbytes] = chunk_data
147
+ bytes_received += chunk_data.nbytes
148
+
149
+ if bytes_received != array.nbytes:
150
+ raise ValueError(
151
+ f'Expected {array.nbytes} bytes but only received {bytes_received} '
152
+ 'bytes.'
153
+ )
154
+ array = array.view(dtype).reshape(proto.shape)
155
+ case _:
156
+ raise ValueError(
157
+ f'Unsupported payload type: {proto.WhichOneof("payload")}'
158
+ )
159
+
160
+ return array
161
+
162
+
163
+ def upcast_floating(x: np.ndarray) -> np.ndarray:
164
+ """Helper to upcast low-precision floating point arrays to float32."""
165
+ dtype = np.result_type(x)
166
+ if (
167
+ np.issubdtype(dtype, np.floating) or dtype == ml_dtypes.bfloat16
168
+ ) and dtype.itemsize < 4:
169
+ return x.astype(np.float32)
170
+ else:
171
+ return x
flax_model/alphagenome/_sdk/typing.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utility functions for type annotations."""
16
+
17
+ import importlib.metadata
18
+ from typing import TypeVar
19
+
20
+ import jaxtyping
21
+ import typeguard
22
+
23
+ _T = TypeVar('_T')
24
+
25
+
26
+ def jaxtyped(fn: _T) -> _T:
27
+ """Wrapper around jaxtyping.jaxtyped that uses typeguard iff typeguard < 3."""
28
+ try:
29
+ major, *_ = importlib.metadata.version('typeguard').split('.')
30
+ except importlib.metadata.PackageNotFoundError:
31
+ major = -1
32
+
33
+ # Only use jaxtyping if typeguard is < 3. See
34
+ # https://docs.kidger.site/jaxtyping/api/runtime-type-checking/#runtime-type-checking
35
+ # for more details.
36
+ if int(major) < 3:
37
+ return jaxtyping.jaxtyped(fn, typechecker=typeguard.typechecked)
38
+ else:
39
+ return fn
flax_model/alphagenome/_sdk/visualization/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Library of tools for visualizing with genomic model predictions."""
flax_model/alphagenome/_sdk/visualization/plot.py ADDED
@@ -0,0 +1,569 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Plotting functions."""
16
+
17
+ from collections.abc import Callable, Mapping, Sequence
18
+ import functools
19
+ import math
20
+ from typing import Any, Literal
21
+
22
+ from absl import logging
23
+ from flax_model.alphagenome._sdk.data import genome
24
+ import matplotlib as mpl
25
+ import matplotlib.pyplot as plt
26
+ import numpy as np
27
+ import pandas as pd
28
+ import seaborn as sns
29
+
30
+
31
+ @functools.lru_cache(maxsize=8)
32
+ def _get_text_path(
33
+ letter: str, letter_colors_scheme: str
34
+ ) -> tuple[mpl.text.TextPath, str]:
35
+ """Returns a memoized TextPath and color for the given letter and scheme."""
36
+ letters_width_colors = dict(
37
+ # Consistent with `classic` scheme from
38
+ # https://github.com/gecrooks/weblogo/blob/master/weblogo/logo.py#L136
39
+ default={
40
+ 'A': (0.35, 'darkgreen'),
41
+ 'C': (0.366, 'blue'),
42
+ 'G': (0.384, 'orange'),
43
+ 'T': (0.305, 'red'),
44
+ },
45
+ # Consistent with factorbook scheme from https://www.factorbook.org/
46
+ factorbook={
47
+ 'A': (0.305, 'red'),
48
+ 'C': (0.366, 'blue'),
49
+ 'G': (0.384, 'orange'),
50
+ 'T': (0.35, 'darkgreen'),
51
+ },
52
+ )
53
+ if (
54
+ letters_width_color := letters_width_colors.get(letter_colors_scheme)
55
+ ) is None:
56
+ raise ValueError(
57
+ f'letter_colors_scheme must be one of {letters_width_colors.keys()}.'
58
+ f'Got "{letter_colors_scheme}".'
59
+ )
60
+ else:
61
+ if (letter_width_color := letters_width_color.get(letter)) is None:
62
+ raise ValueError(
63
+ f'letter must be one of {letters_width_color.keys()}. Got "{letter}".'
64
+ )
65
+ else:
66
+ letter_width, letter_color = letter_width_color
67
+ font = mpl.font_manager.FontProperties(weight='bold')
68
+ return (
69
+ mpl.text.TextPath((-letter_width, 0), letter, size=1, prop=font),
70
+ letter_color,
71
+ )
72
+
73
+
74
+ def seqlogo(
75
+ letter_heights: np.ndarray,
76
+ alphabet: str = 'ACGT',
77
+ one_based: bool = True,
78
+ start: int = 0,
79
+ ax: mpl.axes.Axes | None = None,
80
+ letter_colors: Literal['default', 'factorbook'] = 'default',
81
+ ):
82
+ """Sequence logo plot.
83
+
84
+ Args:
85
+ letter_heights: 2D array with shape (sequence length, len(alphabet)) shape.
86
+ Positive values indicate that a logo element extends above the horizontal
87
+ axis, and negative values indicate that the element extends below.
88
+ alphabet: Alphabet corresponding to the channel axis of `letter_heights`.
89
+ one_based: If True, plot letters at 1-based coordinates: first letter will
90
+ be centered at 1. If False, the first letter will be between 0 and 1.
91
+ start: Interval start.
92
+ ax: matplotlib axis to add figures to.
93
+ letter_colors: Name of the color scheme to use for coloring each letter.
94
+ Must be one of 'default', a scheme consistent with weblogo
95
+ (https://github.com/gecrooks/weblogo/blob/master/weblogo/logo.py#L136) or
96
+ 'factorbook', a scheme consistent with Factorbook
97
+ (https://www.factorbook.org/).
98
+ """
99
+ ax = ax if ax is not None else plt.gca()
100
+
101
+ if letter_heights.ndim != 2:
102
+ raise ValueError(
103
+ 'Expecting a 2D matrix of shape (sequence_length, len(alphabed))'
104
+ )
105
+ if letter_heights.shape[1] != len(alphabet):
106
+ raise ValueError('Last axis needs to match len(alphabet)')
107
+
108
+ globscale = 1.35
109
+ paths = []
110
+ facecolors = []
111
+
112
+ for x_offset, heights in enumerate(letter_heights):
113
+ last_positive_y = 0.0
114
+ last_negative_y = 0.0
115
+ x = start + x_offset + 0.5 + 0.5 * one_based
116
+
117
+ for height, letter in sorted(zip(heights, alphabet)):
118
+ if height == 0:
119
+ continue
120
+ # Start with lowest height and keep track of the last y coordinate.
121
+ if height > 0:
122
+ y_position = last_positive_y
123
+ last_positive_y += height
124
+ else:
125
+ y_position = last_negative_y
126
+ last_negative_y += height
127
+
128
+ base_path, letter_color = _get_text_path(letter, letter_colors)
129
+
130
+ transform = (
131
+ mpl.transforms.Affine2D()
132
+ .scale(globscale, height * globscale)
133
+ .translate(x, y_position)
134
+ )
135
+ paths.append(base_path.transformed(transform))
136
+ facecolors.append(letter_color)
137
+
138
+ if paths:
139
+ collection = mpl.collections.PathCollection(
140
+ paths,
141
+ facecolors=facecolors,
142
+ edgecolors='none',
143
+ linewidths=0,
144
+ transform=ax.transData,
145
+ )
146
+ ax.add_collection(collection)
147
+
148
+ # Make sure all letters are displayed.
149
+ ax.autoscale_view()
150
+
151
+
152
+ def plot_contact_map(
153
+ contact_map: pd.DataFrame,
154
+ vmin: float | None = None,
155
+ vmax: float | None = None,
156
+ square: bool = True,
157
+ cbar_shrink: float = 0.4,
158
+ ax=None,
159
+ **kwargs,
160
+ ):
161
+ """Visualize a contact map.
162
+
163
+ A contact map is a 2D array of values, where each value represents the
164
+ probability that two DNA bases are in contact.
165
+
166
+ The array is symmetric, i.e., the (i, j) values and the (j, i) values of
167
+ `contact_map` are equal.
168
+
169
+ Args:
170
+ contact_map: Contact map to visualize.
171
+ vmin: Minimum value for the colorbar.
172
+ vmax: Maximum value for the colorbar.
173
+ square: If `True`, plots the contact map as a square. If `False`, plots the
174
+ contact map as a rectangle with a shorter height than width.
175
+ cbar_shrink: Fraction by which to multiply the size of the colorbar.
176
+ ax: Matplotlib axis to add the heatmap to.
177
+ **kwargs: Additional keyword arguments passed to `sns.heatmap`.
178
+ """
179
+
180
+ def tuple2string(interval_tuple):
181
+ chromosome, start, end = interval_tuple
182
+ return f'{chromosome}:{start:,}-{end:,}'
183
+
184
+ if isinstance(contact_map, pd.DataFrame):
185
+ interval_string = (
186
+ tuple2string(contact_map.index[0])
187
+ + ' - '
188
+ + tuple2string(contact_map.index[-1])
189
+ )
190
+ contact_map = contact_map.values
191
+ else:
192
+ interval_string = ''
193
+
194
+ if vmin is None:
195
+ vmin = np.nanmin(contact_map[contact_map > 0])
196
+ if vmax is None:
197
+ vmax = np.nanmax(contact_map)
198
+ fall_cmap = mpl.colors.LinearSegmentedColormap.from_list(
199
+ 'fall',
200
+ colors=[
201
+ 'white',
202
+ (245 / 256, 166 / 256, 35 / 256),
203
+ (208 / 256, 2 / 256, 27 / 256),
204
+ 'black',
205
+ ],
206
+ )
207
+ log_norm = mpl.colors.LogNorm(vmin=vmin, vmax=vmax)
208
+ cbar_min = math.floor(math.log10(log_norm.vmin))
209
+ cbar_max = 1 + math.ceil(math.log10(log_norm.vmax))
210
+ cbar_ticks = [math.pow(10, i) for i in range(cbar_min, cbar_max)]
211
+
212
+ if ax is None:
213
+ ax = plt.gca()
214
+ sns.heatmap(
215
+ contact_map + 1e-10,
216
+ cmap=fall_cmap,
217
+ norm=log_norm,
218
+ cbar_kws={'ticks': cbar_ticks, 'shrink': cbar_shrink},
219
+ vmin=log_norm.vmin,
220
+ vmax=log_norm.vmax,
221
+ xticklabels=False,
222
+ yticklabels=False,
223
+ square=square,
224
+ ax=ax,
225
+ **kwargs,
226
+ )
227
+
228
+ ax.set_xlabel(interval_string)
229
+
230
+
231
+ def plot_track(
232
+ arr: np.ndarray,
233
+ ax: plt.Axes,
234
+ x: np.ndarray | None = None,
235
+ legend: bool = False,
236
+ ylim: float | None = None,
237
+ color: str | Sequence[str] | None = None,
238
+ filled: bool = False,
239
+ ) -> None:
240
+ """Plot a single track on the axis after inferring track type from array.
241
+
242
+ This function infers the type of track plot depending on channel
243
+ dimensionality of `arr`:
244
+
245
+ - If `arr` is a single-dimensional array, plot a single track line.
246
+ - If `arr` is a 2D array, and interpreting the second dimension as the
247
+ channel dimension:
248
+
249
+ - Plot a sequence logo if the number of channels is 4 (which suggests one
250
+ channel per DNA base).
251
+ - Plot two overlapping line plots if the number of channels is 2 (which
252
+ suggests one channel per DNA strand).
253
+
254
+ Args:
255
+ arr: array of values to plot.
256
+ ax: matplotlib axis to use for plotting the values on.
257
+ x: optional array of values to use for setting the x axis values. If absent,
258
+ then sequential values starting from 1 to the length of the array arr will
259
+ be used.
260
+ legend: whether to draw a legend or not on the double stranded DNA plot.
261
+ ylim: y axis limit.
262
+ color: color(s) used for line plots.
263
+ filled: whether to fill the space between the x axis and the line (or leave
264
+ it as whitespace).
265
+
266
+ Returns:
267
+ None. Adds a plot to the provided axis.
268
+ """
269
+ sequence_length = len(arr)
270
+
271
+ if x is None:
272
+ x = np.arange(1, sequence_length + 1) # One-based indexing.
273
+
274
+ if arr.ndim == 1 or arr.shape[1] == 1:
275
+ if color is not None:
276
+ if isinstance(color, (list, tuple)):
277
+ logging.warning(
278
+ 'A sequence of colors was passed to plot_track but the second '
279
+ 'array dimension is 1 suggesting single stranded DNA. Using the '
280
+ 'first entry %s as the line color.',
281
+ color[0],
282
+ )
283
+ color = color[0]
284
+
285
+ # In the case of a boolean array, we fill between values and the x axis and
286
+ # simplify the y axis components.
287
+ if arr.dtype == bool:
288
+ ax.fill_between(x, arr, step='mid', color=color)
289
+ ax.yaxis.set_ticks_position('none')
290
+ ax.set_yticklabels([])
291
+ ax.spines['left'].set_visible(False)
292
+ ax.set_ylim([0, 1.5])
293
+
294
+ else:
295
+ if filled:
296
+ ax.fill_between(x, np.ravel(arr), color=color)
297
+ ax.plot(x, np.ravel(arr), color=color)
298
+
299
+ elif arr.shape[1] == 4:
300
+ seqlogo(arr, ax=ax)
301
+
302
+ elif arr.shape[1] == 2:
303
+ if color is not None:
304
+ if not isinstance(color, Sequence) or len(color) != 2:
305
+ raise ValueError(
306
+ 'Must pass sequence of 2 colors if plotting 2 dimensional array.'
307
+ )
308
+ color_1, color_2 = color
309
+ else:
310
+ color_1, color_2 = None, None
311
+ ax.plot(x, arr[:, 0], label='pos', color=color_1)
312
+ ax.plot(x, arr[:, 1], label='neg', color=color_2)
313
+ if legend:
314
+ ax.legend()
315
+ else:
316
+ raise ValueError(
317
+ f'Do not know how to plot array with shape[1] != {arr.shape[1]}. '
318
+ 'Valid values are: 1, 2, or 4.'
319
+ )
320
+ if ylim is not None:
321
+ ax.set_ylim(ylim)
322
+
323
+
324
+ def plot_tracks(
325
+ tracks: Mapping[str, Any],
326
+ x: np.ndarray | None = None,
327
+ title: str | None = None,
328
+ legend: bool = False,
329
+ fig_width: float = 20,
330
+ fig_track_height: float | Mapping[str, float] = 1.5,
331
+ ylim: str | Mapping[str, tuple[int, int]] | tuple[int, int] | None = 'auto',
332
+ yticks_min_max_only: bool = False,
333
+ ylab: bool = True,
334
+ color: str | Mapping[str, str] | None = None,
335
+ horizontal_ylab: bool = True,
336
+ filled_tracks: Sequence[str] | None = None,
337
+ despine: bool = True,
338
+ despine_keep_bottom: bool = False,
339
+ plot_track_fn: Callable[..., Any] = plot_track,
340
+ ) -> mpl.figure.Figure:
341
+ """Plot multiple tracks as subplots within one matplotlib figure.
342
+
343
+ Custom plotting functions may be passed as `plot_track_fn`. Otherwise,
344
+ `plot_track` will be used by default.
345
+
346
+ Args:
347
+ tracks: dictionary of tracks to plot. Example input: tracks = {'a': [1,2,3],
348
+ 'b': [4,5,4]}. The arrays in the dict must have the same length.
349
+ x: optional array of x axis values. If absent, these will be inferred to be
350
+ from 1 to the length of the arrays in tracks.
351
+ title: Optional title to be added to the first subplot in the figure.
352
+ legend: Whether to plot a legend or not.
353
+ fig_width: Figure width.
354
+ fig_track_height: Either a scalar specifying the total height of the figure,
355
+ or a dictionary specifying the height of the subplot for each track.
356
+ ylim: y axis limit. Must be either "same" (shared y limit across subplots)
357
+ or "auto" (free scales, indicating separate limits per subplot).
358
+ yticks_min_max_only: whether the only y axis ticks should be the min and max
359
+ values.
360
+ ylab: y axis label.
361
+ color: Either a string or a dict indicating a color per track.
362
+ horizontal_ylab: Whether to display the y axis label horizontally (instead
363
+ of the default vertical orientation).
364
+ filled_tracks: List of track names that should have filled line plots drawn
365
+ (instead of the default whitespace between the values and x axis).
366
+ despine: Whether to remove top, right, and bottom spines.
367
+ despine_keep_bottom: Whether to remove top and right spines, but keep the
368
+ bottom spine.
369
+ plot_track_fn: Optional custom function for plotting tracks. If absent,
370
+ defaults to `plot_track`.
371
+
372
+ Returns:
373
+ Matplotlib figure.
374
+ """
375
+ # Set figure height (either total height, or per subplot if passed a dict).
376
+ if isinstance(fig_track_height, dict):
377
+ fig_height = sum(fig_track_height.values())
378
+ gridspec_kw = {
379
+ 'height_ratios': [
380
+ height / fig_height for height in fig_track_height.values()
381
+ ]
382
+ }
383
+ else:
384
+ fig_height = fig_track_height
385
+ gridspec_kw = dict()
386
+
387
+ # Generate figure axes.
388
+ fig, axes = plt.subplots(
389
+ nrows=len(tracks),
390
+ ncols=1,
391
+ figsize=(fig_width, fig_height),
392
+ gridspec_kw=gridspec_kw,
393
+ sharex=True,
394
+ )
395
+ if len(tracks) == 1:
396
+ axes = [axes]
397
+
398
+ # Set y axis limits.
399
+ if ylim == 'same':
400
+ ylim = (
401
+ 0,
402
+ max(v.max() for v in tracks.values() if isinstance(v, np.ndarray)),
403
+ )
404
+ elif ylim == 'auto':
405
+ ylim = None
406
+ elif isinstance(ylim, str):
407
+ raise ValueError('Only "same" and "auto" are valid strings for ylim.')
408
+
409
+ # Iterate over tracks and plot one track per axis in the figure.
410
+ for i, (ax, (track, arr)) in enumerate(zip(axes, tracks.items())):
411
+ # Only set title in the first subplot.
412
+ if i == 0 and title is not None:
413
+ ax.set_title(title)
414
+
415
+ # Only set x axis ticks in the final subplot.
416
+ if i != len(tracks) - 1:
417
+ ax.xaxis.set_ticks_position('none')
418
+
419
+ track_ylim = ylim[track] if isinstance(ylim, dict) else ylim
420
+ track_color = color[track] if isinstance(color, dict) else color
421
+
422
+ # If the array is in fact a callable, then it is intended to be used as a
423
+ # custom plotting function (most commonly, for plotting transcripts).
424
+ if hasattr(arr, '__call__'):
425
+ # TODO: b/377225766 - Allow custom plot functions as dictionary elements.
426
+ arr(ax, ylim=ylim, color=color)
427
+ else:
428
+ filled_tracks = filled_tracks or []
429
+ plot_track_fn(
430
+ arr,
431
+ ax=ax,
432
+ x=x,
433
+ legend=legend,
434
+ ylim=track_ylim,
435
+ color=track_color,
436
+ filled=track in filled_tracks,
437
+ )
438
+
439
+ # Set y axis label.
440
+ if ylab:
441
+ if horizontal_ylab:
442
+ ax.set_ylabel(
443
+ track,
444
+ rotation=0,
445
+ multialignment='center',
446
+ va='center',
447
+ ha='right',
448
+ labelpad=5,
449
+ )
450
+ else:
451
+ ax.set_ylabel(track)
452
+
453
+ # Only draw the y axis ticks on the min and max value locations.
454
+ if yticks_min_max_only:
455
+ if track_ylim is None:
456
+ track_ylim = ax.get_yticks()[[0, -1]]
457
+ # TODO: b/377226499 - Preventing negative values might be sub-optimal.
458
+ minimum = max(track_ylim[0], 0)
459
+ maximum = track_ylim[1]
460
+ ax.spines['left'].set_bounds(minimum, maximum)
461
+ ax.set_yticks([minimum, maximum])
462
+
463
+ if despine:
464
+ ax.spines['top'].set_visible(False)
465
+ ax.spines['right'].set_visible(False)
466
+ if i != len(tracks) - 1 and despine_keep_bottom:
467
+ ax.spines['bottom'].set_visible(True)
468
+ else:
469
+ ax.spines['bottom'].set_visible(False)
470
+
471
+ # Enable default tick locator for the final subplot.
472
+ axes[-1].xaxis.set_major_locator(mpl.ticker.AutoLocator())
473
+
474
+ # No height for whitespace between subplots.
475
+ fig.subplots_adjust(hspace=0)
476
+
477
+ return fig
478
+
479
+
480
+ def sashimi_plot(
481
+ junctions: Sequence[genome.Junction],
482
+ ax: plt.Axes,
483
+ interval: genome.Interval | None = None,
484
+ filter_threshold: float = 0.01,
485
+ annotate_counts: bool = True,
486
+ rng: np.random.Generator | None = None,
487
+ ):
488
+ """Plot splice junctions as a Sashimi plot.
489
+
490
+ Sashimi plots (first described [here](https://arxiv.org/abs/1306.3466)
491
+ visualize splice junctions from an RNA-seq experiment.
492
+
493
+ According to the authors,
494
+ * genomic reads are converted into read densities
495
+ * junction reads are plotted as arcs whose width is determined by the number
496
+ of reads aligned to the junction spanning the exons connected by the arc.
497
+
498
+ Args:
499
+ junctions: Splice junctions to plot.
500
+ ax: Matplotlib axis to add the plot to.
501
+ interval: Interval to plot, used to filter text annotations.
502
+ filter_threshold: Junctions with number of reads below this threshold will
503
+ be filtered out.
504
+ annotate_counts: Whether to annotate the junctions with read counts.
505
+ rng: Optional random number generator to use for jittering junction paths.
506
+ If unset will use NumPy's default random number generator.
507
+ """
508
+ rng = rng or np.random.default_rng()
509
+ total = np.sum([junction.k for junction in junctions])
510
+ # Random jitter position to avoid overlap.
511
+ jitters = rng.uniform(low=0.05, high=0.15, size=len(junctions))
512
+ for junction, jt in zip(junctions, jitters):
513
+ if junction.k < filter_threshold:
514
+ continue
515
+ k = junction.k / total
516
+ verts = [
517
+ (junction.start, 0.0),
518
+ (junction.start, jt),
519
+ (junction.end, jt),
520
+ (junction.end, 0.0),
521
+ ]
522
+
523
+ path = mpl.path.Path(
524
+ verts,
525
+ [
526
+ mpl.path.Path.MOVETO,
527
+ mpl.path.Path.CURVE4,
528
+ mpl.path.Path.CURVE4,
529
+ mpl.path.Path.CURVE4,
530
+ ],
531
+ )
532
+ patch = mpl.patches.PathPatch(path, facecolor='none', lw=min(k * 30, 5))
533
+ ax.add_patch(patch)
534
+ if annotate_counts:
535
+ text_pos = junction.center()
536
+ if interval is not None:
537
+ if text_pos < interval.start or text_pos > interval.end:
538
+ continue
539
+ ax.text(text_pos, 0.8 * jt, junction.k, horizontalalignment='center')
540
+ ax.set_ylim(0, 0.15)
541
+
542
+
543
+ def pad_track(track: np.ndarray, new_len: int, value: int = 0) -> np.ndarray:
544
+ """Pad a track with `value` to the desired length.
545
+
546
+ If new_len - len(track) is an even number, the same amount of padding is added
547
+ to both sides.
548
+
549
+ If new_len - len(track) is an odd number, the extra padded element will be
550
+ added at the end of the array.
551
+
552
+ Args:
553
+ track: Track to pad.
554
+ new_len: Desired length of the padded track.
555
+ value: Value to use for padding.
556
+
557
+ Returns:
558
+ Padded track.
559
+ """
560
+ assert track.ndim == 2
561
+ assert new_len >= len(track)
562
+
563
+ out = np.empty((new_len, track.shape[1]))
564
+ out[:] = value
565
+ delta = new_len - len(track)
566
+ i = delta // 2
567
+ j = i + len(track)
568
+ out[i:j] = track
569
+ return out
flax_model/alphagenome/_sdk/visualization/plot_components.py ADDED
@@ -0,0 +1,1555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """Module containing the main plotting code for AlphaGenome model outputs.
17
+
18
+ Three main elements are:
19
+
20
+ * `plot` function: The primary function for visualizing model outputs.
21
+ * Component classes: Implement the visualization components (e.g., tracks,
22
+ contact maps).
23
+ * Annotation classes: Implement the visualization annotations (e.g.,
24
+ intervals, variants).
25
+ """
26
+
27
+ import abc
28
+ from collections.abc import Mapping, Sequence
29
+
30
+ from flax_model.alphagenome._sdk.data import genome
31
+ from flax_model.alphagenome._sdk.data import junction_data
32
+ from flax_model.alphagenome._sdk.data import track_data
33
+ from flax_model.alphagenome._sdk.data import transcript as transcript_utils
34
+ from flax_model.alphagenome._sdk.visualization import plot as plot_lib
35
+ from flax_model.alphagenome._sdk.visualization import plot_transcripts
36
+ from jaxtyping import Float32 # pylint: disable=g-importing-member
37
+ import matplotlib
38
+ from matplotlib import colors as plt_colors
39
+ import matplotlib.pyplot as plt
40
+ import numpy as np
41
+
42
+
43
+ # String, RGB or RGBA color.
44
+ _ColorType = (
45
+ str | tuple[float, float, float] | tuple[float, float, float, float]
46
+ )
47
+
48
+
49
+ def plot(
50
+ components: Sequence['AbstractComponent'],
51
+ interval: genome.Interval,
52
+ fig_width: int = 20,
53
+ fig_height_scale: float = 1.0,
54
+ title: str | None = None,
55
+ despine: bool = True,
56
+ despine_keep_bottom: bool = False,
57
+ annotations: Sequence['AbstractAnnotation'] | None = None,
58
+ annotation_offset_range: tuple[float, float] = (0.1, 0.6),
59
+ hspace: float = 0.3,
60
+ xlabel: str | None = None,
61
+ ) -> matplotlib.figure.Figure:
62
+ """Plots AlphaGenome model outputs as individual panels of 'components'.
63
+
64
+ This function generates a visualization of AlphaGenome model outputs
65
+ using a combination of components (e.g., tracks, contact maps) and
66
+ annotations (e.g., intervals, variants).
67
+
68
+ Args:
69
+ components: A sequence of components to visualize.
70
+ interval: The genomic interval to focus on (similar to setting xlim in
71
+ matplotlib).
72
+ fig_width: The total figure width.
73
+ fig_height_scale: Height of the individual track unit. Total plot height is
74
+ determined as a sum of the individual component heights.
75
+ title: An optional title for the overall plot.
76
+ despine: Whether to remove top, right, and bottom spines from the axes.
77
+ despine_keep_bottom: Whether to remove top and right spines, but keep the
78
+ bottom spine. Does not apply to transcript components, which are always
79
+ despined.
80
+ annotations: Sequence of annotations to visualise across all components.
81
+ annotation_offset_range: Relative positions in y-axis to place labels for
82
+ annotations. Each set of labels in the 'annotations' list are spaced
83
+ evenly within this range.
84
+ hspace: Vertical whitespace between subplots to avoid tick label overlap
85
+ (relative fraction).
86
+ xlabel: If a non-empty string is provided, it is used as the x-axis label.
87
+ If an empty string is provided, the x-axis label is removed. If None, the
88
+ default x-axis label is used.
89
+
90
+ Returns:
91
+ A matplotlib figure.
92
+ """
93
+
94
+ # If we are adding text labels for any of the annotation components,
95
+ # we need to add an extra empty component at the top.
96
+ add_label_axis = False
97
+ if annotations is not None:
98
+ add_label_axis = any(annot.has_labels for annot in annotations)
99
+ components = (
100
+ [EmptyComponent()] + list(components)
101
+ if add_label_axis
102
+ else list(components)
103
+ )
104
+
105
+ num_axes = sum(component.num_axes for component in components)
106
+ fig_height = sum(
107
+ component.total_height * fig_height_scale for component in components
108
+ )
109
+
110
+ offset = 0
111
+ gridspec_kw = {'height_ratios': []}
112
+ for component in components:
113
+ for i in range(component.num_axes):
114
+ gridspec_kw['height_ratios'].append(
115
+ component.get_ax_height(i) * fig_height_scale / fig_height
116
+ )
117
+ offset += 1
118
+
119
+ fig, axes = plt.subplots(
120
+ nrows=num_axes,
121
+ ncols=1,
122
+ figsize=(fig_width, fig_height),
123
+ gridspec_kw=gridspec_kw,
124
+ sharex=True,
125
+ )
126
+ # Handle the case of a single axis (e.g. single REF / ALT plot).
127
+ if not isinstance(axes, np.ndarray):
128
+ axes = [axes]
129
+
130
+ offset = 0
131
+ for component in components:
132
+ for i in range(component.num_axes):
133
+ ax = axes[offset]
134
+ component.plot_ax(ax, axis_index=i, interval=interval)
135
+
136
+ if offset == 0 and title is not None:
137
+ ax.set_title(title)
138
+
139
+ if despine:
140
+ ax.spines['top'].set_visible(False)
141
+ ax.spines['right'].set_visible(False)
142
+ if (
143
+ i != num_axes - 1
144
+ and despine_keep_bottom
145
+ and (not isinstance(component, TranscriptAnnotation))
146
+ ):
147
+ ax.spines['bottom'].set_visible(True)
148
+ else:
149
+ ax.spines['bottom'].set_visible(False)
150
+
151
+ # Remove tick marks which become visible due to a subplots_adjust() call.
152
+ if offset != num_axes - 1:
153
+ ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
154
+
155
+ ax.set_xlim(interval.start, interval.end)
156
+
157
+ offset += 1
158
+
159
+ # Add annotations across all plotted components.
160
+ if annotations is not None:
161
+ # Add small vertical offset for each set of labels to avoid overlap.
162
+ num_labelled_annotations = sum(
163
+ annotation.has_labels for annotation in annotations
164
+ )
165
+ height_offsets = np.linspace(
166
+ annotation_offset_range[0],
167
+ annotation_offset_range[1],
168
+ num_labelled_annotations,
169
+ )
170
+ # Identify which axes are transcripts and which we want to annotate.
171
+ transcript_axes_idx = [
172
+ i
173
+ for i, comp in enumerate(components)
174
+ if isinstance(comp, TranscriptAnnotation)
175
+ ]
176
+ axes_to_annotate_idx = (
177
+ range(1, len(axes)) if add_label_axis else range(len(axes))
178
+ )
179
+ label_index = 0
180
+ for annotation in annotations:
181
+ for annotate_index in axes_to_annotate_idx:
182
+ if not annotation.is_variant and annotate_index in transcript_axes_idx:
183
+ # Do not add interval annotations to axes that involves transcripts.
184
+ continue
185
+ annotation.plot_ax(axes[annotate_index], interval, hspace)
186
+ if annotation.has_labels:
187
+ # Add labels to the empty axis at the top. All labels are added to
188
+ # the top axis, in the order they are supplied in annotations list.
189
+ annotation.plot_labels(axes[0], interval, height_offsets[label_index])
190
+ label_index += 1
191
+
192
+ # Enable default tick locator for the final subplot.
193
+ axes[-1].xaxis.set_major_locator(matplotlib.ticker.AutoLocator())
194
+
195
+ if xlabel is not None:
196
+ axes[-1].set_xlabel(xlabel)
197
+ else:
198
+ axes[-1].set_xlabel(f'Chromosome position; interval={interval}')
199
+
200
+ # Slight whitespace between subplots to avoid tick label overlap.
201
+ fig.subplots_adjust(hspace=hspace)
202
+
203
+ return fig
204
+
205
+
206
+ class AbstractComponent(abc.ABC):
207
+ """Abstract base class for plot components."""
208
+
209
+ @abc.abstractmethod
210
+ def get_ax_height(self, axis_index: int) -> float:
211
+ """Returns the plot height for the individual axis.
212
+
213
+ Args:
214
+ axis_index: The index of the axis.
215
+
216
+ Returns:
217
+ The height of the axis.
218
+ """
219
+
220
+ @property
221
+ def total_height(self) -> float:
222
+ """Returns the total figure height."""
223
+ return sum(self.get_ax_height(i) for i in range(self.num_axes))
224
+
225
+ @property
226
+ @abc.abstractmethod
227
+ def num_axes(self) -> int:
228
+ """Returns the number of matplotlib axes required by the component."""
229
+
230
+ @abc.abstractmethod
231
+ def plot_ax(
232
+ self,
233
+ ax: matplotlib.axes.Axes,
234
+ axis_index: int,
235
+ interval: genome.Interval,
236
+ ):
237
+ """Plots the component on the given axis.
238
+
239
+ Args:
240
+ ax: The matplotlib axis to plot on.
241
+ axis_index: The index of the axis.
242
+ interval: The genomic interval to plot.
243
+ """
244
+
245
+
246
+ class Tracks(AbstractComponent):
247
+ """Component for visualizing tracks."""
248
+
249
+ def __init__(
250
+ self,
251
+ tdata: track_data.TrackData,
252
+ cmap: str = 'viridis',
253
+ truncate_cmap: bool = True,
254
+ track_height: float = 1.0,
255
+ filled: bool = False,
256
+ ylabel_template: str = '{name}:{strand}',
257
+ ylabel_horizontal: bool = True,
258
+ shared_y_scale: bool = False,
259
+ global_ylims: tuple[float, float] | None = None,
260
+ max_num_tracks: int = 50,
261
+ track_colors: Sequence[_ColorType] | str | None = None,
262
+ **kwargs,
263
+ ):
264
+ """Initializes the `Tracks` component.
265
+
266
+ Args:
267
+ tdata: The `TrackData` object to visualize.
268
+ cmap: The colormap to use for the tracks.
269
+ truncate_cmap: Whether to slightly truncate the colormap to avoid extreme
270
+ values.
271
+ track_height: The height of each track.
272
+ filled: Whether to fill the area under the tracks.
273
+ ylabel_template: A template for the y-axis labels.
274
+ ylabel_horizontal: Whether to make the y-axis labels horizontal.
275
+ shared_y_scale: Whether to use the same y-axis scale for all tracks.
276
+ global_ylims: Optional global y-axis limits (min and max).
277
+ max_num_tracks: The maximum number of tracks to plot.
278
+ track_colors: An optional sequence of colors to use for the tracks. If a
279
+ string is passed, it is used as a single color for all tracks.
280
+ **kwargs: Additional keyword arguments to pass to the plotting function.
281
+
282
+ Raises:
283
+ ValueError: If the number of tracks exceeds `max_num_tracks` or if the
284
+ track data has more than one positional axis or if the interval is not
285
+ set within the track data.
286
+ """
287
+ if tdata.num_tracks > max_num_tracks:
288
+ raise ValueError(
289
+ f'Too many tracks to plot: {tdata.num_tracks} > {max_num_tracks}.'
290
+ )
291
+ self._tdata = tdata
292
+ self._num_tracks = tdata.values.shape[-1]
293
+ self._track_height = track_height
294
+ self._filled = filled
295
+ self._ylabel_horizontal = ylabel_horizontal
296
+ self._ylabel_template = ylabel_template
297
+ self._shared_y_scale = shared_y_scale
298
+ self._global_ylims = global_ylims
299
+ self._kwargs = kwargs
300
+
301
+ if len(self._tdata.positional_axes) != 1:
302
+ raise ValueError(
303
+ 'Only track_data with 1 positional axis is supported in Tracks.'
304
+ )
305
+ if self._tdata.interval is None:
306
+ raise ValueError('.interval needs to be set in track_data.')
307
+
308
+ # Set up color per set of interleaved tracks.
309
+ if getattr(self._tdata, 'uns') is not None:
310
+ self._num_tdata = self._tdata.uns['num_interleaved_trackdatas']
311
+ else:
312
+ self._num_tdata = 1
313
+
314
+ cmap = plt.get_cmap(cmap)
315
+ num_track_sets = self._num_tracks // self._num_tdata
316
+ if truncate_cmap:
317
+ # We do *1.2 to make the color change more gradual. This means that the
318
+ # the upper range of the cmap is never displayed, which often tends to
319
+ # achieve nicer results aesthetically.
320
+ self._colors = cmap(np.linspace(0, 1, round(num_track_sets * 1.2)))
321
+ else:
322
+ self._colors = cmap(np.linspace(0, 1, num_track_sets))
323
+
324
+ if track_colors is not None:
325
+ if isinstance(track_colors, str):
326
+ self._colors = [track_colors] * num_track_sets
327
+ elif len(track_colors) == 1:
328
+ self._colors = track_colors * num_track_sets
329
+ elif len(track_colors) != num_track_sets:
330
+ raise ValueError(
331
+ f'track_colors argument (length: {len(track_colors)}) must be'
332
+ ' either a single color, or the same number of track sets provided'
333
+ f' in the tdata ({num_track_sets}).'
334
+ )
335
+ else:
336
+ self._colors = track_colors
337
+
338
+ def _get_ylimits(self, tdata: track_data.TrackData):
339
+ """Computes y-axis limits for track sets."""
340
+ return [
341
+ (
342
+ tdata.values[:, i : i + self._num_tdata].min(),
343
+ tdata.values[:, i : i + self._num_tdata].max(),
344
+ )
345
+ for i in range(0, self._num_tracks, self._num_tdata)
346
+ ]
347
+
348
+ def get_ax_height(self, axis_index: int) -> float:
349
+ """Returns the height of the axis."""
350
+ return self._track_height
351
+
352
+ @property
353
+ def num_axes(self) -> int:
354
+ """Returns the number of matplotlib axes required by the component."""
355
+ return self._tdata.num_tracks
356
+
357
+ def plot_ax(
358
+ self,
359
+ ax: matplotlib.axes.Axes,
360
+ axis_index: int,
361
+ interval: genome.Interval,
362
+ ):
363
+ """Plots the tracks on the given axis.
364
+
365
+ Args:
366
+ ax: The matplotlib axis to plot on.
367
+ axis_index: The index of the axis.
368
+ interval: The genomic interval to plot.
369
+ """
370
+ tdata = self._tdata
371
+ assert tdata.interval is not None
372
+
373
+ # If an interval is passed, zoom in on that specific sub-interval.
374
+ if interval is not None:
375
+ tdata = tdata.slice_by_interval(interval, match_resolution=True)
376
+ del interval
377
+ x = (
378
+ np.arange(tdata.values.shape[0]) * tdata.resolution
379
+ + tdata.interval.start
380
+ + tdata.resolution / 2
381
+ )
382
+ arr = tdata.values[:, axis_index]
383
+
384
+ # Same shared y-axis limits across all tracks.
385
+ if self._shared_y_scale:
386
+ if self._global_ylims is not None:
387
+ ax.set_ylim(self._global_ylims)
388
+ else:
389
+ ax.set_ylim(tdata.values.min(), tdata.values.max())
390
+ else:
391
+ ax.set_ylim(arr.min(), arr.max())
392
+
393
+ # Set the colour of this track.
394
+ track_color = self._colors[axis_index // self._num_tdata]
395
+
396
+ # Draw the line-plot, or filled line-plot if filled=True.
397
+ if self._filled:
398
+ ax.fill_between(x, np.ravel(arr), color=track_color, **self._kwargs)
399
+ else:
400
+ ax.plot(x, arr, c=track_color, **self._kwargs)
401
+
402
+ if self._ylabel_template:
403
+ _set_ylabel(ax, self._get_ylabel(axis_index), self._ylabel_horizontal)
404
+
405
+ def _get_ylabel(self, axis_index: int) -> str:
406
+ """Returns the y-axis label for the given axis index."""
407
+ row = self._tdata.metadata.iloc[axis_index]
408
+ return self._ylabel_template.format(**row.to_dict())
409
+
410
+
411
+ class OverlaidTracks(AbstractComponent):
412
+ """Component for visualizing overlaid track pairs, such as REF/ALT tracks."""
413
+
414
+ def __init__(
415
+ self,
416
+ tdata: Mapping[str, track_data.TrackData],
417
+ colors: Mapping[str, str] | None = None,
418
+ cmap: str | None = 'viridis',
419
+ track_height: float = 1.0,
420
+ ylabel_template: str = '{name}:{strand}',
421
+ ylabel_horizontal: bool = True,
422
+ shared_y_scale: bool = False,
423
+ global_ylims: tuple[float, float] | None = None,
424
+ yticks: Sequence[float] | None = None,
425
+ yticklabels: Sequence[str] | None = None,
426
+ alpha: float = 0.8,
427
+ order_tdata_by_mean: bool = True,
428
+ max_num_tracks: int = 50,
429
+ legend_loc: str = 'upper right',
430
+ **kwargs,
431
+ ):
432
+ """Initializes the `OverlaidTracks` component.
433
+
434
+ Args:
435
+ tdata: A dictionary mapping track names to `TrackData` objects.
436
+ colors: An optional dictionary mapping track names to colors.
437
+ cmap: The colormap to use if `colors` is not provided.
438
+ track_height: The height of each track.
439
+ ylabel_template: A template for the y-axis labels.
440
+ ylabel_horizontal: Whether to make the y-axis labels horizontal.
441
+ shared_y_scale: Whether to use the same y-axis scale for all tracks. This
442
+ is inferred from the min/max data values across all tracks.
443
+ global_ylims: Optional global y-axis limits (min and max).
444
+ yticks: Optional set y-axis tick values manually. If not provided, the
445
+ tick values will be automatically determined. If provided, the length of
446
+ yticks must match the length of yticklabels.
447
+ yticklabels: Optional set y-axis tick labels manually. If not provided,
448
+ the tick values will be automatically determined. If provided, the
449
+ length of yticklabels must match the length of yticks.
450
+ alpha: The transparency of the tracks.
451
+ order_tdata_by_mean: Whether to order the tracks by their mean value (in
452
+ descending order).
453
+ max_num_tracks: The maximum number of tracks to plot.
454
+ legend_loc: The location of the legend (such as 'upper left' or 'best').
455
+ See the matplotlib Axes legend documentation for more details and
456
+ options. If None, no legend is shown.
457
+ **kwargs: Additional keyword arguments to pass to the plotting function.
458
+
459
+ Raises:
460
+ ValueError: If the shapes or metadata of the track data do not match,
461
+ or if the number of tracks exceeds `max_num_tracks`, or if
462
+ the track data has more than one positional axis, or if the
463
+ interval is not set, or if colors are passed but do not match
464
+ the track data names.
465
+ """
466
+ self._tdata = tdata
467
+ self._colors = colors
468
+ self._cmap = cmap
469
+ self._track_height = track_height
470
+ self._ylabel_template = ylabel_template
471
+ self._ylabel_horizontal = ylabel_horizontal
472
+ self._shared_y_scale = shared_y_scale
473
+ self._global_ylims = global_ylims
474
+ self._yticks = yticks
475
+ self._yticklabels = yticklabels
476
+ self._alpha = alpha
477
+ self._order_tdata_by_mean = order_tdata_by_mean
478
+ self._kwargs = kwargs
479
+ self._first_tdata = list(tdata.values())[0]
480
+ self._legend_loc = legend_loc
481
+
482
+ if (
483
+ self._yticks is not None
484
+ and self._yticklabels is not None
485
+ and len(self._yticks) != len(self._yticklabels)
486
+ ):
487
+ raise ValueError(
488
+ 'If passing yticks and yticklabels, the length of yticks must match'
489
+ ' the length of yticklabels.'
490
+ )
491
+
492
+ if len(set(data.values.shape for data in tdata.values())) != 1:
493
+ raise ValueError('Shapes of track data values must be the same.')
494
+
495
+ if not all(
496
+ self._first_tdata.metadata.equals(data.metadata)
497
+ for data in tdata.values()
498
+ ):
499
+ raise ValueError('Metadata of track data must be the same.')
500
+
501
+ if self._first_tdata.num_tracks > max_num_tracks:
502
+ raise ValueError(
503
+ f'Too many tracks to plot: {self._first_tdata.num_tracks} >'
504
+ f' {max_num_tracks}.'
505
+ )
506
+
507
+ # If a colors dict is passed, then there should be a matching color for each
508
+ # track data name.
509
+ if self._colors is not None:
510
+ if self._tdata.keys() != self._colors.keys():
511
+ raise ValueError(
512
+ f'If passing colors, each tdata name {list(self._tdata.keys())} '
513
+ 'must have an associated color.'
514
+ )
515
+ # Otherwise, we define a color from a cmap.
516
+ else:
517
+ cmap = plt.get_cmap(self._cmap)
518
+ colors = cmap(np.linspace(0, 1, len(tdata)))
519
+ self._colors = dict(zip(self._tdata.keys(), colors))
520
+
521
+ if len(self._first_tdata.positional_axes) != 1:
522
+ raise ValueError(
523
+ 'Only track_data with 1 positional axis is supported in'
524
+ ' OverlaidTracks.'
525
+ )
526
+ if self._first_tdata.interval is None:
527
+ raise ValueError('.interval needs to be set in track_data.')
528
+
529
+ if self._shared_y_scale:
530
+ # We compute min and max over all the track data arrays in the tdata dict.
531
+ all_values = np.stack([arr.values for arr in self._tdata.values()])
532
+ self._vmin = np.min(all_values)
533
+ self._vmax = np.max(all_values)
534
+
535
+ def get_ax_height(self, axis_index: int) -> float:
536
+ """Returns the height of the axis."""
537
+ return self._track_height
538
+
539
+ @property
540
+ def num_axes(self) -> int:
541
+ """Returns the number of matplotlib axes required by the component."""
542
+ return self._first_tdata.num_tracks
543
+
544
+ def plot_ax(
545
+ self,
546
+ ax: matplotlib.axes.Axes,
547
+ axis_index: int,
548
+ interval: genome.Interval,
549
+ ):
550
+ """Plots the overlaid tracks on the given axis.
551
+
552
+ Args:
553
+ ax: The matplotlib axis to plot on.
554
+ axis_index: The index of the axis.
555
+ interval: The genomic interval to plot.
556
+ """
557
+
558
+ def _maybe_slice_tdata(td):
559
+ """Slices the track data to the interval if passed."""
560
+ # If an interval is passed, zoom in on that specific sub-interval.
561
+ if interval is not None:
562
+ return td.slice_by_interval(interval, match_resolution=True)
563
+ else:
564
+ return td
565
+
566
+ def _make_ordered_dict_by_mean(tdata):
567
+ """Reorders track data dict by mean (descending) for better plotting."""
568
+ mean_tuples = [
569
+ (name, np.mean(td.values, dtype=np.float64))
570
+ for name, td in tdata.items()
571
+ ]
572
+ sorted_mean_tuples = sorted(
573
+ mean_tuples, key=lambda item: item[1], reverse=True
574
+ )
575
+
576
+ # Extract names in the sorted order and return ordered tdata.
577
+ sorted_names = [name for name, _ in sorted_mean_tuples]
578
+ ordered_tdata = {name: tdata[name] for name in sorted_names}
579
+ return ordered_tdata
580
+
581
+ # Maybe slice the track data to the interval, and reorder by mean.
582
+ tdata_sliced = {
583
+ name: _maybe_slice_tdata(td) for name, td in self._tdata.items()
584
+ }
585
+ self._tdata_ordered = (
586
+ _make_ordered_dict_by_mean(tdata_sliced)
587
+ if self._order_tdata_by_mean
588
+ else tdata_sliced
589
+ )
590
+
591
+ for name, tdata in self._tdata_ordered.items():
592
+ assert tdata.interval is not None
593
+ x = (
594
+ np.arange(tdata.values.shape[0]) * tdata.resolution
595
+ + tdata.interval.start
596
+ + tdata.resolution / 2
597
+ )
598
+ arr = tdata.values[:, axis_index]
599
+
600
+ if self._global_ylims is not None:
601
+ ax.set_ylim(self._global_ylims)
602
+ elif self._shared_y_scale:
603
+ ax.set_ylim(self._vmin, self._vmax)
604
+
605
+ # Plot the two tracks on the same axis. We plot the larger values first so
606
+ # that the overlap is more visible.
607
+ ax.plot(
608
+ x,
609
+ arr,
610
+ alpha=self._alpha,
611
+ **self._kwargs,
612
+ c=self._colors[name],
613
+ )
614
+ if axis_index == 0 and self._legend_loc is not None:
615
+ ax.legend(self._tdata_ordered.keys(), loc=self._legend_loc)
616
+
617
+ if self._yticks is not None:
618
+ ax.set_yticks(self._yticks)
619
+ if self._yticklabels is not None:
620
+ ax.set_yticklabels(self._yticklabels)
621
+
622
+ if self._ylabel_template:
623
+ _set_ylabel(ax, self._get_ylabel(axis_index), self._ylabel_horizontal)
624
+
625
+ def _get_ylabel(self, axis_index: int) -> str:
626
+ """Returns the y-axis label for the given track."""
627
+ # Metadata equality has already been checked so here we grab the first one.
628
+ metadata = self._tdata[list(self._tdata.keys())[0]].metadata
629
+ row = metadata.iloc[axis_index]
630
+ return self._ylabel_template.format(**row.to_dict())
631
+
632
+
633
+ class ContactMaps(AbstractComponent):
634
+ """Component for visualizing contact maps.
635
+
636
+ The `vmin` and `vmax` parameters control the color scaling of the heatmap.
637
+ Values outside this range will be clipped to `vmin` or `vmax`.
638
+ """
639
+
640
+ def __init__(
641
+ self,
642
+ tdata: track_data.TrackData,
643
+ track_height: float = 10.0,
644
+ vmin: float | None = -1.0,
645
+ vmax: float | None = 2.0,
646
+ norm: matplotlib.colors.TwoSlopeNorm | None = None,
647
+ ylabel_horizontal: bool = True,
648
+ ylabel_template: str = '{name}',
649
+ cmap: matplotlib.colors.LinearSegmentedColormap | None = None,
650
+ max_num_tracks: int = 10,
651
+ **kwargs,
652
+ ):
653
+ """Initializes the `ContactMaps` component.
654
+
655
+ Args:
656
+ tdata: The `TrackData` object containing the contact maps.
657
+ track_height: The height of each contact map.
658
+ vmin: The minimum value for the color scale.
659
+ vmax: The maximum value for the color scale.
660
+ norm: An optional normalization for the color scale.
661
+ ylabel_horizontal: Whether to make the y-axis labels horizontal.
662
+ ylabel_template: A template for the y-axis labels.
663
+ cmap: The colormap to use for the contact maps.
664
+ max_num_tracks: The maximum number of tracks to plot.
665
+ **kwargs: Additional keyword arguments to pass to the plotting function.
666
+
667
+ Raises:
668
+ ValueError: If the number of tracks exceeds `max_num_tracks`, or if the
669
+ track data does not have 2 positional axes, or if the contact maps are
670
+ not square, or if the interval is not set in the track data.
671
+ """
672
+ if tdata.num_tracks > max_num_tracks:
673
+ raise ValueError(
674
+ f'Too many tracks to plot: {tdata.num_tracks} > {max_num_tracks}.'
675
+ )
676
+ self._tdata = tdata
677
+ self._resolution = tdata.resolution
678
+ self._track_height = track_height
679
+ self._vmin = vmin
680
+ self._vmax = vmax
681
+ self._norm = norm
682
+ self._ylabel_horizontal = ylabel_horizontal
683
+ self._ylabel_template = ylabel_template
684
+ # TODO: b/377292012 - Add orca_color_map.
685
+ self._cmap = (
686
+ cmap if cmap is not None else matplotlib.pyplot.get_cmap('autumn_r')
687
+ )
688
+ self._kwargs = kwargs
689
+ if len(self._tdata.positional_axes) != 2:
690
+ raise ValueError(
691
+ 'Only track_data with 2 positional axes is supported in ContactMaps.'
692
+ )
693
+ if self._tdata.values.shape[0] != self._tdata.values.shape[1]:
694
+ raise ValueError('Contact maps must be square.')
695
+
696
+ if self._tdata.interval is None:
697
+ raise ValueError('.interval needs to be set in track_data.')
698
+
699
+ def get_ax_height(self, axis_index: int) -> float:
700
+ """Returns the height of the axis."""
701
+ return self._track_height
702
+
703
+ @property
704
+ def num_axes(self) -> int:
705
+ """Returns the number of matplotlib axes required by the component."""
706
+ return self._tdata.num_tracks
707
+
708
+ def _get_bin_positions(
709
+ self, interval: genome.Interval, resolution: int
710
+ ) -> np.ndarray:
711
+ """Gets the positions of contact map bins in chromosome coordinates."""
712
+ bin_indices = np.arange(interval.width // resolution)
713
+ return interval.start + (resolution * bin_indices)
714
+
715
+ def _plot_pcolormesh(
716
+ self,
717
+ ax: matplotlib.axes.Axes,
718
+ x: np.ndarray,
719
+ y: np.ndarray,
720
+ arr: np.ndarray,
721
+ vmin: float | None = None,
722
+ vmax: float | None = None,
723
+ cmap: matplotlib.colors.Colormap | None = None,
724
+ norm: matplotlib.colors.Normalize | None = None,
725
+ **kwargs,
726
+ ) -> matplotlib.collections.QuadMesh:
727
+ """Plots the contact map heatmap using `pcolormesh`.
728
+
729
+ Note that upsampling the contact maps to single base pair resolution and
730
+ using something like .imshow() is infeasible since the upsampling blows up
731
+ memory.
732
+
733
+ Args:
734
+ ax: The matplotlib axis to plot on.
735
+ x: The x-axis coordinates.
736
+ y: The y-axis coordinates.
737
+ arr: The contact map data.
738
+ vmin: The minimum value for the color scale.
739
+ vmax: The maximum value for the color scale.
740
+ cmap: The colormap to use.
741
+ norm: An optional normalization for the color scale.
742
+ **kwargs: Additional keyword arguments to pass to `pcolormesh`.
743
+
744
+ Returns:
745
+ The `matplotlib.collections.QuadMesh` object representing the plot.
746
+ """
747
+ if not norm: # Cannot pass both norm and vmin/vmax simultaneously.
748
+ return ax.pcolormesh(
749
+ x,
750
+ y,
751
+ arr,
752
+ vmin=vmin,
753
+ vmax=vmax,
754
+ cmap=cmap,
755
+ **kwargs,
756
+ )
757
+ else:
758
+ return ax.pcolormesh(
759
+ x,
760
+ y,
761
+ arr,
762
+ norm=norm,
763
+ **kwargs,
764
+ )
765
+
766
+ def plot_ax(
767
+ self,
768
+ ax: matplotlib.axes.Axes,
769
+ axis_index: int,
770
+ interval: genome.Interval,
771
+ ):
772
+ """Plots the contact map on the given axis.
773
+
774
+ Args:
775
+ ax: The matplotlib axis to plot on.
776
+ axis_index: The index of the axis.
777
+ interval: The genomic interval to plot.
778
+ """
779
+ tdata = self._tdata
780
+ assert tdata.interval is not None
781
+
782
+ # If an interval is passed, zoom in on that specific sub-interval.
783
+ if interval is not None:
784
+ tdata = tdata.slice_by_interval(interval, match_resolution=True)
785
+ del interval
786
+
787
+ arr = tdata.values[:, :, axis_index]
788
+ x = self._get_bin_positions(tdata.interval, self._resolution)
789
+
790
+ # We shift the bin edges by half a step since pcolormesh will misalign
791
+ # the x-axis by half a step, since the plot values are centered within each
792
+ # bin, rather than at the bin edges.
793
+ half_bin_width = self._resolution // 2
794
+ x = x + half_bin_width
795
+
796
+ # The -1 reverse ordering ensures that contact maps are plotted with
797
+ # the diagonal going down from left to right.
798
+ y = np.arange(arr.shape[0])[::-1]
799
+
800
+ if not self._vmin:
801
+ self._vmin = np.min(arr)
802
+
803
+ if not self._vmax:
804
+ self._vmax = np.max(arr)
805
+
806
+ self._plot_pcolormesh(
807
+ ax=ax,
808
+ x=x,
809
+ y=y,
810
+ arr=arr,
811
+ vmin=self._vmin,
812
+ vmax=self._vmax,
813
+ cmap=self._cmap,
814
+ norm=self._norm,
815
+ **self._kwargs,
816
+ )
817
+
818
+ if self._ylabel_template:
819
+ _set_ylabel(ax, self._get_ylabel(axis_index), self._ylabel_horizontal)
820
+
821
+ def _get_ylabel(self, axis_index: int) -> str:
822
+ """Returns the y-axis label for the given contact map."""
823
+ row = self._tdata.metadata.iloc[axis_index]
824
+ return self._ylabel_template.format(**row.to_dict())
825
+
826
+
827
+ class ContactMapsDiff(ContactMaps):
828
+ """Component for visualizing contact map differences.
829
+
830
+ This component visualizes the difference between two contact maps. It uses
831
+ a diverging red-blue color map with the center white color pinned to a
832
+ value of zero, with negative values being blue and positive values being red.
833
+
834
+ The `vmin` and `vmax` parameters control the color scaling of the heatmap.
835
+ Values outside this range will be clipped to `vmin` or `vmax`.
836
+ """
837
+
838
+ def __init__(
839
+ self,
840
+ tdata: track_data.TrackData,
841
+ track_height: float = 10.0,
842
+ vmin: float | None = -1.0,
843
+ vmax: float | None = 1.0,
844
+ ylabel_horizontal: bool = True,
845
+ ylabel_template: str = '{name}',
846
+ cmap: matplotlib.colors.LinearSegmentedColormap | str | None = 'RdBu_r',
847
+ max_num_tracks: int = 10,
848
+ **kwargs,
849
+ ):
850
+ """Initializes the `ContactMapsDiff` component.
851
+
852
+ Args:
853
+ tdata: The `TrackData` object containing the contact map differences.
854
+ track_height: The height of each contact map.
855
+ vmin: The minimum value for the color scale.
856
+ vmax: The maximum value for the color scale.
857
+ ylabel_horizontal: Whether to make the y-axis labels horizontal.
858
+ ylabel_template: A template for the y-axis labels.
859
+ cmap: The colormap to use for the contact maps.
860
+ max_num_tracks: The maximum number of tracks to plot.
861
+ **kwargs: Additional keyword arguments to pass to the plotting function.
862
+ """
863
+ self._norm = plt_colors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
864
+
865
+ super().__init__(
866
+ tdata=tdata,
867
+ track_height=track_height,
868
+ vmin=vmin,
869
+ vmax=vmax,
870
+ ylabel_horizontal=ylabel_horizontal,
871
+ ylabel_template=ylabel_template,
872
+ cmap=cmap,
873
+ max_num_tracks=max_num_tracks,
874
+ **kwargs,
875
+ )
876
+
877
+
878
+ def _set_ylabel(ax: matplotlib.axes.Axes, ylabel: str, horizontal: bool):
879
+ """Sets the y-axis label.
880
+
881
+ Args:
882
+ ax: The matplotlib axis to set the label on.
883
+ ylabel: The label text.
884
+ horizontal: Whether to make the label horizontal.
885
+ """
886
+ if ylabel:
887
+ if horizontal:
888
+ ax.set_ylabel(
889
+ ylabel,
890
+ rotation=0,
891
+ multialignment='center',
892
+ va='center',
893
+ ha='right',
894
+ labelpad=5,
895
+ )
896
+ else:
897
+ ax.set_ylabel(ylabel)
898
+
899
+
900
+ class TranscriptAnnotation(AbstractComponent):
901
+ """Visualizes transcript annotations."""
902
+
903
+ def __init__(
904
+ self,
905
+ transcripts: Sequence[transcript_utils.Transcript],
906
+ adaptive_fig_height: bool = True,
907
+ fig_height: float = 1.0,
908
+ transcript_style: plot_transcripts.TranscriptStyle = (
909
+ plot_transcripts.TranscriptStylePreset.MINIMAL.value
910
+ ),
911
+ plot_labels_once: bool = True,
912
+ label_name: str = 'gene_name',
913
+ **kwargs,
914
+ ):
915
+ """Initializes the `TranscriptAnnotation` component.
916
+
917
+ Args:
918
+ transcripts: A sequence of `Transcript` objects to visualize.
919
+ adaptive_fig_height: Whether to adjust the figure height based on the
920
+ number of transcripts.
921
+ fig_height: The base figure height.
922
+ transcript_style: The style to use for plotting transcripts. The options
923
+ are defined in `plot_transcripts.TranscriptStylePreset`.
924
+ plot_labels_once: Whether to plot labels only once per transcript.
925
+ label_name: The attribute of the transcript to use for labels.
926
+ **kwargs: Additional keyword arguments to pass to the plotting function.
927
+ """
928
+ self._transcripts = transcripts
929
+ self._adaptive_fig_height = adaptive_fig_height
930
+ self._fig_height = fig_height
931
+ self._kwargs = kwargs
932
+ self._kwargs['label_name'] = label_name
933
+ self._kwargs['transcript_style'] = transcript_style
934
+ self._kwargs['plot_labels_once'] = plot_labels_once
935
+
936
+ self._num_transcripts = len(self._transcripts)
937
+ # TODO(b/377291518): adaptive fig height should in theory scale with the
938
+ # number of transcripts in the sub-interval, not the total number of
939
+ # transcripts passed, but this is a bit tricky to implement in the code and
940
+ # this approach seems to work well enough for now.
941
+ if self._adaptive_fig_height:
942
+ self._fig_height = max(0.05 * self._num_transcripts * self._fig_height, 1)
943
+
944
+ def get_ax_height(self, axis_index: int) -> float:
945
+ """Returns the height of the axis."""
946
+ return self._fig_height
947
+
948
+ @property
949
+ def num_axes(self) -> int:
950
+ """Returns the number of matplotlib axes required by the component."""
951
+ return 1
952
+
953
+ def plot_ax(
954
+ self, ax: matplotlib.axes.Axes, axis_index: int, interval: genome.Interval
955
+ ):
956
+ """Plots the transcript annotations on the given axis.
957
+
958
+ Args:
959
+ ax: The matplotlib axis to plot on.
960
+ axis_index: The index of the axis.
961
+ interval: The genomic interval to plot.
962
+ """
963
+ # Update transcripts to only those overlapping interval.
964
+ transcripts = [
965
+ t for t in self._transcripts if t.transcript_interval.overlaps(interval)
966
+ ]
967
+ ax.set_yticklabels([])
968
+ ax.set_yticks([])
969
+ ax.spines['left'].set_visible(False)
970
+ plot_transcripts.plot_transcripts(ax, transcripts, interval, **self._kwargs)
971
+
972
+
973
+ class SeqLogo(AbstractComponent):
974
+ """Visualizes a sequence logo."""
975
+
976
+ def __init__(
977
+ self,
978
+ scores: Float32[np.ndarray, 'S A'],
979
+ scores_interval: genome.Interval,
980
+ fig_height: float = 1.0,
981
+ alphabet: str = 'ACGT',
982
+ max_width: int = 1000,
983
+ ylabel: str = '',
984
+ ylabel_horizontal: bool = True,
985
+ ylim: tuple[float, float] | None = None,
986
+ **kwargs,
987
+ ):
988
+ """Initializes the `SeqLogo` component.
989
+
990
+ Args:
991
+ scores: A numpy array of shape (sequence_length, alphabet_size) containing
992
+ the sequence logo scores.
993
+ scores_interval: The genomic interval corresponding to the scores.
994
+ fig_height: The height of the figure.
995
+ alphabet: The alphabet used in the sequence logo.
996
+ max_width: The maximum width of the sequence logo to plot.
997
+ ylabel: An optional label for the y-axis.
998
+ ylabel_horizontal: Whether to make the y-axis label horizontal.
999
+ ylim: An optional range to set for the y-axis.
1000
+ **kwargs: Additional keyword arguments to pass to the plotting function.
1001
+ """
1002
+ self._scores = scores
1003
+ self._scores_interval = scores_interval
1004
+ self._fig_height = fig_height
1005
+ self._alphabet = alphabet
1006
+ self._max_width = max_width
1007
+ self._ylabel = ylabel
1008
+ self._ylabel_horizontal = ylabel_horizontal
1009
+ self._ylim = ylim
1010
+ self._kwargs = kwargs
1011
+
1012
+ def get_ax_height(self, axis_index: int) -> float:
1013
+ """Returns the height of the axis."""
1014
+ return self._fig_height
1015
+
1016
+ @property
1017
+ def num_axes(self) -> int:
1018
+ """Returns the number of matplotlib axes required by the component."""
1019
+ return 1
1020
+
1021
+ def plot_ax(
1022
+ self, ax: matplotlib.axes.Axes, axis_index: int, interval: genome.Interval
1023
+ ):
1024
+ """Plots the sequence logo on the given axis.
1025
+
1026
+ Args:
1027
+ ax: The matplotlib axis to plot on.
1028
+ axis_index: The index of the axis.
1029
+ interval: The genomic interval to plot.
1030
+ """
1031
+ intersection = self._scores_interval.intersect(interval)
1032
+ if intersection is None or intersection.width > self._max_width:
1033
+ return
1034
+ relative_start = intersection.start - self._scores_interval.start
1035
+ scores = self._scores[
1036
+ relative_start : (relative_start + intersection.width)
1037
+ ]
1038
+
1039
+ plot_lib.seqlogo(
1040
+ scores,
1041
+ ax=ax,
1042
+ alphabet=self._alphabet,
1043
+ start=intersection.start,
1044
+ one_based=False,
1045
+ **self._kwargs,
1046
+ )
1047
+
1048
+ _set_ylabel(ax, self._ylabel, self._ylabel_horizontal)
1049
+ if self._ylim is not None:
1050
+ ax.set_ylim(self._ylim)
1051
+
1052
+
1053
+ class Sashimi(AbstractComponent):
1054
+ """Visualizes splice junctions as a Sashimi plot."""
1055
+
1056
+ def __init__(
1057
+ self,
1058
+ junction_track: junction_data.JunctionData,
1059
+ fig_height: float = 1.0,
1060
+ filter_threshold: float | None = None,
1061
+ ylabel_template: str = '{name}',
1062
+ ylabel_horizontal: bool = True,
1063
+ annotate_counts: bool = True,
1064
+ normalize_values: bool = True,
1065
+ interval_contained: bool = True,
1066
+ rng: np.random.Generator | None = None,
1067
+ ):
1068
+ """Initializes the `Sashimi` component.
1069
+
1070
+ Args:
1071
+ junction_track: A `JunctionData` object to visualize.
1072
+ fig_height: The height of the figure.
1073
+ filter_threshold: The minimum value for a junction to be included in the
1074
+ plot. This is typically based on the normalized read count. If None,
1075
+ filter out junction values below 5% of the maximum value.
1076
+ ylabel_template: A template for the y-axis labels.
1077
+ ylabel_horizontal: Whether to make the y-axis label horizontal.
1078
+ annotate_counts: Whether to annotate the junctions with read counts.
1079
+ normalize_values: Whether to normalize the values to a constant sum.
1080
+ interval_contained: Whether to only plot junctions contained in the
1081
+ interval.
1082
+ rng: Optional random number generator to use for jittering junction paths.
1083
+ If unset will use NumPy's default random number generator.
1084
+ """
1085
+ if normalize_values:
1086
+ self._junction_track = junction_track.normalize_values()
1087
+ else:
1088
+ self._junction_track = junction_track
1089
+ self._fig_height = fig_height
1090
+ self._filter_threshold = filter_threshold
1091
+ self._ylabel_template = ylabel_template
1092
+ self._ylabel_horizontal = ylabel_horizontal
1093
+ self._annotate_counts = annotate_counts
1094
+ self._interval_contained = interval_contained
1095
+ self._rng = rng or np.random.default_rng()
1096
+
1097
+ def get_ax_height(self, axis_index: int) -> float:
1098
+ """Returns the height of the axis."""
1099
+ return self._fig_height
1100
+
1101
+ @property
1102
+ def num_axes(self) -> int:
1103
+ """Returns the number of matplotlib axes required by the component."""
1104
+ # Metadata for JunctionData do not have strand information.
1105
+ # Strand information is in JunctionData.intervals.
1106
+ return self._junction_track.num_tracks * len(
1107
+ self._junction_track.possible_strands
1108
+ )
1109
+
1110
+ def _get_strand_and_metadata_index(self, axis_index: int) -> tuple[str, int]:
1111
+ """Returns the strand and metadata index for the given axis index."""
1112
+ if len(self._junction_track.possible_strands) == 1:
1113
+ strand = self._junction_track.possible_strands[0]
1114
+ metadata_index = axis_index
1115
+ else:
1116
+ strand = '+' if axis_index % 2 == 0 else '-'
1117
+ metadata_index = axis_index // 2
1118
+ return strand, metadata_index
1119
+
1120
+ def plot_ax(
1121
+ self, ax: matplotlib.axes.Axes, axis_index: int, interval: genome.Interval
1122
+ ):
1123
+ """Plots the Sashimi plot on the given axis.
1124
+
1125
+ Args:
1126
+ ax: The matplotlib axis to plot on.
1127
+ axis_index: The index of the axis.
1128
+ interval: The genomic interval to plot.
1129
+ """
1130
+ strand, metadata_index = self._get_strand_and_metadata_index(axis_index)
1131
+ track_name = self._junction_track.metadata.iloc[metadata_index]['name']
1132
+ junction_track = self._junction_track.intersect_with_interval(interval)
1133
+ junctions = junction_data.get_junctions_to_plot(
1134
+ predictions=junction_track,
1135
+ strand=strand,
1136
+ name=track_name,
1137
+ k_threshold=self._filter_threshold,
1138
+ )
1139
+ if self._interval_contained:
1140
+ junctions = [j for j in junctions if interval.contains(j)]
1141
+ else:
1142
+ junctions = [j for j in junctions if j.overlaps(interval)]
1143
+
1144
+ plot_lib.sashimi_plot(
1145
+ junctions,
1146
+ ax=ax,
1147
+ interval=interval,
1148
+ filter_threshold=0,
1149
+ annotate_counts=self._annotate_counts,
1150
+ rng=self._rng,
1151
+ )
1152
+ ax.set_yticklabels([])
1153
+ ax.set_yticks([])
1154
+ ax.spines['left'].set_visible(False)
1155
+ if self._ylabel_template:
1156
+ _set_ylabel(ax, self._get_ylabel(axis_index), self._ylabel_horizontal)
1157
+
1158
+ def _get_ylabel(self, axis_index: int) -> str:
1159
+ """Returns the y-axis label for the given axis index."""
1160
+ strand, metadata_index = self._get_strand_and_metadata_index(axis_index)
1161
+ row = self._junction_track.metadata.iloc[metadata_index]
1162
+ row = row.to_dict()
1163
+ row['strand'] = strand
1164
+ return self._ylabel_template.format(**row)
1165
+
1166
+
1167
+ class EmptyComponent(AbstractComponent):
1168
+ """An empty plotting component."""
1169
+
1170
+ def __init__(self, fig_height: float = 1.0):
1171
+ """Initializes the `EmptyComponent`.
1172
+
1173
+ Args:
1174
+ fig_height: The height of the figure.
1175
+ """
1176
+ self._fig_height = fig_height
1177
+
1178
+ def get_ax_height(self, axis_index: int) -> float:
1179
+ """Returns the height of the axis."""
1180
+ return self._fig_height
1181
+
1182
+ @property
1183
+ def num_axes(self) -> int:
1184
+ """Returns the number of matplotlib axes required by the component."""
1185
+ return 1
1186
+
1187
+ def plot_ax(
1188
+ self, ax: matplotlib.axes.Axes, axis_index: int, interval: genome.Interval
1189
+ ):
1190
+ """Plot an empty axis, removing all labels and spines from the axis.
1191
+
1192
+ Args:
1193
+ ax: The matplotlib axis to plot on.
1194
+ axis_index: The index of the axis.
1195
+ interval: The genomic interval to plot.
1196
+ """
1197
+ ax.set_yticklabels([])
1198
+ ax.set_yticks([])
1199
+ ax.spines['left'].set_visible(False)
1200
+ ax.spines['right'].set_visible(False)
1201
+
1202
+
1203
+ class AbstractAnnotation(abc.ABC):
1204
+ """Abstract base class for plot annotations.
1205
+
1206
+ Annotations are visual elements that can be added to plots to highlight
1207
+ specific features or regions. This class defines the common interface
1208
+ for all annotations.
1209
+
1210
+ Attributes:
1211
+ annotations: A sequence of `Variant` or `Interval` objects representing the
1212
+ annotations.
1213
+ colors: An optional string or sequence of strings specifying the colors of
1214
+ the annotations.
1215
+ labels: An optional sequence of strings to use as labels for the
1216
+ annotations.
1217
+ use_default_labels: Whether to use default labels for the annotations if
1218
+ `labels` is not provided.
1219
+ """
1220
+
1221
+ def __init__(
1222
+ self,
1223
+ annotations: Sequence[genome.Variant] | Sequence[genome.Interval],
1224
+ colors: str | Sequence[str] | None,
1225
+ labels: Sequence[str] | None,
1226
+ use_default_labels: bool,
1227
+ ):
1228
+ """Initializes the `AbstractAnnotation` class.
1229
+
1230
+ Args:
1231
+ annotations: A sequence of `Variant` or `Interval` objects.
1232
+ colors: An optional string or sequence of strings specifying colors.
1233
+ labels: An optional sequence of strings to use as labels.
1234
+ use_default_labels: Whether to use default labels if `labels` is not
1235
+ provided.
1236
+
1237
+ Raises:
1238
+ ValueError: If the length of `colors` or `labels` does not match the
1239
+ length of `annotations`.
1240
+ """
1241
+ self._annotations = annotations
1242
+ self._colors = colors
1243
+ self._labels = labels
1244
+ self._use_default_labels = use_default_labels
1245
+
1246
+ # Pre-processing / validation of inputs.
1247
+ num_annotations = len(self._annotations)
1248
+ if (self._colors is not None) and (not isinstance(self._colors, str)):
1249
+ if len(self._colors) != num_annotations:
1250
+ raise ValueError(
1251
+ 'Colors must have the same length as intervals/variants or just'
1252
+ ' a single color string.'
1253
+ )
1254
+ if self._labels is not None:
1255
+ if len(self._labels) != num_annotations:
1256
+ raise ValueError(
1257
+ 'Labels must have the same length as intervals/variants.'
1258
+ )
1259
+
1260
+ @abc.abstractmethod
1261
+ def plot_ax(
1262
+ self, ax: matplotlib.axes.Axes, interval: genome.Interval, hspace: float
1263
+ ):
1264
+ """Adds the annotation to an individual axis.
1265
+
1266
+ Args:
1267
+ ax: The matplotlib axis to add the annotation to.
1268
+ interval: The genomic interval to plot.
1269
+ hspace: The vertical space between subplots.
1270
+ """
1271
+ raise NotImplementedError
1272
+
1273
+ @abc.abstractmethod
1274
+ def plot_labels(
1275
+ self,
1276
+ ax: matplotlib.axes.Axes,
1277
+ interval: genome.Interval,
1278
+ label_height_factor: float,
1279
+ ):
1280
+ """Adds labels for the annotation to an axis.
1281
+
1282
+ Args:
1283
+ ax: The matplotlib axis to add the labels to.
1284
+ interval: The genomic interval to plot.
1285
+ label_height_factor: A scaling factor for the label height.
1286
+ """
1287
+ raise NotImplementedError
1288
+
1289
+ @property
1290
+ def is_variant(self) -> bool:
1291
+ """Returns True if the annotation is a variant annotation."""
1292
+ return isinstance(self._annotations[0], genome.Variant)
1293
+
1294
+ @property
1295
+ def has_labels(self) -> bool:
1296
+ """Returns True if the annotation has labels."""
1297
+ return (self._labels is not None) | (
1298
+ self.is_variant and self._use_default_labels
1299
+ )
1300
+
1301
+ def add_label(
1302
+ self,
1303
+ ax: matplotlib.axes.Axes,
1304
+ label_x_position: float,
1305
+ label: str,
1306
+ angle: float,
1307
+ label_height_factor: float,
1308
+ label_position: str = 'left',
1309
+ ):
1310
+ """Adds a single angled label to an axis.
1311
+
1312
+ Args:
1313
+ ax: The matplotlib axis to add the label to.
1314
+ label_x_position: The x position of the label.
1315
+ label: The label text.
1316
+ angle: The angle of the label.
1317
+ label_height_factor: A scaling factor for the label height.
1318
+ label_position: The (horizontal) placement of the label, relative to the x
1319
+ position. Can be any position string accepted by the horizontalalignment
1320
+ argument of matplotlib.axes.Axes.text.
1321
+ """
1322
+ ylims = ax.get_ylim()
1323
+ label_height = ylims[0] + label_height_factor * np.diff(ylims)[0]
1324
+ ax.text(
1325
+ label_x_position,
1326
+ label_height,
1327
+ label,
1328
+ color='black',
1329
+ fontsize=10,
1330
+ rotation=angle,
1331
+ ha=label_position,
1332
+ va='bottom',
1333
+ )
1334
+ ax.axvline(
1335
+ label_x_position, ymax=label_height * 0.95, color='black', alpha=0.1
1336
+ )
1337
+
1338
+
1339
+ class IntervalAnnotation(AbstractAnnotation):
1340
+ """Visualizes intervals as rectangles across all plot components.
1341
+
1342
+ A rectangle is drawn for each interval and overlaid on top of the final plot,
1343
+ spanning all plot components.
1344
+ """
1345
+
1346
+ def __init__(
1347
+ self,
1348
+ intervals: Sequence[genome.Interval],
1349
+ colors: str | Sequence[str] = 'darkgray',
1350
+ alpha: float = 0.2,
1351
+ labels: Sequence[str] | None = None,
1352
+ use_default_labels: bool = True,
1353
+ label_angle: float = 15,
1354
+ ):
1355
+ """Initializes the `IntervalAnnotation` class.
1356
+
1357
+ Args:
1358
+ intervals: A sequence of `Interval` objects to annotate.
1359
+ colors: An optional string or sequence of strings specifying the colors of
1360
+ the interval annotation.
1361
+ alpha: The transparency of the interval annotation.
1362
+ labels: An optional sequence of strings to use as labels for the
1363
+ intervals.
1364
+ use_default_labels: Whether to use default labels for the intervals if
1365
+ `labels` is not provided.
1366
+ label_angle: The angle of the interval labels.
1367
+ """
1368
+ super().__init__(intervals, colors, labels, use_default_labels)
1369
+ self._label_angle = label_angle
1370
+ self._alpha = alpha
1371
+ self._intervals = intervals
1372
+
1373
+ def plot_ax(
1374
+ self,
1375
+ ax: matplotlib.axes.Axes,
1376
+ interval: genome.Interval,
1377
+ hspace: float = 0.0,
1378
+ ):
1379
+ """Adds the interval annotation to an individual axis.
1380
+
1381
+ Args:
1382
+ ax: The matplotlib axis to add the annotation to.
1383
+ interval: The genomic interval to plot.
1384
+ hspace: The vertical space between subplots.
1385
+ """
1386
+ for i, interval_i in enumerate(self._intervals):
1387
+ if isinstance(self._colors, str):
1388
+ color = self._colors
1389
+ else:
1390
+ color = self._colors[i]
1391
+ # Only plot the piece of the annotation that intersects with the plotting
1392
+ # interval.
1393
+ intersection = interval_i.intersect(interval)
1394
+ if intersection is None:
1395
+ continue
1396
+ ax.axvspan(
1397
+ interval_i.start,
1398
+ interval_i.end,
1399
+ # Set y-maximum to be the top of the axis, including hspace.
1400
+ ymax=(1 + hspace) * 1.03,
1401
+ alpha=self._alpha,
1402
+ facecolor=color,
1403
+ # Set edgecolor to None for intervals to avoid horizongal lines
1404
+ # between axes, but keep it for variants. As variants are typically
1405
+ # a very thin rectangle, removing the edges results in the rectangle
1406
+ # not being visible.
1407
+ edgecolor=None,
1408
+ clip_on=False,
1409
+ )
1410
+
1411
+ def plot_labels(
1412
+ self,
1413
+ ax: matplotlib.axes.Axes,
1414
+ interval: genome.Interval,
1415
+ label_height_factor: float,
1416
+ ):
1417
+ """Adds interval labels to an axis.
1418
+
1419
+ Args:
1420
+ ax: The matplotlib axis to add the labels to.
1421
+ interval: The genomic interval to plot.
1422
+ label_height_factor: A scaling factor for the label height.
1423
+ """
1424
+ # Only add labels if they are provided.
1425
+ if self.has_labels:
1426
+ for i, interval_i in enumerate(self._intervals):
1427
+ label = self._labels[i]
1428
+ # Place label in the middle of the rectangle.
1429
+ # Only plot the piece of the annotation that intersects with the
1430
+ # plotting interval.
1431
+ intersection = interval_i.intersect(interval)
1432
+ if intersection is None:
1433
+ continue
1434
+
1435
+ self.add_label(
1436
+ ax,
1437
+ label_x_position=np.mean((interval_i.start, interval_i.end)),
1438
+ label=label,
1439
+ angle=self._label_angle,
1440
+ label_height_factor=label_height_factor,
1441
+ )
1442
+
1443
+
1444
+ class VariantAnnotation(AbstractAnnotation):
1445
+ """Visualizes variants as thin line-like rectangles across plot components."""
1446
+
1447
+ def __init__(
1448
+ self,
1449
+ variants: Sequence[genome.Variant],
1450
+ colors: str | Sequence[str] = 'orange',
1451
+ alpha: float = 0.8,
1452
+ labels: Sequence[str] | None = None,
1453
+ use_default_labels: bool = True,
1454
+ label_angle: float = 15,
1455
+ label_position: str = 'left',
1456
+ ):
1457
+ """Initializes the `VariantAnnotation` class.
1458
+
1459
+ Args:
1460
+ variants: A sequence of `Variant` objects to annotate.
1461
+ colors: An optional string or sequence of strings specifying the colors of
1462
+ the variant annotation.
1463
+ alpha: The transparency of the variant annotation.
1464
+ labels: An optional sequence of strings to use as labels for the variants.
1465
+ use_default_labels: Whether to use default labels for the variants if
1466
+ `labels` is not provided.
1467
+ label_angle: The angle of the variant labels.
1468
+ label_position: The (horizontal) placement of the variant label, relative
1469
+ to the variant position. Can be any position string accepted by the
1470
+ horizontalalignment argument of matplotlib.axes.Axes.text.
1471
+ """
1472
+ super().__init__(variants, colors, labels, use_default_labels)
1473
+ self._label_angle = label_angle
1474
+ self._alpha = alpha
1475
+ self._variants = variants
1476
+ self._label_position = label_position
1477
+
1478
+ def plot_ax(
1479
+ self,
1480
+ ax: matplotlib.axes.Axes,
1481
+ interval: genome.Interval,
1482
+ hspace: float = 0.0,
1483
+ ):
1484
+ """Adds a variant annotation to an individual axis.
1485
+
1486
+ Args:
1487
+ ax: The matplotlib axis to add the annotation to.
1488
+ interval: The genomic interval to plot.
1489
+ hspace: The vertical space between subplots.
1490
+ """
1491
+ for i, variant in enumerate(self._variants):
1492
+ if isinstance(self._colors, str):
1493
+ color = self._colors
1494
+ else:
1495
+ color = self._colors[i]
1496
+ interval_i = variant.reference_interval
1497
+ # Only plot the piece of the annotation that intersects with the plotting
1498
+ # interval.
1499
+ intersection = interval_i.intersect(interval)
1500
+ if intersection is None:
1501
+ continue
1502
+ ax.axvspan(
1503
+ interval_i.start,
1504
+ interval_i.end,
1505
+ # Set y-maximum to be the top of the axis, including hspace.
1506
+ ymax=(1 + hspace) * 1.03,
1507
+ alpha=self._alpha,
1508
+ facecolor=color,
1509
+ # Set edgecolor to None for intervals to avoid horizongal lines
1510
+ # between axes, but keep it for variants. As variants are typically
1511
+ # a very thin rectanle, removing the edges results in the rectangle
1512
+ # not being visible.
1513
+ edgecolor=color,
1514
+ clip_on=False,
1515
+ )
1516
+
1517
+ def plot_labels(
1518
+ self,
1519
+ ax: matplotlib.axes.Axes,
1520
+ interval: genome.Interval,
1521
+ label_height_factor: float,
1522
+ ):
1523
+ """Adds variant labels to an axis.
1524
+
1525
+ Args:
1526
+ ax: The matplotlib axis to add the labels to.
1527
+ interval: The genomic interval to plot.
1528
+ label_height_factor: A scaling factor for the label height.
1529
+ """
1530
+ # Only add labels if they are provided.
1531
+ if self.has_labels:
1532
+ for i, variant in enumerate(self._variants):
1533
+ interval_i = variant.reference_interval
1534
+ # Using truncated string method for genome.Variant class to get default
1535
+ # labels for variants.
1536
+ label = (
1537
+ variant.as_truncated_str(max_length=20)
1538
+ if self._use_default_labels
1539
+ else self._labels[i]
1540
+ )
1541
+ # Place label in the middle of the rectangle.
1542
+ # Only plot the piece of the annotation that intersects with the
1543
+ # plotting interval.
1544
+ intersection = interval_i.intersect(interval)
1545
+ if intersection is None:
1546
+ continue
1547
+
1548
+ self.add_label(
1549
+ ax,
1550
+ label_x_position=np.mean((interval_i.start, interval_i.end)),
1551
+ label=label,
1552
+ angle=self._label_angle,
1553
+ label_height_factor=label_height_factor,
1554
+ label_position=self._label_position,
1555
+ )
flax_model/alphagenome/_sdk/visualization/plot_transcripts.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Visualize transcripts/gene annotation in matplotlib."""
16
+
17
+ from collections.abc import Sequence
18
+ import dataclasses
19
+ import enum
20
+ from typing import Any
21
+
22
+ from flax_model.alphagenome._sdk.data import genome
23
+ from flax_model.alphagenome._sdk.data import transcript as transcript_utils
24
+ import intervaltree
25
+ import matplotlib as mpl
26
+ import matplotlib.figure
27
+ import matplotlib.path
28
+ import matplotlib.pyplot as plt
29
+
30
+
31
+ @dataclasses.dataclass
32
+ class TranscriptStyle:
33
+ """Style specification for a transcript plot.
34
+
35
+ CDS = protein coding sequence.
36
+ UTR = untranslated region.
37
+
38
+ Attributes:
39
+ cds_height: CDS height.
40
+ utr_height: UTR height.
41
+ cds_color: CDS color.
42
+ utr5_color: 5' UTR color.
43
+ utr3_color: 3' UTR color.
44
+ first_noncoding_exon_color: Color of the first non-coding exon. This helps
45
+ to indicate transcript directionality.
46
+ label_color: Label color.
47
+ xlim_pad: Controls amount of whitespace on the region flanks.
48
+ """
49
+
50
+ cds_height: float
51
+ utr_height: float
52
+ cds_color: str
53
+ utr5_color: str
54
+ utr3_color: str
55
+ first_noncoding_exon_color: str
56
+ label_color: str
57
+ xlim_pad: float
58
+
59
+
60
+ class TranscriptStylePreset(enum.Enum):
61
+ """Style enum for transcript plots.
62
+
63
+ Attributes:
64
+ STANDARD: Standard transcript style.
65
+ MINIMAL: Minimal transcript style.
66
+ """
67
+
68
+ STANDARD = TranscriptStyle(
69
+ cds_height=0.7,
70
+ utr_height=0.35,
71
+ cds_color='#7f7f7f', # Grey from tab10 palette.
72
+ utr5_color='#ff7f0e', # Orange.
73
+ utr3_color='#1f77b4', # Blue.
74
+ first_noncoding_exon_color='#2ca02c', # Green.
75
+ label_color='#7f7f7f',
76
+ xlim_pad=0.01,
77
+ )
78
+
79
+ MINIMAL = TranscriptStyle(
80
+ cds_height=0.4,
81
+ utr_height=0.22,
82
+ cds_color='black',
83
+ utr5_color='black',
84
+ utr3_color='black',
85
+ # TODO(b/377291432): find a nice way of specifying strand orientation.
86
+ first_noncoding_exon_color='black',
87
+ label_color='black',
88
+ xlim_pad=0.05,
89
+ )
90
+
91
+
92
+ def plot_transcripts(
93
+ ax: plt.Axes,
94
+ transcripts: Sequence[transcript_utils.Transcript],
95
+ interval: genome.Interval,
96
+ zero_origin: bool = False,
97
+ label_name: str | None = None,
98
+ transcript_style: TranscriptStyle = TranscriptStylePreset.STANDARD.value,
99
+ plot_labels_once: bool = False,
100
+ **kwargs,
101
+ ) -> mpl.figure.Figure:
102
+ """Plot transcripts.
103
+
104
+ Loops over each transcript in `transcripts` and calls `draw_transcript`.
105
+
106
+ Args:
107
+ ax: Matplotlib axis onto which to plot transcript annotations.
108
+ transcripts: Sequence of transcripts returned by a
109
+ transcript.TranscriptExtractor.
110
+ interval: Genomic interval at which to visualize the transcripts.
111
+ zero_origin: If True, the beginning of the interval will start with 0.
112
+ label_name: Which label in transcript.info to draw next to the transcript.
113
+ transcript_style: specification of transcript styling details.
114
+ plot_labels_once: If True, labels will only be plotted once.
115
+ **kwargs: kwargs passed to draw_transcript.
116
+
117
+ Returns:
118
+ Matplotlib figure object.
119
+ """
120
+ if not transcripts:
121
+ return
122
+
123
+ # Slightly pad x limits for nicer spacing, and shift x axis limits if needed.
124
+ shift = -interval.start if zero_origin else 0
125
+ xlim_pad = interval.width * transcript_style.xlim_pad
126
+ ax.set_xlim(
127
+ [interval.start + shift - xlim_pad, interval.end + shift + xlim_pad]
128
+ )
129
+
130
+ # Get typical label width and transcript heights.
131
+ text_width = _get_text_width(transcripts[0].info[label_name], ax=ax)
132
+ heights = _get_placement_heights(
133
+ transcripts, extend_fraction=1.0, front_padding=text_width
134
+ )
135
+
136
+ labels_already_drawn = []
137
+ for transcript in transcripts:
138
+ # Add transcript labels.
139
+ if label_name is not None:
140
+ label = transcript.info[label_name]
141
+ else:
142
+ label = None
143
+
144
+ draw_transcript(
145
+ ax=ax,
146
+ transcript=transcript,
147
+ interval=interval,
148
+ y=heights[transcript.transcript_id],
149
+ cds_height=transcript_style.cds_height,
150
+ utr_height=transcript_style.utr_height,
151
+ cds_color=transcript_style.cds_color,
152
+ utr5_color=transcript_style.utr5_color,
153
+ utr3_color=transcript_style.utr3_color,
154
+ first_noncoding_exon_color=transcript_style.first_noncoding_exon_color,
155
+ label_color=transcript_style.label_color,
156
+ shift=shift,
157
+ label=None
158
+ if (label in labels_already_drawn and plot_labels_once)
159
+ else label,
160
+ num_transcripts=len(transcripts),
161
+ **kwargs,
162
+ )
163
+
164
+ labels_already_drawn.append(label)
165
+
166
+ ax.set_ylim([min(heights.values()) - 1, max(heights.values()) + 1])
167
+
168
+
169
+ def draw_transcript(
170
+ ax: plt.Axes,
171
+ transcript: transcript_utils.Transcript,
172
+ interval: genome.Interval,
173
+ y: float,
174
+ cds_height: float = 0.7,
175
+ utr_height: float = 0.35,
176
+ cds_color: str = '#7f7f7f', # Grey from tab10 palette.
177
+ utr5_color: str = '#ff7f0e', # Orange.
178
+ utr3_color: str = '#1f77b4', # Blue.
179
+ first_noncoding_exon_color: str = '#2ca02c', # Green.
180
+ shift: int = 0,
181
+ label: str | None = None,
182
+ label_color: str = '#7f7f7f',
183
+ num_transcripts: int = 1,
184
+ **kwargs,
185
+ ) -> None:
186
+ """Draw an individual transcript as rectangular components on an axis.
187
+
188
+ CDS = protein coding sequence.
189
+ UTR = untranslated region.
190
+
191
+ This function is used by `plot_transcripts`.
192
+
193
+ Args:
194
+ ax: Matplotlib axis onto which to draw the transcript.
195
+ transcript: Transcript to draw.
196
+ interval: Genomic interval at which to visualize the transcript.
197
+ y: Vertical position at which to draw the transcript.
198
+ cds_height: CDS height.
199
+ utr_height: UTR height.
200
+ cds_color: CDS color in hex string format.
201
+ utr5_color: 5' UTR color in hex string format.
202
+ utr3_color: 3' UTR color in hex string format.
203
+ first_noncoding_exon_color: Color of the first non-coding exon. This helps
204
+ to indicate transcript directionality. Hex string format.
205
+ shift: X-axis shift.
206
+ label: Optional label to draw next to the transcript.
207
+ label_color: Label color.
208
+ num_transcripts: Total number of transcripts being drawn, used for dynamic
209
+ arrow sizing.
210
+ **kwargs: Additional keyword arguments passed to matplotlib plotting
211
+ functions.
212
+ """
213
+ ax.set_yticklabels([])
214
+ ax.set_yticks([])
215
+
216
+ def draw_exons_and_introns(exons, color, exon_height):
217
+ if not exons:
218
+ return
219
+ # 1. Draw all exons.
220
+ for exon in exons:
221
+ # TODO: b/377291432 - Skip drawing an exon if it will be drawn below
222
+ # separately to avoid overlap if alpha<1 and overlaps in vector format.
223
+ draw_interval(
224
+ ax=ax,
225
+ interval=exon,
226
+ y=y,
227
+ shift=shift,
228
+ height=exon_height,
229
+ color=color,
230
+ **kwargs,
231
+ )
232
+
233
+ # 2. Draw all introns.
234
+ for intron in transcript_utils.Transcript(exons).introns:
235
+ ax.plot([intron.start, intron.end], [y, y], color=color, linewidth=0.5)
236
+
237
+ # First draw all exons and introns with UTR height.
238
+ draw_exons_and_introns(
239
+ transcript.exons, color=cds_color, exon_height=utr_height
240
+ )
241
+ draw_interval(
242
+ ax=ax,
243
+ interval=transcript.exons[0],
244
+ y=y,
245
+ shift=shift,
246
+ label=label,
247
+ height=utr_height,
248
+ color=cds_color,
249
+ label_color=label_color,
250
+ **kwargs,
251
+ )
252
+
253
+ # Draw the first non-coding exon with a special color.
254
+ first_exon_index = 0 if transcript.is_negative_strand else -1
255
+ draw_interval(
256
+ ax=ax,
257
+ interval=transcript.exons[first_exon_index],
258
+ y=y,
259
+ height=utr_height,
260
+ shift=shift,
261
+ color=first_noncoding_exon_color,
262
+ **kwargs,
263
+ )
264
+
265
+ # Add UTRs for coding transcripts.
266
+ if transcript.cds is not None:
267
+ draw_exons_and_introns(
268
+ transcript.utr5, color=utr5_color, exon_height=utr_height
269
+ )
270
+ draw_exons_and_introns(
271
+ transcript.cds, color=cds_color, exon_height=cds_height
272
+ )
273
+ draw_exons_and_introns(
274
+ transcript.utr3, color=utr3_color, exon_height=utr_height
275
+ )
276
+
277
+ # Draw strand arrows across the full transcript span.
278
+ draw_strand_arrows(
279
+ ax=ax,
280
+ transcript=transcript,
281
+ interval=interval,
282
+ y=y,
283
+ color=cds_color,
284
+ cds_height=cds_height,
285
+ num_transcripts=num_transcripts,
286
+ )
287
+
288
+
289
+ def draw_strand_arrows(
290
+ ax: plt.Axes,
291
+ transcript: transcript_utils.Transcript,
292
+ interval: genome.Interval,
293
+ y: float,
294
+ color: str,
295
+ *,
296
+ cds_height: float = 0.22,
297
+ num_transcripts: int = 1,
298
+ max_arrows_per_intron: int = 5,
299
+ ) -> None:
300
+ """Draw strand direction arrows on intron lines.
301
+
302
+ Arrow count per intron is computed dynamically based on the intron's width
303
+ relative to the visible interval. Marker size is derived from the UTR height
304
+ so arrows are always visually smaller than UTR exons.
305
+
306
+ Args:
307
+ ax: Matplotlib axis.
308
+ transcript: The transcript being drawn.
309
+ interval: The visible genomic interval.
310
+ y: Vertical position of the transcript.
311
+ color: Arrow color.
312
+ cds_height: CDS height in data coordinates, used to scale arrows.
313
+ num_transcripts: Total number of transcripts being drawn.
314
+ max_arrows_per_intron: Maximum number of arrows per intron.
315
+ """
316
+ introns = transcript_utils.Transcript(transcript.exons).introns
317
+ if not introns:
318
+ return
319
+
320
+ fig = ax.get_figure()
321
+ if fig is not None:
322
+ _, fig_height_inches = (
323
+ fig.get_size_inches() # pytype: disable=attribute-error
324
+ )
325
+ ax_height_inches = ax.get_position().height * fig_height_inches
326
+ y_range = num_transcripts + 2
327
+ if y_range > 0:
328
+ pts_per_data = (ax_height_inches * 72) / y_range
329
+ markersize = min(4.0, cds_height * pts_per_data * 2)
330
+ else:
331
+ markersize = 4.0
332
+ else:
333
+ markersize = 4.0
334
+
335
+ # Custom chevron path: two line segments forming > or < shape.
336
+ if transcript.is_negative_strand:
337
+ chevron = matplotlib.path.Path(
338
+ [(0.5, 0.5), (-0.5, 0.0), (0.5, -0.5)],
339
+ [
340
+ matplotlib.path.Path.MOVETO,
341
+ matplotlib.path.Path.LINETO,
342
+ matplotlib.path.Path.LINETO,
343
+ ],
344
+ )
345
+ else:
346
+ chevron = matplotlib.path.Path(
347
+ [(-0.5, 0.5), (0.5, 0.0), (-0.5, -0.5)],
348
+ [
349
+ matplotlib.path.Path.MOVETO,
350
+ matplotlib.path.Path.LINETO,
351
+ matplotlib.path.Path.LINETO,
352
+ ],
353
+ )
354
+
355
+ arrow_positions = []
356
+ for intron in introns:
357
+ intron_to_interval_fraction = intron.width / interval.width
358
+ # Skip arrows for introns that are too small.
359
+ if intron_to_interval_fraction < 0.01:
360
+ continue
361
+ # Use sqrt scaling so large introns don't get overwhelmed with arrows.
362
+ num_arrows = min(
363
+ max(1, round(intron_to_interval_fraction**0.5 * max_arrows_per_intron)),
364
+ max_arrows_per_intron,
365
+ )
366
+ space = intron.width / (num_arrows + 1)
367
+ for i in range(1, num_arrows + 1):
368
+ arrow_pos = intron.start + i * space
369
+ # Skip arrows too close to interval edges.
370
+ if arrow_pos < interval.start + 10 or arrow_pos > interval.end - 10:
371
+ continue
372
+ arrow_positions.append(arrow_pos)
373
+
374
+ if arrow_positions:
375
+ ax.plot(
376
+ arrow_positions,
377
+ [y] * len(arrow_positions),
378
+ marker=chevron,
379
+ markersize=markersize,
380
+ color=color,
381
+ fillstyle='none',
382
+ markeredgewidth=0.8,
383
+ linestyle='none',
384
+ clip_on=True,
385
+ )
386
+
387
+
388
+ def draw_interval(
389
+ ax: plt.Axes,
390
+ interval: genome.Interval,
391
+ y: float,
392
+ label: str | None = None,
393
+ height: float = 0.5,
394
+ shift: int = 0,
395
+ label_color: str = '#7f7f7f',
396
+ **kwargs,
397
+ ):
398
+ """Draw rectangle patch on the axis given a genomic interval.
399
+
400
+ Args:
401
+ ax: Matplotlib axis onto which to draw the interval.
402
+ interval: Genomic interval to draw.
403
+ y: Vertical position at which to draw the interval.
404
+ label: Optional label to draw next to the interval.
405
+ height: Height of the interval.
406
+ shift: X-axis shift.
407
+ label_color: Label color in hex string format.
408
+ **kwargs: Additional keyword arguments passed to matplotlib plotting
409
+ functions.
410
+ """
411
+ xy = (interval.start + shift, y - height / 2)
412
+ ax.add_patch(
413
+ mpl.patches.Rectangle(
414
+ xy=xy,
415
+ width=interval.width,
416
+ height=height,
417
+ clip_on=True,
418
+ linewidth=0,
419
+ **kwargs,
420
+ )
421
+ )
422
+
423
+ # Add center-aligned text label.
424
+ if label is not None:
425
+ ax.text(
426
+ x=max(xy[0], ax.get_xlim()[0]),
427
+ y=y,
428
+ s=label,
429
+ color=label_color,
430
+ horizontalalignment='right',
431
+ verticalalignment='center',
432
+ )
433
+
434
+
435
+ def _get_placement_heights(
436
+ transcripts: Sequence[transcript_utils.Transcript],
437
+ extend_fraction: float = 1.0,
438
+ front_padding: float = 0.0,
439
+ ) -> dict[Any, int]:
440
+ """Get heights at which to place the transcripts."""
441
+ # TODO: b/376672690 - Implement simpler packing algorithm.
442
+ levels = [intervaltree.IntervalTree()]
443
+ # Sort transcripts by length and start placing longest transcripts first.
444
+ sorted_transcripts = sorted(
445
+ transcripts, key=lambda x: x.transcript_interval.width, reverse=True
446
+ )
447
+ transcript_levels = {}
448
+ for transcript in sorted_transcripts:
449
+ placed = False
450
+ level_idx = 0
451
+ while not placed:
452
+ if level_idx >= len(levels):
453
+ levels.append(intervaltree.IntervalTree())
454
+ if levels[level_idx].overlaps(
455
+ transcript.transcript_interval.start - front_padding,
456
+ transcript.transcript_interval.end,
457
+ ):
458
+ # Overlaps an existing interval -> increase the level.
459
+ level_idx += 1
460
+ else:
461
+ # Doesn't overlap. Remember the interval.
462
+ levels[level_idx].addi(
463
+ transcript.transcript_interval.start - front_padding,
464
+ int(transcript.transcript_interval.end * extend_fraction),
465
+ )
466
+ transcript_levels[transcript.transcript_id] = level_idx
467
+ placed = True
468
+ return {
469
+ transcript_id: len(levels) - 1 - level_idx
470
+ for transcript_id, level_idx in transcript_levels.items()
471
+ }
472
+
473
+
474
+ def _get_text_width(label: str, ax: plt.Axes, **kwargs) -> float:
475
+ """Get text width in data coordinates."""
476
+ text = ax.text(0, 0, label, **kwargs)
477
+ plt.gcf().canvas.draw()
478
+ bb = text.get_window_extent().transformed(ax.transData.inverted())
479
+ text.remove() # Remove text.
480
+ return bb.x1 - bb.x0
flax_model/alphagenome/evals/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """AlphaGenome Evaluation Utilities."""
flax_model/alphagenome/evals/regression_metrics.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utility functions for experiments."""
16
+
17
+ from typing import Sequence
18
+ from flax_model.alphagenome._sdk import typing
19
+ import chex
20
+ import jax
21
+ import jax.numpy as jnp
22
+ from jaxtyping import ArrayLike, Float, PyTree # pylint: disable=g-importing-member, g-multiple-import
23
+ import numpy as np
24
+
25
+
26
+ @chex.dataclass
27
+ class _PearsonRState:
28
+ """State to compute PearsonR correlation coefficient."""
29
+
30
+ xy_sum: jax.Array
31
+ x_sum: jax.Array
32
+ xx_sum: jax.Array
33
+ y_sum: jax.Array
34
+ yy_sum: jax.Array
35
+ count: jax.Array
36
+
37
+ def __add__(self, other: '_PearsonRState') -> '_PearsonRState':
38
+ return jax.tree.map(lambda x, y: x + y, self, other)
39
+
40
+
41
+ def _pearsonr_initialize() -> '_PearsonRState':
42
+ """Initialize PearsonrState with zeros."""
43
+ return _PearsonRState(
44
+ xy_sum=np.zeros(()),
45
+ x_sum=np.zeros(()),
46
+ xx_sum=np.zeros(()),
47
+ y_sum=np.zeros(()),
48
+ yy_sum=np.zeros(()),
49
+ count=np.zeros(()),
50
+ )
51
+
52
+
53
+ def _pearsonr_update(
54
+ x: jax.Array,
55
+ y: jax.Array,
56
+ axis: Sequence[int] | int | None = None,
57
+ mask: jax.Array | None = None,
58
+ ) -> _PearsonRState:
59
+ """Construct PearsonrState by correlating two arrays."""
60
+ if mask is not None:
61
+ mask = jnp.astype(mask, bool)
62
+ return _PearsonRState(
63
+ xy_sum=jnp.sum(x * y, axis=axis, where=mask, dtype=jnp.float32),
64
+ x_sum=jnp.sum(x, axis=axis, where=mask, dtype=jnp.float32),
65
+ xx_sum=jnp.sum(jnp.square(x), axis=axis, where=mask, dtype=jnp.float32),
66
+ y_sum=jnp.sum(y, axis=axis, where=mask, dtype=jnp.float32),
67
+ yy_sum=jnp.sum(jnp.square(y), axis=axis, where=mask, dtype=jnp.float32),
68
+ count=jnp.sum(jnp.ones_like(x), axis=axis, where=mask, dtype=jnp.float32),
69
+ )
70
+
71
+
72
+ def _pearsonr_result(state: _PearsonRState) -> jax.Array:
73
+ """Get PearsonR correlation coeficient."""
74
+ x_mean = state.x_sum / state.count
75
+ y_mean = state.y_sum / state.count
76
+
77
+ covariance = state.xy_sum - state.count * x_mean * y_mean
78
+
79
+ x_var = state.xx_sum - state.count * x_mean * x_mean
80
+ y_var = state.yy_sum - state.count * y_mean * y_mean
81
+ variance = x_var**0.5 * y_var**0.5
82
+ eps = jnp.finfo(variance.dtype).eps # Avoid division by zero.
83
+ return covariance / (variance + eps)
84
+
85
+
86
+ @chex.dataclass
87
+ class RegressionState:
88
+ """State for accumulating regression statistics."""
89
+
90
+ pearsonr: _PearsonRState
91
+ pearsonr_log1p: _PearsonRState
92
+ sq_error: jax.Array
93
+ abs_error: jax.Array
94
+ count: jax.Array
95
+
96
+ def __add__(self, other: 'RegressionState') -> 'RegressionState':
97
+ return jax.tree.map(
98
+ lambda a, b: a + b,
99
+ self,
100
+ other,
101
+ )
102
+
103
+
104
+ def initialize_regression_metrics() -> RegressionState:
105
+ """Initialize metric state."""
106
+ return RegressionState(
107
+ pearsonr=_pearsonr_initialize(),
108
+ pearsonr_log1p=_pearsonr_initialize(),
109
+ sq_error=np.zeros(()),
110
+ abs_error=np.zeros(()),
111
+ count=np.zeros(()),
112
+ )
113
+
114
+
115
+ def update_regression_metrics(
116
+ y_true: jax.Array,
117
+ y_pred: jax.Array,
118
+ mask: jax.Array | None = None,
119
+ ) -> RegressionState:
120
+ y_true = jnp.astype(y_true, jnp.float32)
121
+ y_pred = jnp.astype(y_pred, jnp.float32)
122
+ return RegressionState(
123
+ pearsonr=_pearsonr_update(y_true, y_pred, mask=mask, axis=(-2, -3)),
124
+ pearsonr_log1p=_pearsonr_update(
125
+ jnp.log1p(y_true), jnp.log1p(y_pred), mask=mask, axis=(-2, -3)
126
+ ),
127
+ sq_error=jnp.sum(
128
+ jnp.square(y_true - y_pred),
129
+ axis=(-2, -3),
130
+ where=mask,
131
+ dtype=jnp.float32,
132
+ ),
133
+ abs_error=jnp.sum(
134
+ jnp.abs(y_true - y_pred),
135
+ axis=(-2, -3),
136
+ where=mask,
137
+ dtype=jnp.float32,
138
+ ),
139
+ count=jnp.sum(
140
+ jnp.ones_like(y_true), axis=(-2, -3), where=mask, dtype=jnp.float32
141
+ ),
142
+ )
143
+
144
+
145
+ def finalize_regression_metrics(
146
+ state: PyTree[RegressionState],
147
+ ) -> PyTree[jax.Array]:
148
+ """Compute final metrics from accumulated state."""
149
+
150
+ def _finalize(state: RegressionState) -> PyTree[jax.Array]:
151
+ return {
152
+ 'pearsonr': (
153
+ _pearsonr_result(state.pearsonr).mean(
154
+ where=state.pearsonr.count > 0
155
+ )
156
+ ),
157
+ 'pearsonr_log1p': (
158
+ _pearsonr_result(state.pearsonr_log1p).mean(
159
+ where=state.pearsonr_log1p.count > 0
160
+ )
161
+ ),
162
+ 'mse': jnp.mean(state.sq_error / state.count, where=state.count > 0),
163
+ 'mae': jnp.mean(state.abs_error / state.count, where=state.count > 0),
164
+ }
165
+
166
+ return jax.tree.map(
167
+ _finalize, state, is_leaf=lambda x: isinstance(x, RegressionState)
168
+ )
169
+
170
+
171
+ def reduce_regression_metrics(
172
+ previous_metrics: PyTree[RegressionState],
173
+ current_metrics: PyTree[RegressionState],
174
+ ) -> RegressionState:
175
+ """Reduce metrics from a single device to a single scalar."""
176
+ return jax.tree.map(
177
+ lambda x, y: x + y,
178
+ previous_metrics,
179
+ current_metrics,
180
+ is_leaf=lambda x: isinstance(x, RegressionState),
181
+ )
182
+
183
+
184
+ @typing.jaxtyped
185
+ def crop_sequence_length(
186
+ x: Float[ArrayLike, '... S D'], *, target_length: int
187
+ ) -> Float[ArrayLike, '... {target_length} D']:
188
+ """Crops an array to match the target length along the sequence dimension."""
189
+ sequence_axis = -2
190
+ if x.shape[sequence_axis] < target_length:
191
+ raise ValueError(
192
+ f'Input length {x.shape[sequence_axis]} is shorter than the requested'
193
+ f' cropped length of {target_length}.'
194
+ )
195
+ elif x.shape[sequence_axis] == target_length:
196
+ return x
197
+ else:
198
+ ltrim = (x.shape[sequence_axis] - target_length) // 2
199
+ rtrim = x.shape[sequence_axis] - target_length - ltrim
200
+ slices = [
201
+ slice(None),
202
+ ] * len(x.shape)
203
+ slices[sequence_axis] = slice(ltrim, -rtrim)
204
+ return x[tuple(slices)]
flax_model/alphagenome/evals/track_prediction.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Evaluates AlphaGenome track prediction performance."""
16
+
17
+ from collections.abc import Iterator
18
+ import pprint
19
+ from typing import Callable, Sequence
20
+ from absl import app
21
+ from absl import logging
22
+ from flax_model.alphagenome._sdk.data import fold_intervals
23
+ from flax_model.alphagenome._sdk.models import dna_output
24
+ from flax_model.alphagenome.evals import regression_metrics
25
+ from flax_model.alphagenome.io import bundles as bundles_lib
26
+ from flax_model.alphagenome.io import dataset
27
+ from flax_model.alphagenome.model import dna_model
28
+ from flax_model.alphagenome.model import model as model_lib
29
+ from flax_model.alphagenome.model import schemas
30
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
31
+ import haiku as hk
32
+ import jax
33
+ from jax import sharding
34
+ from jax.experimental import mesh_utils
35
+ from jaxtyping import PyTree # pylint: disable=g-importing-member
36
+ import jmp
37
+ import kagglehub
38
+ import orbax.checkpoint as ocp
39
+ import pandas as pd
40
+ import tensorflow as tf
41
+
42
+
43
+ PS = sharding.PartitionSpec
44
+ PredictFn = Callable[
45
+ [
46
+ hk.Params,
47
+ hk.State,
48
+ jax.Array,
49
+ jax.Array,
50
+ ],
51
+ PyTree[jax.Array],
52
+ ]
53
+
54
+ _SUBSET = fold_intervals.Subset.VALID
55
+ _LOG_FREQUENCY = 5
56
+ _EVAL_BUNDLES = [
57
+ bundles_lib.BundleName.ATAC,
58
+ bundles_lib.BundleName.CAGE,
59
+ bundles_lib.BundleName.CHIP_HISTONE,
60
+ bundles_lib.BundleName.CHIP_TF,
61
+ bundles_lib.BundleName.DNASE,
62
+ bundles_lib.BundleName.PROCAP,
63
+ bundles_lib.BundleName.RNA_SEQ,
64
+ ]
65
+
66
+
67
+ def load_model(
68
+ model_version: dna_model.ModelVersion = dna_model.ModelVersion.FOLD_0,
69
+ ) -> tuple[hk.Params, hk.State, PredictFn]:
70
+ """Loads the model consiting of params, state and predict function."""
71
+ checkpoint_path = kagglehub.model_download(
72
+ f'google/alphagenome/jax/{model_version.name.lower()}'
73
+ )
74
+ params, state = ocp.StandardCheckpointer().restore(checkpoint_path)
75
+ metadata = {
76
+ organism: metadata_lib.load(organism) for organism in dna_model.Organism
77
+ }
78
+
79
+ @hk.transform_with_state
80
+ def forward(dna_sequence, organism_index):
81
+ policy = jmp.get_policy('params=float32,compute=bfloat16,output=bfloat16')
82
+ with hk.mixed_precision.push_policy(model_lib.AlphaGenome, policy):
83
+ return model_lib.AlphaGenome(metadata)(dna_sequence, organism_index)
84
+
85
+ @jax.jit(
86
+ in_shardings=(PS(), PS(), PS('data'), PS('data')),
87
+ out_shardings=PS('data'),
88
+ )
89
+ def predict(params, state, dna_sequence, organism_index) -> PyTree[jax.Array]:
90
+ (predictions, _), _ = forward.apply(
91
+ params, state, None, dna_sequence, organism_index
92
+ )
93
+ predictions = dna_model.extract_predictions(predictions)
94
+ return predictions
95
+
96
+ return params, state, predict
97
+
98
+
99
+ def create_eval_step(
100
+ predict_fn: PredictFn, bundles: Sequence[bundles_lib.BundleName]
101
+ ):
102
+ """Returns the eval step function."""
103
+
104
+ @jax.jit(
105
+ in_shardings=(PS(), PS(), PS('data')),
106
+ out_shardings=PS(),
107
+ )
108
+ def eval_step(params, state, batch: schemas.DataBatch):
109
+ predictions = predict_fn(
110
+ params, state, batch.dna_sequence, batch.organism_index
111
+ )
112
+ metrics_step = {}
113
+ for bundle in bundles:
114
+ targets_true, mask = batch.get_genome_tracks(bundle)
115
+ targets_pred = predictions[dna_output.OutputType[bundle.name]]
116
+ targets_pred = regression_metrics.crop_sequence_length(
117
+ targets_pred, target_length=targets_true.shape[-2]
118
+ )
119
+ metrics_step[bundle.name] = regression_metrics.update_regression_metrics(
120
+ targets_true, targets_pred, mask
121
+ )
122
+ return metrics_step
123
+
124
+ return eval_step
125
+
126
+
127
+ def evaluate(
128
+ params: hk.Params,
129
+ state: hk.State,
130
+ predict_fn: PredictFn,
131
+ bundles: Sequence[bundles_lib.BundleName],
132
+ dataset_iterator: Iterator[tuple[schemas.DataBatch, dataset.BatchMetadata]],
133
+ ):
134
+ """Evaluates the model."""
135
+ # Setup Mesh.
136
+ devices = mesh_utils.create_device_mesh((jax.local_device_count(),))
137
+ mesh = jax.sharding.Mesh(devices, axis_names=('data',))
138
+ sharding_rep = sharding.NamedSharding(mesh, PS())
139
+ sharding_data = sharding.NamedSharding(mesh, PS('data'))
140
+
141
+ # Replicate params and state.
142
+ params = jax.device_put(params, sharding_rep)
143
+ state = jax.device_put(state, sharding_rep)
144
+
145
+ eval_step = create_eval_step(predict_fn, bundles)
146
+ metrics = {
147
+ b.name: regression_metrics.initialize_regression_metrics()
148
+ for b in bundles
149
+ }
150
+ num_elements = 0
151
+
152
+ for i, (batch, _) in enumerate(dataset_iterator):
153
+ num_elements += batch.dna_sequence.shape[0]
154
+ if i % _LOG_FREQUENCY == 1:
155
+ m = pprint.pformat(
156
+ regression_metrics.finalize_regression_metrics(metrics)
157
+ )
158
+ logging.info('step %d: %s', i, m)
159
+
160
+ with jax.set_mesh(mesh):
161
+ batch = jax.device_put(batch, sharding_data)
162
+ step_metrics = eval_step(params, state, batch)
163
+
164
+ # Accumulate metrics.
165
+ step_metrics = jax.device_get(step_metrics)
166
+ metrics = regression_metrics.reduce_regression_metrics(
167
+ metrics, step_metrics
168
+ )
169
+ logging.info('num_elements: %d', num_elements)
170
+ return regression_metrics.finalize_regression_metrics(metrics)
171
+
172
+
173
+ def run(
174
+ organism: dna_model.Organism = dna_model.Organism.HOMO_SAPIENS,
175
+ model_version: dna_model.ModelVersion = dna_model.ModelVersion.FOLD_0,
176
+ ) -> pd.DataFrame:
177
+ """Runs the track prediction experiment."""
178
+ logging.info('Starting track prediction experiment.')
179
+ params, state, predict_fn = load_model(model_version)
180
+ dataset_iterator = dataset.get_numpy_dataset_iterator(
181
+ batch_size=jax.local_device_count(),
182
+ organism=organism,
183
+ model_version=model_version,
184
+ bundles=_EVAL_BUNDLES,
185
+ subset=_SUBSET,
186
+ )
187
+ results = evaluate(params, state, predict_fn, _EVAL_BUNDLES, dataset_iterator)
188
+ flattened_results = {}
189
+ for bundle, result in results.items():
190
+ for metric, value in result.items():
191
+ logging.info('bundle: %s, metric: %s, value: %s', bundle, metric, value)
192
+ flattened_results[f'{bundle}_{metric}'] = value
193
+ df = pd.DataFrame(
194
+ {'metric': flattened_results.keys(), 'value': flattened_results.values()}
195
+ )
196
+ return df
197
+
198
+
199
+ if __name__ == '__main__':
200
+ # Hide local GPUs from TF. TF is only used for data loading.
201
+ tf.config.set_visible_devices([], 'GPU')
202
+ app.run(lambda _: run())
flax_model/alphagenome/finetuning/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utilities for finetuning."""
flax_model/alphagenome/finetuning/dataset.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Data pipeline for reading sequence and tracks for fine-tuning."""
15
+
16
+ from collections.abc import Iterator, Sequence
17
+ import concurrent.futures
18
+ from typing import Any, Mapping
19
+
20
+ from absl import logging
21
+ from flax_model.alphagenome._sdk.data import genome
22
+ from flax_model.alphagenome._sdk.models import dna_model
23
+ from flax_model.alphagenome._sdk.models import dna_output
24
+ from flax_model.alphagenome.io import fasta
25
+ from flax_model.alphagenome.model import one_hot_encoder
26
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
27
+ import numpy as np
28
+ import pandas as pd
29
+ import pyBigWig
30
+ import tensorflow as tf
31
+
32
+
33
+ class BigWigExtractor:
34
+ """BigWig file extractor using pyBigWig."""
35
+
36
+ def __init__(self, file_path: str):
37
+ self._bw = pyBigWig.open(file_path)
38
+ self._chromosomes = set(self._bw.chroms().keys())
39
+
40
+ def __del__(self):
41
+ self.close()
42
+
43
+ @property
44
+ def chromosomes(self) -> set[str]:
45
+ return self._chromosomes
46
+
47
+ def extract(self, interval: genome.Interval) -> tf.Tensor:
48
+ """Extracts values from a BigWig file for a given interval."""
49
+ if interval.chromosome not in self._chromosomes:
50
+ raise ValueError(
51
+ f'Chromosome {interval.chromosome} not found in BigWig. '
52
+ 'Check self._bw.chroms() for available chromosomes. '
53
+ )
54
+ start = max(0, interval.start)
55
+ end = min(self._bw.chroms()[interval.chromosome], interval.end)
56
+
57
+ if end <= start:
58
+ return tf.cast(np.zeros(interval.width), tf.bfloat16)
59
+
60
+ values = self._bw.values(interval.chromosome, start, end, numpy=True)
61
+ if values.shape[0] != interval.width:
62
+ pad_left = start - interval.start
63
+ pad_right = interval.end - end
64
+ values = np.pad(
65
+ values, pad_width=(pad_left, pad_right), constant_values=0
66
+ )
67
+ return tf.cast(np.nan_to_num(values, nan=0.0), tf.bfloat16)
68
+
69
+ def close(self):
70
+ if self._bw is not None:
71
+ self._bw.close()
72
+
73
+
74
+ class MultiTrackExtractor:
75
+ """Multi-track BigWig file extractor.
76
+
77
+ Returns tracks per output type. Tracks are ordered as per the metadata.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ output_metadata: metadata_lib.AlphaGenomeOutputMetadata,
83
+ sequence_length: int,
84
+ max_workers=32,
85
+ ):
86
+ """Initializes the MultiTrackExtractor.
87
+
88
+ Args:
89
+ output_metadata: The output metadata containing the track information.
90
+ sequence_length: The length of the sequence to extract.
91
+ max_workers: The maximum number of workers to use for parallel extraction.
92
+ """
93
+ self._output_metadata = output_metadata
94
+ self._sequence_length = sequence_length
95
+ self._executor = concurrent.futures.ThreadPoolExecutor(
96
+ max_workers=max_workers
97
+ )
98
+
99
+ # group bigwig extractors by output type.
100
+ self._bw_extractors: dict[
101
+ dna_output.OutputType, Sequence[BigWigExtractor]
102
+ ] = {}
103
+ for output_type in dna_output.OutputType:
104
+ if (metadata := output_metadata.get(output_type)) is not None:
105
+ self._bw_extractors[output_type] = [
106
+ BigWigExtractor(file_path) for file_path in metadata['file_path']
107
+ ]
108
+
109
+ self._track_masks = {
110
+ f'{output_type.name.lower()}_mask': np.logical_not(mask).reshape(1, -1)
111
+ for output_type, mask in output_metadata.padding.items()
112
+ }
113
+
114
+ def get_output_signature(self):
115
+ """Returns the output signature of the dataset."""
116
+ signature = {}
117
+ for output_type, extractors in self._bw_extractors.items():
118
+ num_tracks = len(extractors)
119
+ output_name = output_type.name.lower()
120
+ signature[output_name] = tf.TensorSpec(
121
+ shape=(self._sequence_length, num_tracks),
122
+ dtype=tf.bfloat16,
123
+ )
124
+ signature[f'{output_name}_mask'] = tf.TensorSpec(
125
+ shape=(1, num_tracks),
126
+ dtype=tf.bool,
127
+ )
128
+ return signature
129
+
130
+ def extract(self, interval: genome.Interval) -> Mapping[str, tf.Tensor]:
131
+ """Extracts all tracks for all groups in parallel."""
132
+
133
+ # Submit
134
+ future_map = {}
135
+ for output_type, extractors in self._bw_extractors.items():
136
+ future_map[output_type] = [
137
+ self._executor.submit(bw.extract, interval) for bw in extractors
138
+ ]
139
+
140
+ # Collect
141
+ data = {}
142
+ for output_type, futures in future_map.items():
143
+ try:
144
+ results = [f.result() for f in futures]
145
+ data[output_type.name.lower()] = np.stack(results, axis=-1)
146
+ except Exception as e:
147
+ raise RuntimeError(
148
+ f"Failed to extract tracks for output group '{output_type.name}'"
149
+ ) from e
150
+ return data | self._track_masks
151
+
152
+ def close(self):
153
+ for bw_extractor in self._bw_extractors.values():
154
+ for bw in bw_extractor:
155
+ bw.close()
156
+
157
+
158
+ class DataPipeline:
159
+ """Data pipeline for reading sequence and tracks."""
160
+
161
+ def __init__(
162
+ self,
163
+ *,
164
+ fasta_path: str,
165
+ intervals: pd.DataFrame,
166
+ output_metadata: metadata_lib.AlphaGenomeOutputMetadata,
167
+ organism: dna_model.Organism,
168
+ sequence_length: int,
169
+ ):
170
+ if organism != dna_model.Organism.HOMO_SAPIENS:
171
+ raise NotImplementedError('Only HOMO_SAPIENS is currently supported.')
172
+
173
+ self._fasta_extractor = fasta.FastaExtractor(fasta_path)
174
+ self._intervals = intervals
175
+ self._organsim = organism
176
+ self._organism_index = 0 # Only one organism is supported.
177
+ self._sequence_length = sequence_length
178
+ self._one_hot_encoder = one_hot_encoder.DNAOneHotEncoder()
179
+ self._multi_track_extractor = MultiTrackExtractor(
180
+ output_metadata=output_metadata,
181
+ sequence_length=sequence_length,
182
+ )
183
+
184
+ def get_element(self, idx: int) -> Mapping[str, Any]:
185
+ """Returns a single element of the dataset."""
186
+ interval = self._intervals.iloc[idx]
187
+ interval = genome.Interval(
188
+ chromosome=interval['chromosome'],
189
+ start=int(interval['start']),
190
+ end=int(interval['end']),
191
+ )
192
+ interval = interval.resize(self._sequence_length)
193
+ seq_str = self._fasta_extractor.extract(interval)
194
+ seq_one_hot = self._one_hot_encoder.encode(seq_str)
195
+ track_bundles = self._multi_track_extractor.extract(interval)
196
+ return {
197
+ 'dna_sequence': seq_one_hot,
198
+ 'organism_index': self._organism_index,
199
+ 'bundles': track_bundles,
200
+ }
201
+
202
+ def get_output_signature(self):
203
+ """Returns the output signature of the dataset."""
204
+ return {
205
+ 'dna_sequence': tf.TensorSpec(
206
+ shape=(self._sequence_length, 4), dtype=tf.float32
207
+ ),
208
+ 'organism_index': tf.TensorSpec(shape=[], dtype=tf.int32),
209
+ 'bundles': self._multi_track_extractor.get_output_signature(),
210
+ }
211
+
212
+ def get_generator(
213
+ self, num_epochs: int = -1, shuffle: bool = True, seed: int = 0
214
+ ) -> Iterator[Mapping[str, Any]]:
215
+ """Returns a generator for the dataset."""
216
+ rng = np.random.default_rng(seed=seed)
217
+ num_epochs = num_epochs if num_epochs > 0 else float('inf')
218
+ epoch_idx = 0
219
+ while epoch_idx < num_epochs:
220
+ epoch_idx += 1
221
+ if shuffle:
222
+ self._intervals = self._intervals.sample(
223
+ frac=1, random_state=rng
224
+ ).reset_index(drop=True)
225
+ for idx in range(len(self._intervals)):
226
+ try:
227
+ yield self.get_element(idx)
228
+ except Exception as e: # pylint: disable=broad-except
229
+ logging.warning(
230
+ 'Failed to get interval %s. With error: %s',
231
+ self._intervals.iloc[idx].to_dict(),
232
+ e,
233
+ )
234
+
235
+ def close(self):
236
+ self._multi_track_extractor.close()
flax_model/alphagenome/finetuning/dataset_test.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from unittest import mock
16
+
17
+ from absl.testing import absltest
18
+ from absl.testing import parameterized
19
+ from flax_model.alphagenome._sdk.data import genome
20
+ from flax_model.alphagenome._sdk.models import dna_client
21
+ from flax_model.alphagenome.finetuning import dataset
22
+ from flax_model.alphagenome.io import fasta
23
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
24
+ import chex
25
+ import numpy as np
26
+ import pandas as pd
27
+ import pyBigWig
28
+ import tensorflow as tf
29
+
30
+ MOCK_CHROM_SIZES = {'chr1': 100}
31
+
32
+
33
+ def get_mock_metadata(num_atac_tracks: int = 2, num_dnase_tracks: int = 1):
34
+ return metadata_lib.AlphaGenomeOutputMetadata(
35
+ atac=pd.DataFrame({
36
+ 'file_path': [f'path{i}' for i in range(num_atac_tracks)],
37
+ 'name': [f'atac_track{i}' for i in range(num_atac_tracks)],
38
+ 'strand': ['.'] * num_atac_tracks,
39
+ }),
40
+ dnase=pd.DataFrame({
41
+ 'file_path': [f'path{i}' for i in range(num_dnase_tracks)],
42
+ 'name': [f'dnase_track{i}' for i in range(num_dnase_tracks)],
43
+ 'strand': ['.'] * num_dnase_tracks,
44
+ }),
45
+ )
46
+
47
+
48
+ def get_mock_bw():
49
+ mock_bw = mock.Mock()
50
+ mock_bw.chroms.return_value = MOCK_CHROM_SIZES
51
+
52
+ def mock_values(chrom, start, end, numpy=True):
53
+ del chrom, numpy
54
+ return np.full(end - start, 1.0)
55
+
56
+ mock_bw.values = mock_values
57
+ return mock_bw
58
+
59
+
60
+ class MockFastaExtractor:
61
+
62
+ def __init__(self, path):
63
+ pass
64
+
65
+ def extract(self, interval):
66
+ return 'A' * interval.width
67
+
68
+
69
+ class DataTest(parameterized.TestCase):
70
+
71
+ @parameterized.named_parameters([
72
+ dict(
73
+ testcase_name='valid_interval',
74
+ interval=genome.Interval('chr1', 10, 20),
75
+ expected_values=np.ones(10),
76
+ ),
77
+ dict(
78
+ testcase_name='interval_start_negative',
79
+ interval=genome.Interval('chr1', -10, 10),
80
+ expected_values=np.concatenate([np.zeros(10), np.ones(10)]),
81
+ ),
82
+ dict(
83
+ testcase_name='interval_end_beyond_chrom_length',
84
+ interval=genome.Interval('chr1', 90, 110),
85
+ expected_values=np.concatenate([np.ones(10), np.zeros(10)]),
86
+ ),
87
+ dict(
88
+ testcase_name='interval_fully_outside_negative',
89
+ interval=genome.Interval('chr1', -20, -10),
90
+ expected_values=np.zeros(10),
91
+ ),
92
+ dict(
93
+ testcase_name='interval_fully_outside_positive',
94
+ interval=genome.Interval('chr1', 110, 120),
95
+ expected_values=np.zeros(10),
96
+ ),
97
+ dict(
98
+ testcase_name='zero_length_interval',
99
+ interval=genome.Interval('chr1', 10, 10),
100
+ expected_values=np.array([]),
101
+ ),
102
+ ])
103
+ @mock.patch.object(pyBigWig, 'open')
104
+ def test_bigwig_extractor(self, mock_bigwig_open, interval, expected_values):
105
+ mock_bigwig_open.return_value = get_mock_bw()
106
+ extractor = dataset.BigWigExtractor('mock_path')
107
+ values = extractor.extract(interval)
108
+ self.assertEqual(values.shape, expected_values.shape)
109
+ np.testing.assert_array_equal(values, expected_values)
110
+ extractor.close()
111
+ mock_bigwig_open.assert_called_once_with('mock_path')
112
+
113
+ @mock.patch.object(pyBigWig, 'open')
114
+ def test_bigwig_extractor_wrong_chromosome(self, mock_bigwig_open):
115
+ mock_bigwig_open.return_value = get_mock_bw()
116
+ extractor = dataset.BigWigExtractor('mock_path')
117
+ with self.assertRaisesRegex(ValueError, 'Chromosome chr2 not found'):
118
+ extractor.extract(genome.Interval('chr2', 0, 10))
119
+ extractor.close()
120
+ mock_bigwig_open.assert_called_once_with('mock_path')
121
+
122
+ @mock.patch.object(pyBigWig, 'open')
123
+ def test_multi_track_extractor(self, mock_bigwig_open):
124
+ mock_bigwig_open.return_value = get_mock_bw()
125
+ extractor = dataset.MultiTrackExtractor(
126
+ output_metadata=get_mock_metadata(
127
+ num_atac_tracks=3, num_dnase_tracks=4
128
+ ),
129
+ sequence_length=10,
130
+ )
131
+ interval = genome.Interval('chr1', 10, 20)
132
+ result = extractor.extract(interval)
133
+
134
+ chex.assert_shape(result['atac'], (10, 3))
135
+ self.assertEqual(result['atac'].dtype, tf.bfloat16)
136
+ chex.assert_shape(result['atac_mask'], (1, 3))
137
+ self.assertEqual(result['atac_mask'].dtype, tf.bool)
138
+ chex.assert_shape(result['dnase'], (10, 4))
139
+ chex.assert_shape(result['dnase_mask'], (1, 4))
140
+ extractor.close()
141
+ self.assertEqual(mock_bigwig_open.call_count, 7)
142
+
143
+ @parameterized.named_parameters([
144
+ dict(testcase_name='one_epoch', num_epochs=1),
145
+ dict(testcase_name='three_epochs', num_epochs=3),
146
+ ])
147
+ @mock.patch.object(fasta, 'FastaExtractor', MockFastaExtractor)
148
+ @mock.patch.object(pyBigWig, 'open')
149
+ def test_data_pipeline(self, mock_bigwig_open, num_epochs):
150
+ mock_bigwig_open.return_value = get_mock_bw()
151
+ intervals = pd.DataFrame({
152
+ 'chromosome': ['chr1', 'chr1', 'chr1'],
153
+ 'start': [10, 50, 90],
154
+ 'end': [30, 70, 110],
155
+ })
156
+ sequence_length = 30
157
+ pipeline = dataset.DataPipeline(
158
+ fasta_path='mock_fasta',
159
+ intervals=intervals,
160
+ output_metadata=get_mock_metadata(
161
+ num_atac_tracks=2, num_dnase_tracks=1
162
+ ),
163
+ organism=dna_client.Organism.HOMO_SAPIENS,
164
+ sequence_length=sequence_length,
165
+ )
166
+
167
+ generator = pipeline.get_generator(num_epochs=num_epochs, shuffle=False)
168
+ elements = list(generator)
169
+ self.assertLen(elements, len(intervals) * num_epochs)
170
+
171
+ element = elements[0]
172
+ chex.assert_shape(element['dna_sequence'], (sequence_length, 4))
173
+ np.testing.assert_array_equal(
174
+ element['dna_sequence'],
175
+ np.tile(np.array([[1, 0, 0, 0]]), (sequence_length, 1)), # 'A'
176
+ )
177
+ self.assertEqual(element['organism_index'], 0)
178
+ bundles = element['bundles']
179
+ chex.assert_shape(bundles['atac'], (sequence_length, 2))
180
+ chex.assert_shape(bundles['dnase'], (sequence_length, 1))
181
+
182
+ # The third interval (non shuffled) is half out of bounds.
183
+ element = elements[2]
184
+ bundles = element['bundles']
185
+ expected_track = np.concatenate([np.ones(15), np.zeros(15)])
186
+ np.testing.assert_array_equal(
187
+ bundles['atac'], np.stack([expected_track] * 2, axis=-1)
188
+ )
189
+ np.testing.assert_array_equal(
190
+ bundles['dnase'], expected_track.reshape(-1, 1)
191
+ )
192
+
193
+ pipeline.close()
194
+ self.assertEqual(mock_bigwig_open.call_count, 3)
195
+
196
+
197
+ if __name__ == '__main__':
198
+ absltest.main()
flax_model/alphagenome/finetuning/finetune.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """AlphaGenome finetuning script."""
15
+
16
+ from collections.abc import Iterator, Mapping
17
+ from typing import Any, Callable
18
+ from flax_model.alphagenome._sdk.data import fold_intervals
19
+ from flax_model.alphagenome._sdk.models import dna_model
20
+ from flax_model.alphagenome.finetuning import dataset as dataset_lib
21
+ from flax_model.alphagenome.model import model
22
+ from flax_model.alphagenome.model import schemas
23
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
24
+ import haiku as hk
25
+ import jax
26
+ import jmp
27
+ import optax
28
+ import tensorflow as tf
29
+
30
+ _FASTA_PATH = (
31
+ 'https://storage.googleapis.com/alphagenome/reference/gencode/'
32
+ 'hg38/GRCh38.p13.genome.fa'
33
+ )
34
+
35
+
36
+ def get_dataset_iterator(
37
+ *,
38
+ batch_size: int,
39
+ sequence_length: int,
40
+ output_metadata: metadata_lib.AlphaGenomeOutputMetadata,
41
+ model_version: dna_model.ModelVersion,
42
+ subset: fold_intervals.Subset,
43
+ organism: dna_model.Organism = dna_model.Organism.HOMO_SAPIENS,
44
+ fasta_path: str = _FASTA_PATH,
45
+ example_regions_path: str | None = None,
46
+ ) -> Iterator[schemas.DataBatch]:
47
+ """Converts pipeline output dict to a DataBatch schema.
48
+
49
+ Args:
50
+ batch_size: The batch size of the dataset.
51
+ sequence_length: The sequence length of the dataset.
52
+ output_metadata: Metadata for the output tracks for each organism.
53
+ model_version: The model version to use.
54
+ subset: The subset of the dataset.
55
+ organism: The organism to use.
56
+ fasta_path: The path to the reference genome FASTA file.
57
+ example_regions_path: The path to the example regions BED file.
58
+
59
+ Returns:
60
+ A schemas.DataBatch object.
61
+ """
62
+ intervals = fold_intervals.get_fold_intervals(
63
+ model_version,
64
+ organism,
65
+ subset,
66
+ example_regions_path=example_regions_path,
67
+ )
68
+ pipeline = dataset_lib.DataPipeline(
69
+ fasta_path=fasta_path,
70
+ intervals=intervals,
71
+ output_metadata=output_metadata,
72
+ organism=organism,
73
+ sequence_length=sequence_length,
74
+ )
75
+ dataset = tf.data.Dataset.from_generator(
76
+ pipeline.get_generator,
77
+ output_signature=pipeline.get_output_signature(),
78
+ )
79
+ dataset = (
80
+ dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE).as_numpy_iterator()
81
+ )
82
+
83
+ def iterator():
84
+ for batch in dataset:
85
+ yield schemas.DataBatch(
86
+ dna_sequence=batch['dna_sequence'],
87
+ organism_index=batch['organism_index'],
88
+ **batch['bundles'],
89
+ )
90
+
91
+ return iterator()
92
+
93
+
94
+ def get_forward_fn(
95
+ output_metadata: Mapping[
96
+ dna_model.Organism, metadata_lib.AlphaGenomeOutputMetadata
97
+ ],
98
+ jmp_policy: str = 'params=float32,compute=bfloat16,output=bfloat16',
99
+ ) -> hk.TransformedWithState:
100
+ """Creates a Haiku transformed function for the AlphaGenome model.
101
+
102
+ Args:
103
+ output_metadata: Metadata for the output tracks for each organism.
104
+ jmp_policy: The JMP policy to use for mixed precision.
105
+
106
+ Returns:
107
+ A `hk.TransformedWithState` object representing the forward pass.
108
+ """
109
+ jmp_policy = jmp.get_policy(jmp_policy)
110
+
111
+ @hk.transform_with_state
112
+ def forward(batch: schemas.DataBatch):
113
+ with hk.mixed_precision.push_policy(model.AlphaGenome, jmp_policy):
114
+ return model.AlphaGenome(
115
+ output_metadata, freeze_trunk_embeddings=True
116
+ ).loss(batch)
117
+
118
+ return forward
119
+
120
+
121
+ def get_train_step(
122
+ predict_fn: Callable[..., Any],
123
+ optimizer: optax.GradientTransformation,
124
+ ):
125
+ """Creates a jitted training step function using AlphaGenome.loss.
126
+
127
+ Args:
128
+ predict_fn: The Haiku transformed forward function.
129
+ optimizer: An Optax optimizer to apply gradients.
130
+
131
+ Returns:
132
+ A jitted function `train_step` that takes `params`, `state`, `opt_state`,
133
+ and a `batch` as input and returns the updated `params`, `next_state`,
134
+ `new_opt_state`, and a dictionary of `metrics`.
135
+ """
136
+
137
+ @jax.jit
138
+ def train_step(params, state, opt_state, batch):
139
+ def loss_fn(params, state, batch):
140
+ (loss, scalars, predictions), new_state = predict_fn(
141
+ params, state, None, batch
142
+ )
143
+ del predictions # Unused.
144
+ return loss, (new_state, scalars)
145
+
146
+ loss_grad_fn = jax.value_and_grad(loss_fn, has_aux=True)
147
+ (loss, (next_state, scalars)), grads = loss_grad_fn(params, state, batch)
148
+ scalars['loss'] = loss
149
+ updates, new_opt_state = optimizer.update(grads, opt_state, params)
150
+ new_params = optax.apply_updates(params, updates)
151
+ return new_params, next_state, new_opt_state, scalars
152
+
153
+ return train_step