Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- .msc +0 -0
- .mv +1 -0
- LICENSE +203 -0
- README.md +248 -0
- flax_model/__init__.py +1 -0
- flax_model/alphagenome/__init__.py +39 -0
- flax_model/alphagenome/_sdk/__init__.py +25 -0
- flax_model/alphagenome/_sdk/colab_utils.py +62 -0
- flax_model/alphagenome/_sdk/data/__init__.py +15 -0
- flax_model/alphagenome/_sdk/data/fold_intervals.py +115 -0
- flax_model/alphagenome/_sdk/data/gene_annotation.py +445 -0
- flax_model/alphagenome/_sdk/data/genome.py +1181 -0
- flax_model/alphagenome/_sdk/data/junction_data.py +272 -0
- flax_model/alphagenome/_sdk/data/ontology.py +122 -0
- flax_model/alphagenome/_sdk/data/track_data.py +918 -0
- flax_model/alphagenome/_sdk/data/transcript.py +755 -0
- flax_model/alphagenome/_sdk/interpretation/__init__.py +15 -0
- flax_model/alphagenome/_sdk/interpretation/ism.py +150 -0
- flax_model/alphagenome/_sdk/models/__init__.py +15 -0
- flax_model/alphagenome/_sdk/models/dna_client.py +907 -0
- flax_model/alphagenome/_sdk/models/dna_model.py +507 -0
- flax_model/alphagenome/_sdk/models/dna_output.py +435 -0
- flax_model/alphagenome/_sdk/models/interval_scorers.py +145 -0
- flax_model/alphagenome/_sdk/models/junction_data_utils.py +216 -0
- flax_model/alphagenome/_sdk/models/track_data_utils.py +260 -0
- flax_model/alphagenome/_sdk/models/variant_scorers.py +878 -0
- flax_model/alphagenome/_sdk/protos/__init__.py +15 -0
- flax_model/alphagenome/_sdk/protos/dna_model.proto +559 -0
- flax_model/alphagenome/_sdk/protos/dna_model_pb2.py +100 -0
- flax_model/alphagenome/_sdk/protos/dna_model_pb2_grpc.py +4 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service.proto +268 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service_pb2.py +57 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service_pb2_grpc.py +274 -0
- flax_model/alphagenome/_sdk/protos/tensor.proto +104 -0
- flax_model/alphagenome/_sdk/protos/tensor_pb2.py +33 -0
- flax_model/alphagenome/_sdk/protos/tensor_pb2_grpc.py +4 -0
- flax_model/alphagenome/_sdk/tensor_utils.py +171 -0
- flax_model/alphagenome/_sdk/typing.py +39 -0
- flax_model/alphagenome/_sdk/visualization/__init__.py +15 -0
- flax_model/alphagenome/_sdk/visualization/plot.py +569 -0
- flax_model/alphagenome/_sdk/visualization/plot_components.py +1555 -0
- flax_model/alphagenome/_sdk/visualization/plot_transcripts.py +480 -0
- flax_model/alphagenome/evals/__init__.py +14 -0
- flax_model/alphagenome/evals/regression_metrics.py +204 -0
- flax_model/alphagenome/evals/track_prediction.py +202 -0
- flax_model/alphagenome/finetuning/__init__.py +15 -0
- flax_model/alphagenome/finetuning/dataset.py +236 -0
- flax_model/alphagenome/finetuning/dataset_test.py +198 -0
- 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
|