wuxing0105 commited on
Commit
2e69359
·
verified ·
1 Parent(s): 8ad4804

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. flax_model/alphagenome/_sdk/colab_utils.py +62 -0
  2. flax_model/alphagenome/_sdk/data/fold_intervals.py +115 -0
  3. flax_model/alphagenome/_sdk/data/junction_data.py +272 -0
  4. flax_model/alphagenome/_sdk/data/ontology.py +122 -0
  5. flax_model/alphagenome/_sdk/data/track_data.py +918 -0
  6. flax_model/alphagenome/_sdk/data/transcript.py +755 -0
  7. flax_model/alphagenome/_sdk/interpretation/__init__.py +15 -0
  8. flax_model/alphagenome/_sdk/interpretation/ism.py +150 -0
  9. flax_model/alphagenome/_sdk/models/__init__.py +15 -0
  10. flax_model/alphagenome/_sdk/models/dna_client.py +907 -0
  11. flax_model/alphagenome/_sdk/models/dna_model.py +507 -0
  12. flax_model/alphagenome/_sdk/models/dna_output.py +435 -0
  13. flax_model/alphagenome/_sdk/models/junction_data_utils.py +216 -0
  14. flax_model/alphagenome/_sdk/models/track_data_utils.py +260 -0
  15. flax_model/alphagenome/_sdk/protos/__init__.py +15 -0
  16. flax_model/alphagenome/_sdk/protos/dna_model.proto +559 -0
  17. flax_model/alphagenome/_sdk/protos/dna_model_pb2.py +100 -0
  18. flax_model/alphagenome/_sdk/protos/dna_model_pb2_grpc.py +4 -0
  19. flax_model/alphagenome/_sdk/protos/dna_model_service.proto +268 -0
  20. flax_model/alphagenome/_sdk/protos/dna_model_service_pb2.py +57 -0
  21. flax_model/alphagenome/_sdk/protos/dna_model_service_pb2_grpc.py +274 -0
  22. flax_model/alphagenome/_sdk/protos/tensor.proto +104 -0
  23. flax_model/alphagenome/_sdk/protos/tensor_pb2.py +33 -0
  24. flax_model/alphagenome/_sdk/protos/tensor_pb2_grpc.py +4 -0
  25. flax_model/alphagenome/_sdk/tensor_utils.py +171 -0
  26. flax_model/alphagenome/_sdk/visualization/plot.py +569 -0
  27. flax_model/alphagenome/_sdk/visualization/plot_components.py +1555 -0
  28. flax_model/alphagenome/_sdk/visualization/plot_transcripts.py +480 -0
  29. flax_model/alphagenome/finetuning/__init__.py +15 -0
  30. flax_model/alphagenome/finetuning/dataset.py +236 -0
  31. flax_model/alphagenome/finetuning/dataset_test.py +198 -0
  32. flax_model/alphagenome/finetuning/finetune.py +153 -0
  33. flax_model/alphagenome/finetuning/finetune_test.py +179 -0
  34. flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_HOMO_SAPIENS.textproto +0 -0
  35. flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_MUS_MUSCULUS.textproto +0 -0
  36. flax_model/alphagenome/model/metadata/metadata_test.py +215 -0
  37. flax_model/alphagenome/model/variant_scoring/__init__.py +15 -0
  38. flax_model/alphagenome/model/variant_scoring/splice_junction.py +306 -0
  39. flax_model/alphagenome/model/variant_scoring/variant_scoring_test.py +76 -0
  40. scripts/inference.sh +28 -0
  41. weight/README.md +13 -0
  42. weight/alphagenome-all-folds/README.md +334 -0
  43. weight/alphagenome-all-folds/_CHECKPOINT_METADATA +1 -0
  44. weight/alphagenome-all-folds/_METADATA +0 -0
  45. weight/alphagenome-all-folds/d/549b1a1ce6bea654c08ac287872a5d99 +0 -0
  46. weight/alphagenome-all-folds/gitattributes +38 -0
  47. weight/alphagenome-all-folds/manifest.ocdbt +0 -0
  48. weight/alphagenome-all-folds/notebook.ipynb +0 -0
  49. weight/alphagenome-all-folds/ocdbt.process_0/d/41569d1134215cd16b582168403f7509 +0 -0
  50. weight/alphagenome-all-folds/ocdbt.process_0/manifest.ocdbt +0 -0
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/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/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/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/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/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/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
flax_model/alphagenome/finetuning/finetune_test.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import os
16
+
17
+ from absl.testing import absltest
18
+ from flax_model.alphagenome.finetuning import finetune
19
+ from flax_model.alphagenome.model import dna_model
20
+ from flax_model.alphagenome.model import model as model_lib
21
+ from flax_model.alphagenome.model import schemas
22
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
23
+ import haiku as hk
24
+ import jax
25
+ import jax.numpy as jnp
26
+ import numpy as np
27
+ import optax
28
+ import orbax.checkpoint as ocp
29
+ import pandas as pd
30
+
31
+
32
+ def _create_mock_df(modality_name: str, num_tracks: int) -> pd.DataFrame | None:
33
+ if num_tracks == 0:
34
+ return None
35
+ return pd.DataFrame({
36
+ 'file_path': [f'path{i}' for i in range(num_tracks)],
37
+ 'name': [f'{modality_name}_track{i}' for i in range(num_tracks)],
38
+ 'strand': ['.'] * num_tracks,
39
+ })
40
+
41
+
42
+ def get_mock_metadata(
43
+ num_atac_tracks: int = 0, num_dnase_tracks: int = 0, rna_seq_tracks: int = 0
44
+ ) -> metadata_lib.AlphaGenomeOutputMetadata:
45
+ return metadata_lib.AlphaGenomeOutputMetadata(
46
+ atac=_create_mock_df('atac', num_atac_tracks),
47
+ dnase=_create_mock_df('dnase', num_dnase_tracks),
48
+ rna_seq=_create_mock_df('rna_seq', rna_seq_tracks),
49
+ )
50
+
51
+
52
+ class FinetuneTest(absltest.TestCase):
53
+
54
+ def test_finetune_train_step(self):
55
+ seq_length, batch_size, key = 131072, 1, jax.random.key(0)
56
+
57
+ # Setup base model.
58
+ base_metadata = {
59
+ dna_model.Organism.HOMO_SAPIENS: get_mock_metadata(
60
+ num_atac_tracks=(atac_tracks_base := 2),
61
+ rna_seq_tracks=(rna_seq_tracks_base := 1),
62
+ )
63
+ }
64
+
65
+ @hk.transform_with_state
66
+ def base_forward(batch: schemas.DataBatch):
67
+ return model_lib.AlphaGenome(base_metadata).loss(batch)
68
+
69
+ # Batch contains ATAC and RNA-seq data.
70
+ base_batch = schemas.DataBatch(
71
+ dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32),
72
+ organism_index=np.zeros((batch_size,), dtype=np.int32),
73
+ atac=jnp.zeros(
74
+ (batch_size, seq_length, atac_tracks_base), dtype=np.float32
75
+ ),
76
+ atac_mask=np.ones((batch_size, 1, atac_tracks_base), dtype=bool),
77
+ rna_seq=np.zeros(
78
+ (batch_size, seq_length, rna_seq_tracks_base), dtype=np.float32
79
+ ),
80
+ rna_seq_mask=np.ones((batch_size, 1, rna_seq_tracks_base), dtype=bool),
81
+ )
82
+ base_params, base_state = jax.eval_shape(base_forward.init, key, base_batch)
83
+
84
+ # Setup fine-tuning model with different metadata.
85
+ ft_metadata = {
86
+ dna_model.Organism.HOMO_SAPIENS: get_mock_metadata(
87
+ num_atac_tracks=(atac_tracks_ft := 3), # More ATAC tracks.
88
+ rna_seq_tracks=0, # No RNA-seq tracks.
89
+ num_dnase_tracks=(dnase_tracks_ft := 7), # New DNase tracks.
90
+ )
91
+ }
92
+ batch_ft = schemas.DataBatch(
93
+ dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32),
94
+ organism_index=np.zeros((batch_size,), dtype=np.int32),
95
+ atac=jnp.zeros(
96
+ (batch_size, seq_length, atac_tracks_ft), dtype=np.float32
97
+ ),
98
+ atac_mask=np.ones((batch_size, 1, atac_tracks_ft), dtype=bool),
99
+ dnase=jnp.zeros(
100
+ (batch_size, seq_length, dnase_tracks_ft), dtype=np.float32
101
+ ),
102
+ dnase_mask=np.ones((batch_size, 1, dnase_tracks_ft), dtype=bool),
103
+ )
104
+
105
+ forward_fn = finetune.get_forward_fn(ft_metadata)
106
+ params_ft, _ = jax.eval_shape(forward_fn.init, key, batch_ft)
107
+
108
+ def merge(frozen, trainable):
109
+ trainable = hk.data_structures.filter(
110
+ lambda module_name, *_: 'head' in module_name, trainable
111
+ )
112
+ merged = hk.data_structures.merge(frozen, trainable)
113
+ return merged
114
+
115
+ params = merge(base_params, params_ft)
116
+
117
+ optimizer = optax.adam(learning_rate=1e-3)
118
+ opt_state = jax.eval_shape(optimizer.init, params)
119
+ train_step = finetune.get_train_step(forward_fn.apply, optimizer)
120
+ _, _, _, scalars = jax.eval_shape(
121
+ train_step, params, base_state, opt_state, batch_ft
122
+ )
123
+ self.assertIn('loss', scalars)
124
+ self.assertIn('atac_loss', scalars)
125
+ self.assertIn('dnase_loss', scalars)
126
+ self.assertNotIn('rna_seq_loss', scalars) # RNA-seq head not in FT model.
127
+
128
+ def test_create_finetuned_dna_sequence_model(self):
129
+ seq_length, batch_size, key = 131072, 1, jax.random.key(0)
130
+ ft_metadata = {
131
+ dna_model.Organism.HOMO_SAPIENS: get_mock_metadata(
132
+ num_atac_tracks=(atac_tracks_ft := 3), # More ATAC tracks.
133
+ rna_seq_tracks=0, # No RNA-seq tracks.
134
+ num_dnase_tracks=(dnase_tracks_ft := 7), # New DNase tracks.
135
+ )
136
+ }
137
+ batch_ft = schemas.DataBatch(
138
+ dna_sequence=np.zeros((batch_size, seq_length, 4), dtype=np.float32),
139
+ organism_index=np.zeros((batch_size,), dtype=np.int32),
140
+ atac=jnp.zeros(
141
+ (batch_size, seq_length, atac_tracks_ft), dtype=np.float32
142
+ ),
143
+ atac_mask=np.ones((batch_size, 1, atac_tracks_ft), dtype=bool),
144
+ dnase=jnp.zeros(
145
+ (batch_size, seq_length, dnase_tracks_ft), dtype=np.float32
146
+ ),
147
+ dnase_mask=np.ones((batch_size, 1, dnase_tracks_ft), dtype=bool),
148
+ )
149
+
150
+ forward_fn = finetune.get_forward_fn(ft_metadata)
151
+ params_ft, state_ft = jax.eval_shape(forward_fn.init, key, batch_ft)
152
+
153
+ checkpointer = ocp.StandardCheckpointer()
154
+ ckpt_dir = self.create_tempdir().full_path
155
+ ckpt_path = os.path.join(ckpt_dir, 'checkpoint')
156
+ checkpointer.save(
157
+ ckpt_path,
158
+ jax.tree_util.tree_map(
159
+ lambda x: jnp.empty(x.shape, x.dtype), (params_ft, state_ft)
160
+ ),
161
+ )
162
+ checkpointer.wait_until_finished()
163
+
164
+ ft_organism_settings = {
165
+ k: dna_model.OrganismSettings(metadata=v)
166
+ for k, v in ft_metadata.items()
167
+ }
168
+ finetuned_dna_sequence_model = dna_model.create(
169
+ ckpt_path,
170
+ organism_settings=ft_organism_settings,
171
+ device=jax.devices('cpu')[0],
172
+ )
173
+ self.assertIsInstance(
174
+ finetuned_dna_sequence_model, dna_model.AlphaGenomeModel
175
+ )
176
+
177
+
178
+ if __name__ == '__main__':
179
+ absltest.main()
flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_HOMO_SAPIENS.textproto ADDED
The diff for this file is too large to render. See raw diff
 
flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_MUS_MUSCULUS.textproto ADDED
The diff for this file is too large to render. See raw diff
 
flax_model/alphagenome/model/metadata/metadata_test.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 collections.abc import Mapping, Sequence
16
+ from absl.testing import absltest
17
+ from absl.testing import parameterized
18
+ from flax_model.alphagenome._sdk.data import ontology
19
+ from flax_model.alphagenome._sdk.models import dna_model
20
+ from flax_model.alphagenome._sdk.models import dna_output
21
+ from flax_model.alphagenome.model.metadata import metadata as metadata_lib
22
+ import chex
23
+ import jax
24
+ import numpy as np
25
+ import pandas as pd
26
+
27
+
28
+ class MetadataTest(parameterized.TestCase):
29
+
30
+ @parameterized.parameters([
31
+ dict(organism=dna_model.Organism.HOMO_SAPIENS),
32
+ dict(organism=dna_model.Organism.MUS_MUSCULUS),
33
+ ])
34
+ def test_load(self, organism: dna_model.Organism):
35
+ metadata = metadata_lib.load(organism)
36
+ self.assertIsInstance(metadata, dna_output.OutputMetadata)
37
+ reindexing = metadata.strand_reindexing
38
+ self.assertIsInstance(reindexing, dict)
39
+ self.assertCountEqual(dna_output.OutputType, reindexing.keys())
40
+
41
+ def test_organism_shapes_match(self):
42
+ metadata = tuple(metadata_lib.load(o) for o in dna_model.Organism)
43
+ for output_type in dna_output.OutputType:
44
+ with self.subTest(f'{output_type=}'):
45
+ track_metadata = tuple(m.get(output_type).values for m in metadata)
46
+ # Only assert the first track dimension matches, as fields may differ.
47
+ chex.assert_equal_shape_prefix(track_metadata, 1)
48
+
49
+ @parameterized.named_parameters([
50
+ dict(
51
+ testcase_name='Normal',
52
+ output_type=dna_output.OutputType.ATAC,
53
+ names=[f'track_{i}' for i in range(6)],
54
+ strands=['+', '+', '+', '-', '-', '-'],
55
+ expected=[3, 4, 5, 0, 1, 2],
56
+ ),
57
+ dict(
58
+ testcase_name='Unstranded',
59
+ output_type=dna_output.OutputType.RNA_SEQ,
60
+ names=[f'track_{i}' for i in range(6)],
61
+ strands=['.', '.', '.', '.', '.', '.'],
62
+ expected=[0, 1, 2, 3, 4, 5],
63
+ ),
64
+ dict(
65
+ testcase_name='Interleaved',
66
+ output_type=dna_output.OutputType.RNA_SEQ,
67
+ names=[f'track_{i}' for i in range(6)],
68
+ strands=['+', '-', '+', '-', '+', '-'],
69
+ expected=[1, 0, 3, 2, 5, 4],
70
+ ),
71
+ dict(
72
+ testcase_name='Padding',
73
+ output_type=dna_output.OutputType.DNASE,
74
+ names=[f'track_{i}' for i in range(6)] + ['Padding', 'padding'],
75
+ strands=['+', '-', '+', '-', '+', '-', '.', '.'],
76
+ expected=[1, 0, 3, 2, 5, 4, 6, 7],
77
+ ),
78
+ dict(
79
+ testcase_name='SpliceJunctions',
80
+ output_type=dna_output.OutputType.SPLICE_JUNCTIONS,
81
+ names=[f'track_{i}' for i in range(3)],
82
+ strands=['.'] * 3,
83
+ expected=[3, 4, 5, 0, 1, 2],
84
+ ),
85
+ dict(
86
+ testcase_name='SpliceJunctionPadding',
87
+ output_type=dna_output.OutputType.SPLICE_JUNCTIONS,
88
+ names=[f'track_{i}' for i in range(3)] + ['PADDING'],
89
+ strands=['.'] * 4,
90
+ expected=[4, 5, 6, 7, 0, 1, 2, 3],
91
+ ),
92
+ ])
93
+ def test_strand_reindexing(
94
+ self,
95
+ output_type: dna_output.OutputType,
96
+ names: Sequence[str],
97
+ strands: Sequence[str],
98
+ expected: Sequence[int],
99
+ ):
100
+ metadata = metadata_lib.AlphaGenomeOutputMetadata(
101
+ **{
102
+ output_type.name.lower(): pd.DataFrame(
103
+ {'name': names, 'strand': strands}
104
+ ),
105
+ },
106
+ )
107
+ result = metadata.strand_reindexing.get(output_type)
108
+ result = result.tolist() if result is not None else None
109
+ self.assertEqual(result, expected)
110
+
111
+ @parameterized.named_parameters(
112
+ (
113
+ 'Normal',
114
+ dna_output.OutputType.ATAC,
115
+ [f'track_{i}' for i in range(6)],
116
+ ['.'] * 6,
117
+ [False] * 6,
118
+ ),
119
+ (
120
+ 'Padding',
121
+ dna_output.OutputType.DNASE,
122
+ [f'track_{i}' for i in range(3)] + ['Padding', 'Padding'],
123
+ ['.'] * 3 + ['.', '.'],
124
+ [False] * 3 + [True, True],
125
+ ),
126
+ )
127
+ def test_padding(
128
+ self,
129
+ output_type: dna_output.OutputType,
130
+ names: Sequence[str],
131
+ strands: Sequence[str],
132
+ expected: Sequence[int],
133
+ ):
134
+ metadata = metadata_lib.AlphaGenomeOutputMetadata(
135
+ **{
136
+ output_type.name.lower(): pd.DataFrame(
137
+ {'name': names, 'strand': strands}
138
+ )
139
+ },
140
+ )
141
+ np.testing.assert_array_equal(metadata.padding.get(output_type), expected)
142
+
143
+ @parameterized.named_parameters(
144
+ (
145
+ 'SingleOutput',
146
+ [dna_output.OutputType.ATAC],
147
+ None,
148
+ {dna_output.OutputType.ATAC: [True, True, True]},
149
+ ),
150
+ (
151
+ 'SingleOntology',
152
+ [dna_output.OutputType.ATAC],
153
+ [ontology.from_curie('CL:0000084')],
154
+ {dna_output.OutputType.ATAC: [True, True, False]},
155
+ ),
156
+ (
157
+ 'MultipleOutputsMissingOntology',
158
+ [dna_output.OutputType.ATAC, dna_output.OutputType.DNASE],
159
+ [ontology.from_curie('CL:0000001')],
160
+ {
161
+ dna_output.OutputType.ATAC: [False, False, False],
162
+ dna_output.OutputType.DNASE: [True, True, True, False, False],
163
+ },
164
+ ),
165
+ (
166
+ 'SpliceSites',
167
+ [dna_output.OutputType.SPLICE_SITES],
168
+ [ontology.from_curie('CL:0000001')],
169
+ {
170
+ dna_output.OutputType.SPLICE_SITES: [True, True, True],
171
+ },
172
+ ),
173
+ (
174
+ 'AllOutputs',
175
+ list(dna_output.OutputType),
176
+ None,
177
+ {
178
+ dna_output.OutputType.ATAC: [True, True, True],
179
+ dna_output.OutputType.DNASE: [True, True, True, False, False],
180
+ dna_output.OutputType.SPLICE_SITES: [True, True, True],
181
+ },
182
+ ),
183
+ )
184
+ def test_create_track_masks(
185
+ self,
186
+ requested_outputs: Sequence[dna_output.OutputType],
187
+ requested_ontologies: Sequence[str] | None,
188
+ expected: Mapping[dna_output.OutputType, ...],
189
+ ):
190
+ example_metadata = metadata_lib.AlphaGenomeOutputMetadata(
191
+ atac=pd.DataFrame({
192
+ 'name': ['track_1', 'track_2', 'track_3'],
193
+ 'strand': ['+', '-', '.'],
194
+ 'ontology_curie': ['CL:0000084', 'CL:0000084', 'UBERONE:0000001'],
195
+ }),
196
+ dnase=pd.DataFrame({
197
+ 'name': [f'track_{i}' for i in range(3)] + ['Padding', 'Padding'],
198
+ 'strand': ['+'] * 3 + ['.', '.'],
199
+ 'ontology_curie': ['CL:0000001'] * 3 + ['', ''],
200
+ }),
201
+ splice_sites=pd.DataFrame({
202
+ 'name': [f'track_{i}' for i in range(3)],
203
+ 'strand': ['.'] * 3,
204
+ }),
205
+ )
206
+ track_masks = metadata_lib.create_track_masks(
207
+ example_metadata,
208
+ requested_outputs=requested_outputs,
209
+ requested_ontologies=requested_ontologies,
210
+ )
211
+ jax.tree.map(np.testing.assert_array_equal, track_masks, expected)
212
+
213
+
214
+ if __name__ == '__main__':
215
+ absltest.main()
flax_model/alphagenome/model/variant_scoring/__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
+ """AlphaGenome Variant Scoring."""
flax_model/alphagenome/model/variant_scoring/splice_junction.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Implementation of splice junction variant scoring."""
16
+
17
+ from flax_model.alphagenome._sdk import typing
18
+ from flax_model.alphagenome._sdk.data import genome
19
+ from flax_model.alphagenome._sdk.data import junction_data
20
+ from flax_model.alphagenome._sdk.models import dna_output
21
+ from flax_model.alphagenome._sdk.models import variant_scorers
22
+ from flax_model.alphagenome.model.variant_scoring import gene_mask_extractor
23
+ from flax_model.alphagenome.model.variant_scoring import variant_scoring
24
+ import anndata
25
+ import einshape
26
+ import jax
27
+ import jax.numpy as jnp
28
+ from jaxtyping import Float, Int32, Shaped # pylint: disable=g-multiple-import, g-importing-member
29
+ import numpy as np
30
+ import pandas as pd
31
+ import pyranges
32
+
33
+ MAX_SPLICE_SITES = 256
34
+ PAD_VALUE = -1
35
+
36
+
37
+ def _create_empty(mask_metadata: pd.DataFrame, track_metadata: pd.DataFrame):
38
+ """Create empty AnnData object for splice junction scoring."""
39
+ junction_columns = [
40
+ 'junction_Start',
41
+ 'junction_End',
42
+ ]
43
+ return variant_scoring.create_anndata(
44
+ np.zeros((0, len(track_metadata['name'])), dtype=np.float32),
45
+ obs=pd.DataFrame(columns=list(mask_metadata.columns) + junction_columns),
46
+ var=pd.DataFrame({
47
+ 'strand': '.',
48
+ 'name': track_metadata['name'],
49
+ 'gtex_tissue': track_metadata.get('gtex_tissue'),
50
+ 'ontology_curie': track_metadata.get('ontology_curie'),
51
+ 'biosample_type': track_metadata.get('biosample_type'),
52
+ 'biosample_name': track_metadata.get('biosample_name'),
53
+ 'biosample_life_stage': track_metadata.get('biosample_life_stage'),
54
+ 'data_source': track_metadata.get('data_source'),
55
+ 'Assay title': track_metadata.get('Assay title'),
56
+ }),
57
+ )
58
+
59
+
60
+ def _create(
61
+ junction_scores: pd.DataFrame,
62
+ mask_metadata: pd.DataFrame,
63
+ track_metadata: pd.DataFrame,
64
+ ) -> anndata.AnnData:
65
+ """Converts a dataframe of junction scores to an AnnData object."""
66
+ if mask_metadata.empty or junction_scores.empty:
67
+ raise ValueError('Both junction_scores and mask_metadata must be non-empty')
68
+
69
+ junction_scores = junction_scores[
70
+ junction_scores['gene_id'].isin(mask_metadata['gene_id'])
71
+ ]
72
+
73
+ gene_max_scores = []
74
+ track_names = track_metadata['name']
75
+ for gene_id in junction_scores['gene_id'].unique():
76
+ gene_junction_scores = junction_scores[
77
+ junction_scores.gene_id == gene_id
78
+ ].reset_index(drop=True)
79
+ gene_junction_scores = gene_junction_scores.iloc[
80
+ gene_junction_scores[track_names].values.argmax(0)
81
+ ]
82
+ gene_max_scores.append(gene_junction_scores)
83
+ junction_scores = pd.concat(gene_max_scores)
84
+ score_values = junction_scores[track_names].values
85
+
86
+ # Merge junction information with mask metadata.
87
+ junctions_all_genes = junction_scores[['gene_id', 'Start', 'End']]
88
+ junctions_all_genes.columns = ['gene_id', 'junction_Start', 'junction_End']
89
+ mask_metadata = junctions_all_genes.merge(
90
+ mask_metadata, on='gene_id', sort=False
91
+ )
92
+ # Create the final track metadata.
93
+ track_metadata = pd.DataFrame({
94
+ 'strand': '.', # We already matched prediction by strand.
95
+ 'name': track_names,
96
+ 'gtex_tissue': track_metadata['gtex_tissue'],
97
+ 'ontology_curie': track_metadata.get('ontology_curie'),
98
+ 'biosample_type': track_metadata.get('biosample_type'),
99
+ 'biosample_name': track_metadata.get('biosample_name'),
100
+ 'biosample_life_stage': track_metadata.get('biosample_life_stage'),
101
+ 'data_source': track_metadata.get('data_source'),
102
+ 'Assay title': track_metadata.get('Assay title'),
103
+ })
104
+ ann_data = variant_scoring.create_anndata(
105
+ score_values,
106
+ obs=mask_metadata,
107
+ var=track_metadata,
108
+ )
109
+ # Remove duplicated junctions. Per gene, we report junctions that has maximum
110
+ # score in any tissue. For the reported junctions, we return their predictions
111
+ # in all tissues.
112
+ return ann_data[~ann_data.obs.duplicated()].copy()
113
+
114
+
115
+ @typing.jaxtyped
116
+ def unstack_junction_predictions(
117
+ splice_junction_prediction: Float[np.ndarray, 'D D _'],
118
+ splice_site_positions: Int32[np.ndarray, '4 D'],
119
+ interval: genome.Interval | None = None,
120
+ ) -> tuple[
121
+ Float[np.ndarray, 'num_junctions num_tracks'],
122
+ Shaped[np.ndarray, 'num_junctions'],
123
+ Int32[np.ndarray, 'num_junctions'],
124
+ Int32[np.ndarray, 'num_junctions'],
125
+ ]:
126
+ """Unstack splice junction predictions to long format."""
127
+ # Unpack splice junction predictions.
128
+ splice_junction_prediction = einshape.numpy_einshape(
129
+ 'da(st)->dast', splice_junction_prediction, s=2
130
+ )
131
+ # Convert splice site positions.
132
+ remove_padding_fn = lambda x: x[x != PAD_VALUE]
133
+ pos_donors = remove_padding_fn(splice_site_positions[0])
134
+ pos_acceptors = remove_padding_fn(splice_site_positions[1])
135
+ neg_donors = remove_padding_fn(splice_site_positions[2])
136
+ neg_acceptors = remove_padding_fn(splice_site_positions[3])
137
+ junction_pred_pos = splice_junction_prediction[
138
+ : len(pos_donors), : len(pos_acceptors), 0
139
+ ]
140
+ junction_pred_pos = einshape.numpy_einshape('dat->(da)t', junction_pred_pos)
141
+ num_pos_donors = len(pos_donors)
142
+ pos_donors = np.repeat(pos_donors, len(pos_acceptors))
143
+ pos_acceptors = np.tile(pos_acceptors, num_pos_donors)
144
+ junction_pred_neg = splice_junction_prediction[
145
+ : len(neg_donors), : len(neg_acceptors), 1
146
+ ]
147
+ junction_pred_neg = einshape.numpy_einshape('dat->(da)t', junction_pred_neg)
148
+ num_neg_donors = len(neg_donors)
149
+ neg_donors = np.repeat(neg_donors, len(neg_acceptors))
150
+ neg_acceptors = np.tile(neg_acceptors, num_neg_donors)
151
+ # Combine into a single output.
152
+ junction_predictions = np.concatenate(
153
+ [junction_pred_pos, junction_pred_neg], axis=0
154
+ )
155
+ # Junction start and end positions.
156
+ starts = np.concatenate([pos_donors, neg_acceptors]) + 1
157
+ starts += interval.start if interval is not None else 0
158
+ ends = np.concatenate([pos_acceptors, neg_donors])
159
+ ends += interval.start if interval is not None else 0
160
+ strands = np.array(['+'] * len(pos_donors) + ['-'] * len(neg_donors))
161
+ filter_mask = (starts < ends) & (starts > 0)
162
+
163
+ return (
164
+ junction_predictions[filter_mask],
165
+ strands[filter_mask],
166
+ starts[filter_mask],
167
+ ends[filter_mask],
168
+ )
169
+
170
+
171
+ def junction_predictions_to_dataframe(
172
+ splice_junction_prediction: Float[np.ndarray, 'D D _'],
173
+ splice_site_positions: Int32[np.ndarray, 'T_mul_4 D'],
174
+ metadata: junction_data.JunctionMetadata,
175
+ interval: genome.Interval,
176
+ ) -> pd.DataFrame:
177
+ """Convert splice junction predictions to a dataframe."""
178
+ junction_predictions, strands, starts, ends = unstack_junction_predictions(
179
+ splice_junction_prediction, splice_site_positions, interval
180
+ )
181
+ junctions = pd.DataFrame({
182
+ 'Chromosome': interval.chromosome,
183
+ 'Start': starts,
184
+ 'End': ends,
185
+ 'Strand': strands,
186
+ })
187
+ predictions = pd.DataFrame(junction_predictions, columns=metadata['name'])
188
+ return pd.concat([junctions, predictions], axis=1)
189
+
190
+
191
+ class SpliceJunctionVariantScorer(variant_scoring.VariantScorer):
192
+ """Implements the SpliceJunction variant scoring strategy.
193
+
194
+ Scores variants by the maximum of absolute delta pair counts of junctions
195
+ within the input interval. Junctions are annotated by overlapping with the
196
+ gtf gene intervals.
197
+ """
198
+
199
+ def __init__(self, gtf: pd.DataFrame):
200
+ self._gene_mask_extractor = gene_mask_extractor.GeneMaskExtractor(
201
+ gtf,
202
+ gene_mask_extractor.GeneMaskType.BODY,
203
+ gene_query_type=gene_mask_extractor.GeneQueryType.VARIANT_OVERLAPPING,
204
+ filter_protein_coding=True,
205
+ )
206
+
207
+ def get_masks_and_metadata(
208
+ self,
209
+ interval: genome.Interval,
210
+ variant: genome.Variant,
211
+ *,
212
+ settings: variant_scorers.SpliceJunctionScorer,
213
+ track_metadata: dna_output.OutputMetadata,
214
+ ) -> tuple[None, pd.DataFrame]:
215
+ """See base class."""
216
+ del settings, track_metadata
217
+ _, metadata = self._gene_mask_extractor.extract(interval, variant)
218
+ metadata['interval'] = interval
219
+ return None, metadata
220
+
221
+ def score_variant(
222
+ self,
223
+ ref: variant_scoring.ScoreVariantInput,
224
+ alt: variant_scoring.ScoreVariantInput,
225
+ *,
226
+ masks: None,
227
+ settings: variant_scorers.SpliceJunctionScorer,
228
+ variant: genome.Variant | None = None,
229
+ interval: genome.Interval | None = None,
230
+ ) -> variant_scoring.ScoreVariantOutput:
231
+ """See base class."""
232
+ del variant, interval, masks
233
+ ref_junctions = ref[settings.requested_output]['predictions']
234
+ alt_junctions = alt[settings.requested_output]['predictions']
235
+
236
+ splice_site_positions = ref[settings.requested_output][
237
+ 'splice_site_positions'
238
+ ]
239
+
240
+ # JAX dynamic slicing does not work with transfer_guard.
241
+ with jax.transfer_guard('allow'):
242
+ # Ignore splice sites beyond the max_splice_sites specified. This works
243
+ # because padding splice sites are always at the end of the array.
244
+ ref_junctions = ref_junctions[:MAX_SPLICE_SITES, :MAX_SPLICE_SITES]
245
+ alt_junctions = alt_junctions[:MAX_SPLICE_SITES, :MAX_SPLICE_SITES]
246
+ splice_site_positions = splice_site_positions[:, :MAX_SPLICE_SITES]
247
+
248
+ @jax.jit
249
+ def _apply_log_offset(x):
250
+ return jnp.log(x + 1e-7)
251
+
252
+ ref_junctions = _apply_log_offset(ref_junctions)
253
+ alt_junctions = _apply_log_offset(alt_junctions)
254
+
255
+ return {
256
+ 'delta_counts': (alt_junctions - ref_junctions).astype(jnp.float16),
257
+ 'splice_site_positions': splice_site_positions,
258
+ }
259
+
260
+ def finalize_variant(
261
+ self,
262
+ scores: variant_scoring.ScoreVariantResult,
263
+ *,
264
+ track_metadata: dna_output.OutputMetadata,
265
+ mask_metadata: pd.DataFrame,
266
+ settings: variant_scorers.SpliceJunctionScorer,
267
+ ) -> anndata.AnnData:
268
+ """See base class."""
269
+ track_metadata = track_metadata.get(settings.requested_output)
270
+
271
+ if mask_metadata.empty:
272
+ return _create_empty(mask_metadata, track_metadata)
273
+
274
+ delta_counts = scores['delta_counts']
275
+
276
+ interval = mask_metadata['interval'].values[0]
277
+ mask_metadata = mask_metadata.drop(columns=['interval'])
278
+
279
+ delta_counts = junction_predictions_to_dataframe(
280
+ np.abs(delta_counts, dtype=np.float32),
281
+ scores['splice_site_positions'],
282
+ metadata=track_metadata,
283
+ interval=interval,
284
+ )
285
+ if delta_counts.empty:
286
+ return _create_empty(mask_metadata, track_metadata)
287
+
288
+ junction_scores = (
289
+ pyranges.PyRanges(delta_counts)
290
+ .join(
291
+ pyranges.PyRanges(
292
+ mask_metadata.rename(columns={'strand': 'Strand'})
293
+ ),
294
+ strandedness='same',
295
+ )
296
+ .df
297
+ )
298
+
299
+ if not junction_scores.empty:
300
+ junction_scores = junction_scores[
301
+ (junction_scores['Start'] > junction_scores['Start_b'])
302
+ & (junction_scores['End'] < junction_scores['End_b'])
303
+ ]
304
+ return _create(junction_scores, mask_metadata, track_metadata)
305
+ else:
306
+ return _create_empty(mask_metadata, track_metadata)
flax_model/alphagenome/model/variant_scoring/variant_scoring_test.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 absl.testing import absltest
16
+ from absl.testing import parameterized
17
+ from flax_model.alphagenome._sdk.data import genome
18
+ from flax_model.alphagenome.model.variant_scoring import variant_scoring
19
+ import chex
20
+ import numpy as np
21
+
22
+
23
+ class AlignAlternateTest(parameterized.TestCase):
24
+
25
+ @parameterized.named_parameters(
26
+ dict(
27
+ testcase_name='snp',
28
+ variant=genome.Variant('chr1', 2, 'C', 'T'),
29
+ expected_aligned_alt=np.arange(10, dtype=np.float32).reshape(-1, 1),
30
+ ),
31
+ dict(
32
+ testcase_name='insertion',
33
+ variant=genome.Variant('chr1', 2, 'C', 'TGA'),
34
+ expected_aligned_alt=np.array(
35
+ [0, 3, 4, 5, 6, 7, 8, 9, 0, 0], dtype=np.float32
36
+ ).reshape(-1, 1),
37
+ ),
38
+ dict(
39
+ testcase_name='multi_insertion',
40
+ variant=genome.Variant('chr1', 2, 'CC', 'CCTGA'),
41
+ expected_aligned_alt=np.array(
42
+ [0, 1, 5, 6, 7, 8, 9, 0, 0, 0], dtype=np.float32
43
+ ).reshape(-1, 1),
44
+ ),
45
+ dict(
46
+ testcase_name='multi_insertion_long_insertion',
47
+ variant=genome.Variant('chr1', 5, 'A', 'AAAAAAA'),
48
+ expected_aligned_alt=np.array(
49
+ [0, 1, 2, 3, 9, 0, 0, 0, 0, 0], dtype=np.float32
50
+ ).reshape(-1, 1),
51
+ ),
52
+ dict(
53
+ testcase_name='deletion',
54
+ variant=genome.Variant('chr1', 2, 'CGA', 'C'),
55
+ expected_aligned_alt=np.array(
56
+ [0, 1, 0, 0, 2, 3, 4, 5, 6, 7], dtype=np.float32
57
+ ).reshape(-1, 1),
58
+ ),
59
+ dict(
60
+ testcase_name='multi_deletion',
61
+ variant=genome.Variant('chr1', 2, 'CCCGA', 'CCC'),
62
+ expected_aligned_alt=np.array(
63
+ [0, 1, 2, 3, 0, 0, 4, 5, 6, 7], dtype=np.float32
64
+ ).reshape(-1, 1),
65
+ ),
66
+ )
67
+ def test_align_alt(self, variant, expected_aligned_alt):
68
+ interval = genome.Interval('chr1', 0, 10)
69
+ alt = np.arange(10, dtype=np.float32).reshape(-1, 1)
70
+ aligned_alt = variant_scoring.align_alternate(alt, variant, interval)
71
+ chex.assert_shape(aligned_alt, (10, 1))
72
+ np.testing.assert_array_equal(aligned_alt, expected_aligned_alt)
73
+
74
+
75
+ if __name__ == '__main__':
76
+ absltest.main()
scripts/inference.sh ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
7
+
8
+ if [[ -n "${ONESCIENCE_DATASETS_DIR:-}" ]]; then
9
+ DATA_ROOT_DIR="${ONESCIENCE_DATASETS_DIR}/AlphaGenome"
10
+ else
11
+ DATA_ROOT_DIR="${PROJECT_DIR}/data"
12
+ fi
13
+
14
+ if [[ -n "${ONESCIENCE_MODELS_DIR:-}" ]]; then
15
+ MODEL_ROOT_DIR="${ONESCIENCE_MODELS_DIR}/AlphaGenome"
16
+ else
17
+ MODEL_ROOT_DIR="${PROJECT_DIR}/weight"
18
+ fi
19
+
20
+ export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH:-}"
21
+
22
+ python "${SCRIPT_DIR}/run_inference.py" \
23
+ --fasta_path "${DATA_ROOT_DIR}/reference/HOMO_SAPIENS/GRCh38.p13.genome.fa" \
24
+ --model_dir "${MODEL_ROOT_DIR}/alphagenome-all-folds" \
25
+ --chromosome chr19 \
26
+ --start 10587331 \
27
+ --end 11635907 \
28
+ --output_dir "${PROJECT_DIR}/outputs"
weight/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AlphaGenome Weights
2
+
3
+ Place local AlphaGenome Orbax checkpoints here when not using the shared OneScience model directory.
4
+
5
+ Expected local layout:
6
+
7
+ ```text
8
+ weight/
9
+ └── alphagenome-all-folds/
10
+ ├── _CHECKPOINT_METADATA
11
+ ├── _METADATA
12
+ └── ...
13
+ ```
weight/alphagenome-all-folds/README.md ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: alphagenome
4
+ license_link: https://deepmind.google.com/science/alphagenome/model-terms
5
+ extra_gated_heading: Access AlphaGenome on Hugging Face
6
+ extra_gated_prompt: >-
7
+ AlphaGenome is provided for non-commercial use only and is subject to the
8
+ [Model Terms of Use](https://deepmind.google.com/science/alphagenome/model-terms).
9
+ To accept terms, please login, complete the required fields and click Accept.
10
+ Requests are processed immediately.
11
+ extra_gated_button_content: Accept and continue
12
+ extra_gated_fields:
13
+ Organization, university, or other affiliation(s): text
14
+ language:
15
+ - en
16
+ tags:
17
+ - biology
18
+ ---
19
+
20
+ ## Description
21
+
22
+ AlphaGenome is a unified DNA sequence model designed to advance regulatory
23
+ variant-effect prediction and shed light on genome function. It analyzes DNA
24
+ sequences of up to 1 million base pairs to deliver predictions at single
25
+ base-pair resolution across diverse modalities, including gene expression,
26
+ splicing patterns, chromatin features, and contact maps.
27
+
28
+ The core architecture features a U-Net-style design that combines an encoder for
29
+ downsampling, transformers with inter-device communication to capture long-range
30
+ interactions, and a decoder for upsampling. These components feed into
31
+ task-specific output heads that generate predictions at their respective
32
+ assay-specific resolutions.
33
+
34
+ By achieving state-of-the-art performance across diverse genomic benchmarks,
35
+ AlphaGenome provides a robust framework for understanding the molecular function
36
+ of DNA sequences and interpreting non-coding variation.
37
+
38
+ ## Inputs and outputs
39
+
40
+ * **Input:** Up to 1 Mb (2\*\*20) one-hot encoded DNA sequence, and an
41
+ organism type index (representing human or mouse).
42
+ * **Output:** 11 diverse modalities, including:
43
+ * RNA expression (RNA-Seq, CAGE-seq and PRO-cap).
44
+ * Chromatin accessibility (DNase-seq and ATAC-seq).
45
+ * Histone modifications.
46
+ * Transcription factor binding.
47
+ * Chromatin contact maps.
48
+ * Splice sites and their usage.
49
+ * Splice junction coordinates and strength.
50
+
51
+ ## Citation
52
+
53
+ ```
54
+ @article{alphagenome,
55
+ title={Advancing regulatory variant effect prediction with {AlphaGenome}},
56
+ author={Avsec, {\v Z}iga and Latysheva, Natasha and Cheng, Jun and Novati, Guido and Taylor, Kyle R. and Ward, Tom and Bycroft, Clare and Nicolaisen, Lauren and Arvaniti, Eirini and Pan, Joshua and Thomas, Raina and Dutordoir, Vincent and Perino, Matteo and De, Soham and Karollus, Alexander and Gayoso, Adam and Sargeant, Toby and Mottram, Anne and Wong, Lai Hong and Drot{\'a}r, Pavol and Kosiorek, Adam and Senior, Andrew and Tanburn, Richard and Applebaum, Taylor and Basu, Souradeep and Hassabis, Demis and Kohli, Pushmeet},
57
+ journal={Nature},
58
+ volume={649},
59
+ number={8099},
60
+ year={2026},
61
+ doi={10.1038/s41586-025-10014-0},
62
+ publisher={Nature Publishing Group UK London}
63
+ }
64
+ ```
65
+
66
+ ## Installation
67
+
68
+ To install the accompanying code necessary to run the model, please run the
69
+ following:
70
+
71
+ ```shell
72
+ $ pip install git+https://github.com/google-deepmind/alphagenome_research.git
73
+ ```
74
+
75
+ ## Usage
76
+
77
+ In addition to the model, we provide a DNA model class that wraps the core model
78
+ and provides a more intuitive set of functions for creating predictions, scoring
79
+ variants, performing in silico mutagenesis (ISM) and more.
80
+
81
+ Here's an example of making a variant prediction:
82
+
83
+ ```python
84
+ from alphagenome.data import genome
85
+ from alphagenome.visualization import plot_components
86
+ from alphagenome_research.model import dna_model
87
+ import matplotlib.pyplot as plt
88
+
89
+ model = dna_model.create_from_huggingface('all_folds')
90
+
91
+ interval = genome.Interval(chromosome='chr22', start=35677410, end=36725986)
92
+ variant = genome.Variant(
93
+ chromosome='chr22',
94
+ position=36201698,
95
+ reference_bases='A',
96
+ alternate_bases='C',
97
+ )
98
+
99
+ outputs = model.predict_variant(
100
+ interval=interval,
101
+ variant=variant,
102
+ ontology_terms=['UBERON:0001157'],
103
+ requested_outputs=[dna_model.OutputType.RNA_SEQ],
104
+ )
105
+
106
+ plot_components.plot(
107
+ [
108
+ plot_components.OverlaidTracks(
109
+ tdata={
110
+ 'REF': outputs.reference.rna_seq,
111
+ 'ALT': outputs.alternate.rna_seq,
112
+ },
113
+ colors={'REF': 'dimgrey', 'ALT': 'red'},
114
+ ),
115
+ ],
116
+ interval=outputs.reference.rna_seq.interval.resize(2**15),
117
+ # Annotate the location of the variant as a vertical line.
118
+ annotations=[plot_components.VariantAnnotation([variant], alpha=0.8)],
119
+ )
120
+ plt.show()
121
+ ```
122
+
123
+ ## Model Data
124
+
125
+ AlphaGenome was trained to predict read coverage for a wide range of different
126
+ functional genomics assays – including RNA-seq, DNase-seq, CAGE, ChIP-seq
127
+ –&nbsp;directly from human and mouse reference genome sequences. The training
128
+ data was sourced from large public consortia including
129
+ [ENCODE](http://encodeproject.org/), [GTEx](https://www.gtexportal.org/),
130
+ [4D Nucleome](https://4dnucleome.org/) and
131
+ [FANTOM5](https://fantom.gsc.riken.jp/5/), encompassing experimental
132
+ measurements of key regulatory modalities across hundreds of cell types and
133
+ tissues. For more information, please refer to the
134
+ [AlphaGenome manuscript](https://www.biorxiv.org/content/10.1101/2025.06.25.661532v2).
135
+
136
+ Along with the release of the weights and model inference code, we are releasing
137
+ the complete training, validation and test datasets used for AlphaGenome. The
138
+ data is stored in compressed TFRecord format and can be loaded as follows:
139
+
140
+ ```python
141
+ from alphagenome.data import fold_intervals
142
+ from alphagenome_research.io import dataset
143
+ from alphagenome_research.model import dna_model
144
+
145
+ ds_iter = dataset.create_dataset(
146
+ organism=dna_model.Organism.HOMO_SAPIENS,
147
+ fold_split=dna_model.ModelVersion.ALL_FOLDS,
148
+ subset=fold_intervals.Subset.TRAIN,
149
+ ).as_numpy_iterator()
150
+ element = next(ds_iter)
151
+ ```
152
+
153
+ Each element in the dataset is a tuple, where each item corresponds to a
154
+ specific data bundle. The example below illustrates this structure, showing the
155
+ ATAC and DNase bundles alongside the input DNA sequence, masks, and interval
156
+ metadata:
157
+
158
+ ```python
159
+ ({"atac": "bfloat16[1052672,256]",
160
+ "atac_mask": "bool[1,256]",
161
+ "dna_sequence": "float32[1052672,4]",
162
+ "interval/chromosome": b"chr7",
163
+ "interval/end": "int64[]",
164
+ "interval/start": "int64[]"},
165
+ {"dna_sequence": "float32[1052672,4]",
166
+ "dnase": "bfloat16[1052672,384]",
167
+ "dnase_mask": "bool[1,384]",
168
+ "interval/chromosome": b"chr7",
169
+ "interval/end": "int64[]",
170
+ "interval/start": "int64[]"},
171
+ ...
172
+ )
173
+ ```
174
+
175
+ **Notes:**
176
+
177
+ * **Interval extension**: To support data augmentation via sequence shifting
178
+ during training, the 1 Mb intervals were extended by 4,096 base pairs (2,048
179
+ bp on each side).
180
+ * **GTEx data exclusion**: The released dataset does not contain GTEx tissue
181
+ data due to licensing restrictions, though the corresponding column headers
182
+ remain in the bundle’s metadata. However, users can easily extend the
183
+ dataset with GTEx or other external sources using the provided interval
184
+ metadata (chromosome, start, end).
185
+
186
+ ## Implementation Information
187
+
188
+ ### Hardware
189
+
190
+ AlphaGenome training involved a two-stage process: pre-training and
191
+ distillation. Pre-training was carried out on 256
192
+ [Tensor Processing Units (TPUv3)](https://docs.cloud.google.com/tpu/docs/v3)
193
+ using sequence parallelism across groups of 4 interconnected chips. We leverage
194
+ TPU pods (large clusters of TPUs) to provide a scalable solution for training,
195
+ to enable large batch sizes which can lead to better model quality. For
196
+ distillation, AlphaGenome was trained on 64 NVIDIA H100 GPUs without sequence
197
+ parallelism. The evaluation of all models was carried out on NVIDIA H100 GPUs
198
+ without sequence parallelism.
199
+
200
+ ### Software
201
+
202
+ Training was done using [JAX](https://github.com/google/jax) and
203
+ [JAXline](https://github.com/google-deepmind/jaxline).
204
+
205
+ JAX allows researchers to take advantage of the latest generation of hardware,
206
+ including TPUs, for faster and more efficient training of large models.
207
+
208
+ JAXline is a distributed JAX training and evaluation framework. It is designed
209
+ to be forked, covering only the most general aspects of experiment boilerplate.
210
+
211
+ ## Evaluation
212
+
213
+ AlphaGenome demonstrates state-of-the-art performance across a diverse set of
214
+ genomic prediction tasks. The model was evaluated using two primary approaches:
215
+
216
+ 1. **Genome Track Prediction**: Assessing the ability to predict functional
217
+ genomic signals (read coverage) on previously unseen DNA sequences (held-out
218
+ test intervals).
219
+ 2. **Variant Effect Prediction (VEP):** Assessing the ability to predict the
220
+ molecular consequences of genetic variants (e.g., single nucleotide
221
+ variants) by comparing predictions for reference and alternative alleles
222
+ against ground-truth datasets (e.g., experimental QTL effect sizes, readouts
223
+ from reporter assays).
224
+
225
+ Key Highlights:
226
+
227
+ * **Broad SOTA Performance:** AlphaGenome matched or outperformed the best
228
+ available external models on **22 out of 24** genome track prediction
229
+ evaluations.
230
+ * **Variant Interpretation:** For variant effect prediction, AlphaGenome
231
+ matched or exceeded top-performing external models on **25 out of 26**
232
+ evaluations.
233
+ * **Multimodal Capability:** Unlike specialized models, AlphaGenome jointly
234
+ predicts all assessed modalities –&nbsp;including splicing, expression,
235
+ accessibility, and 3D contact maps –&nbsp;within a single framework.
236
+ * **Single-Pass Efficiency:** The distilled student model achieves this
237
+ performance and broad coverage with a single inference pass, eliminating the
238
+ need for complex model ensembling.
239
+
240
+ The tables below detail the performance metrics for specific modalities and
241
+ tasks.
242
+
243
+ ## Benchmark Results
244
+
245
+ The following table focuses on the accuracy of the pre-trained, non-distilled
246
+ model in predicting genomic tracks on unseen sequences:
247
+
248
+ Modality | Evaluation | Metric | Resolution | Value | Baseline Model | Relative Improvement (%)
249
+ :-------------------- | :------------------------------- | :---------- | :--------- | :---- | :------------- | :-----------------------
250
+ **Splicing** | Splice site classification | auPRC | 1 bp | 0.79 | DeltaSplice | 1.0
251
+ &nbsp; | Splice site usage | Pearson r | 1 bp | 0.86 | DeltaSplice | 6.7
252
+ **RNA expression** | RNA-seq coverage | Pearson r | 1 bp | 0.59 | Borzoi | 28.2
253
+ &nbsp; | RNA-seq coverage | Pearson r | 32 bp | 0.78 | Borzoi | 4.6
254
+ &nbsp; | RNA-seq gene expr. LFC | Pearson r | Gene | 0.57 | Borzoi | 14.7
255
+ &nbsp; | CAGE coverage | Pearson r | 32 bp | 0.74 | Borzoi | 4.4
256
+ &nbsp; | CAGE coverage | Pearson r | 128 bp | 0.71 | Enformer | \-0.3
257
+ &nbsp; | Alternative PA | Spearman r | Gene | 0.87 | Borzoi | 13.1
258
+ **DNA accessibility** | DNase-seq coverage | Profile JSD | 1 bp | 0.51 | ChromBPNet | 6.4
259
+ &nbsp; | DNase-seq coverage | Pearson r | 32 bp | 0.86 | Borzoi | 4.7
260
+ &nbsp; | DNase-seq coverage | Pearson r | 128 bp | 0.87 | Enformer | 2.4
261
+ &nbsp; | ATAC-seq coverage | Profile JSD | 1 bp | 0.46 | ChromBPNet | 1.6
262
+ &nbsp; | ATAC-seq coverage | Pearson r | 32 bp | 0.57 | Borzoi | 3.4
263
+ &nbsp; | ATAC-seq coverage | Pearson r | 128 bp | 0.72 | Enformer | 2.7
264
+ **Histone mods** | Histone ChIP-seq coverage | Pearson r | 32 bp | 0.69 | Borzoi | 3.1
265
+ &nbsp; | Histone ChIP-seq coverage | Pearson r | 128 bp | 0.71 | Enformer | 2.4
266
+ **TF binding** | TF ChIP-seq coverage | Pearson r | 32 bp | 0.55 | Borzoi | 5.0
267
+ &nbsp; | TF ChIP-seq coverage | Pearson r | 128 bp | 0.58 | Enformer | 1.4
268
+ **DNA contact maps** | Orca Contact maps | Pearson r | 4000 bp | 0.79 | Orca | 6.3
269
+ &nbsp; | Orca Contact maps cell type diff | Pearson r | 4000 bp | 0.42 | Orca | 42.3
270
+
271
+ And the next table details the performance of the distilled model in predicting
272
+ the functional effects of genetic variants:
273
+
274
+ Modality | Evaluation | Type | Metric | Value | Baseline Model | Relative Improvement (%)
275
+ :-------------------- | :---------------------------- | :-------- | :--------- | :---- | :------------- | :-----------------------
276
+ **Splicing** | ClinVar splice site region | Causality | auPRC | 0.57 | Pangolin | 3.7
277
+ &nbsp; | ClinVar noncoding | \- | auPRC | 0.66 | Pangolin | 2.9
278
+ &nbsp; | ClinVar missense | \- | auPRC | 0.18 | DeltaSplice | 13.7
279
+ &nbsp; | Splicing outlier (zero-shot) | \- | auPRC | 0.22 | Pangolin | 59.1
280
+ &nbsp; | Splicing outlier (supervised) | \- | auPRC | 0.28 | AbSplice | 13.0
281
+ &nbsp; | sQTL | Causality | auPRC | 0.76 | Pangolin | 13.9
282
+ &nbsp; | MFASS | \- | auPRC | 0.51 | Pangolin | \-5.7
283
+ **RNA expression** | eQTL | Direction | Spearman r | 0.49 | Borzoi | 25.5
284
+ &nbsp; | eQTL (zero-shot) | Causality | auROC | 0.71 | Borzoi | 5.4
285
+ &nbsp; | eQTL (supervised) | Causality | auROC | 0.80 | Borzoi | 15.6
286
+ &nbsp; | ENCODE E2G (zero-shot) | \- | auPRC | 0.75 | Borzoi | 13.0
287
+ &nbsp; | paQTL | \- | auPRC | 0.63 | Borzoi | 7.3
288
+ **DNA accessibility** | CAGI5 MPRA | Causality | Pearson r | 0.65 | Borzoi | 6.3
289
+ &nbsp; | ds/caQTL | Direction | Pearson r | 0.70 | ChromBPNet | 7.7
290
+ &nbsp; | ds/caQTL | Causality | auPRC | 0.52 | Borzoi | 18.0
291
+ **TF binding** | bQTL | Direction | Pearson r | 0.55 | Borzoi | 2.8
292
+ &nbsp; | bQTL | Causality | auPRC | 0.50 | Borzoi | 6.0
293
+
294
+ ## Usage and Limitations
295
+
296
+ ### License
297
+
298
+ Unless required by applicable law or agreed to in writing, all software and
299
+ materials distributed here (under the
300
+ [Apache 2.0 License](https://github.com/google-deepmind/alphagenome_research/blob/main/README.md)
301
+ with respect to model code and
302
+ [non-commercial terms](https://deepmind.google.com/science/alphagenome/model-terms)
303
+ for the model parameters) are distributed on an "AS IS" BASIS, WITHOUT
304
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
305
+ licenses for the specific language governing permissions and limitations under
306
+ those licenses.
307
+
308
+ ### Intended usage
309
+
310
+ * Non-Commercial use only: The model parameters are restricted to
311
+ non-commercial use by non-commercial organizations (e.g., universities,
312
+ non-profits, research institutes, and journalism). It must not be used for
313
+ any commercial activities or on behalf of commercial entities.
314
+ * Model derivatives: If you fine-tune or modify this model, the resulting
315
+ model is classified as a "Derivative". All derivatives are subject to these
316
+ exact same terms, meaning strictly no commercial use is permitted for
317
+ fine-tuned versions.
318
+ * Distillation: Training a new model using the outputs or predictions of
319
+ AlphaGenome is also restricted; resulting models must be governed by the
320
+ AlphaGenome Model Parameters Terms of Use
321
+
322
+ ### Limitations
323
+
324
+ * Like other sequence-based models, accurately capturing the influence of very
325
+ distant regulatory elements, like those over 100,000 DNA letters away, is
326
+ still an ongoing challenge. Another priority for future work is further
327
+ increasing the model’s ability to capture cell- and tissue-specific
328
+ patterns. We haven't designed or validated AlphaGenome for personal genome
329
+ prediction, a known challenge for AI models. Instead, we focused more on
330
+ characterising the performance on individual genetic variants. And while
331
+ AlphaGenome can predict molecular outcomes, it doesn't give the full picture
332
+ of how genetic variations lead to complex traits or diseases. These often
333
+ involve broader biological processes, like developmental and environmental
334
+ factors, that are beyond the direct scope of our model.
weight/alphagenome-all-folds/_CHECKPOINT_METADATA ADDED
@@ -0,0 +1 @@
 
 
1
+ {"item_handlers": "orbax.checkpoint._src.handlers.standard_checkpoint_handler.StandardCheckpointHandler", "metrics": {}, "performance_metrics": {}, "init_timestamp_nsecs": 1759332751356271135, "commit_timestamp_nsecs": 1759332758258030815, "custom_metadata": {}}
weight/alphagenome-all-folds/_METADATA ADDED
The diff for this file is too large to render. See raw diff
 
weight/alphagenome-all-folds/d/549b1a1ce6bea654c08ac287872a5d99 ADDED
Binary file (77.3 kB). View file
 
weight/alphagenome-all-folds/gitattributes ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ocdbt.process_0/d/48900811ab32b5ad3873d40e96d63846 filter=lfs diff=lfs merge=lfs -text
37
+ ocdbt.process_0/d/6b08a83c9d6a2ade925dbcdd91299ce2 filter=lfs diff=lfs merge=lfs -text
38
+ ocdbt.process_0/d/bda173d4f00d3afcef0a043614df05a6 filter=lfs diff=lfs merge=lfs -text
weight/alphagenome-all-folds/manifest.ocdbt ADDED
Binary file (120 Bytes). View file
 
weight/alphagenome-all-folds/notebook.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
weight/alphagenome-all-folds/ocdbt.process_0/d/41569d1134215cd16b582168403f7509 ADDED
Binary file (2.31 kB). View file
 
weight/alphagenome-all-folds/ocdbt.process_0/manifest.ocdbt ADDED
Binary file (277 Bytes). View file