Add files using upload-large-folder tool
Browse files- flax_model/alphagenome/_sdk/colab_utils.py +62 -0
- flax_model/alphagenome/_sdk/data/fold_intervals.py +115 -0
- flax_model/alphagenome/_sdk/data/junction_data.py +272 -0
- flax_model/alphagenome/_sdk/data/ontology.py +122 -0
- flax_model/alphagenome/_sdk/data/track_data.py +918 -0
- flax_model/alphagenome/_sdk/data/transcript.py +755 -0
- flax_model/alphagenome/_sdk/interpretation/__init__.py +15 -0
- flax_model/alphagenome/_sdk/interpretation/ism.py +150 -0
- flax_model/alphagenome/_sdk/models/__init__.py +15 -0
- flax_model/alphagenome/_sdk/models/dna_client.py +907 -0
- flax_model/alphagenome/_sdk/models/dna_model.py +507 -0
- flax_model/alphagenome/_sdk/models/dna_output.py +435 -0
- flax_model/alphagenome/_sdk/models/junction_data_utils.py +216 -0
- flax_model/alphagenome/_sdk/models/track_data_utils.py +260 -0
- flax_model/alphagenome/_sdk/protos/__init__.py +15 -0
- flax_model/alphagenome/_sdk/protos/dna_model.proto +559 -0
- flax_model/alphagenome/_sdk/protos/dna_model_pb2.py +100 -0
- flax_model/alphagenome/_sdk/protos/dna_model_pb2_grpc.py +4 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service.proto +268 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service_pb2.py +57 -0
- flax_model/alphagenome/_sdk/protos/dna_model_service_pb2_grpc.py +274 -0
- flax_model/alphagenome/_sdk/protos/tensor.proto +104 -0
- flax_model/alphagenome/_sdk/protos/tensor_pb2.py +33 -0
- flax_model/alphagenome/_sdk/protos/tensor_pb2_grpc.py +4 -0
- flax_model/alphagenome/_sdk/tensor_utils.py +171 -0
- flax_model/alphagenome/_sdk/visualization/plot.py +569 -0
- flax_model/alphagenome/_sdk/visualization/plot_components.py +1555 -0
- flax_model/alphagenome/_sdk/visualization/plot_transcripts.py +480 -0
- flax_model/alphagenome/finetuning/__init__.py +15 -0
- flax_model/alphagenome/finetuning/dataset.py +236 -0
- flax_model/alphagenome/finetuning/dataset_test.py +198 -0
- flax_model/alphagenome/finetuning/finetune.py +153 -0
- flax_model/alphagenome/finetuning/finetune_test.py +179 -0
- flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_HOMO_SAPIENS.textproto +0 -0
- flax_model/alphagenome/model/metadata/OutputMetadataResponse_ORGANISM_MUS_MUSCULUS.textproto +0 -0
- flax_model/alphagenome/model/metadata/metadata_test.py +215 -0
- flax_model/alphagenome/model/variant_scoring/__init__.py +15 -0
- flax_model/alphagenome/model/variant_scoring/splice_junction.py +306 -0
- flax_model/alphagenome/model/variant_scoring/variant_scoring_test.py +76 -0
- scripts/inference.sh +28 -0
- weight/README.md +13 -0
- weight/alphagenome-all-folds/README.md +334 -0
- weight/alphagenome-all-folds/_CHECKPOINT_METADATA +1 -0
- weight/alphagenome-all-folds/_METADATA +0 -0
- weight/alphagenome-all-folds/d/549b1a1ce6bea654c08ac287872a5d99 +0 -0
- weight/alphagenome-all-folds/gitattributes +38 -0
- weight/alphagenome-all-folds/manifest.ocdbt +0 -0
- weight/alphagenome-all-folds/notebook.ipynb +0 -0
- weight/alphagenome-all-folds/ocdbt.process_0/d/41569d1134215cd16b582168403f7509 +0 -0
- 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 |
+
– 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 – including splicing, expression,
|
| 235 |
+
accessibility, and 3D contact maps – 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 |
+
| 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 |
+
| RNA-seq coverage | Pearson r | 32 bp | 0.78 | Borzoi | 4.6
|
| 254 |
+
| RNA-seq gene expr. LFC | Pearson r | Gene | 0.57 | Borzoi | 14.7
|
| 255 |
+
| CAGE coverage | Pearson r | 32 bp | 0.74 | Borzoi | 4.4
|
| 256 |
+
| CAGE coverage | Pearson r | 128 bp | 0.71 | Enformer | \-0.3
|
| 257 |
+
| 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 |
+
| DNase-seq coverage | Pearson r | 32 bp | 0.86 | Borzoi | 4.7
|
| 260 |
+
| DNase-seq coverage | Pearson r | 128 bp | 0.87 | Enformer | 2.4
|
| 261 |
+
| ATAC-seq coverage | Profile JSD | 1 bp | 0.46 | ChromBPNet | 1.6
|
| 262 |
+
| ATAC-seq coverage | Pearson r | 32 bp | 0.57 | Borzoi | 3.4
|
| 263 |
+
| 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 |
+
| 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 |
+
| 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 |
+
| 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 |
+
| ClinVar noncoding | \- | auPRC | 0.66 | Pangolin | 2.9
|
| 278 |
+
| ClinVar missense | \- | auPRC | 0.18 | DeltaSplice | 13.7
|
| 279 |
+
| Splicing outlier (zero-shot) | \- | auPRC | 0.22 | Pangolin | 59.1
|
| 280 |
+
| Splicing outlier (supervised) | \- | auPRC | 0.28 | AbSplice | 13.0
|
| 281 |
+
| sQTL | Causality | auPRC | 0.76 | Pangolin | 13.9
|
| 282 |
+
| MFASS | \- | auPRC | 0.51 | Pangolin | \-5.7
|
| 283 |
+
**RNA expression** | eQTL | Direction | Spearman r | 0.49 | Borzoi | 25.5
|
| 284 |
+
| eQTL (zero-shot) | Causality | auROC | 0.71 | Borzoi | 5.4
|
| 285 |
+
| eQTL (supervised) | Causality | auROC | 0.80 | Borzoi | 15.6
|
| 286 |
+
| ENCODE E2G (zero-shot) | \- | auPRC | 0.75 | Borzoi | 13.0
|
| 287 |
+
| paQTL | \- | auPRC | 0.63 | Borzoi | 7.3
|
| 288 |
+
**DNA accessibility** | CAGI5 MPRA | Causality | Pearson r | 0.65 | Borzoi | 6.3
|
| 289 |
+
| ds/caQTL | Direction | Pearson r | 0.70 | ChromBPNet | 7.7
|
| 290 |
+
| ds/caQTL | Causality | auPRC | 0.52 | Borzoi | 18.0
|
| 291 |
+
**TF binding** | bQTL | Direction | Pearson r | 0.55 | Borzoi | 2.8
|
| 292 |
+
| 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
|
|
|