dnakit 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
"""Lazy optional backends for extracting sequence-level DNA representations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
|
|
15
|
+
from dnakit.exceptions import BackendExecutionError, BackendUnavailableError, ConfigurationError
|
|
16
|
+
|
|
17
|
+
from .models import DNAEmbeddingModel, RepresentationConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RepresentationBackend(Protocol):
|
|
21
|
+
"""Backend contract used by :func:`extract_representations`."""
|
|
22
|
+
|
|
23
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
24
|
+
"""Return one finite numeric vector per input sequence."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _missing_dependency(spec: DNAEmbeddingModel, package: str) -> BackendUnavailableError:
|
|
28
|
+
requirement = spec.required_package or package
|
|
29
|
+
return BackendUnavailableError(
|
|
30
|
+
f"The {spec.display_name} representation backend is not installed.",
|
|
31
|
+
code="MISSING_NEURAL_DEPENDENCY",
|
|
32
|
+
context={"model": spec.name, "dependency": requirement},
|
|
33
|
+
hint=f"Install the required backend: {requirement}",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _require_module(name: str, spec: DNAEmbeddingModel) -> Any:
|
|
38
|
+
try:
|
|
39
|
+
return importlib.import_module(name)
|
|
40
|
+
except ImportError as exc:
|
|
41
|
+
raise _missing_dependency(spec, name) from exc
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _progress(values: Sequence[str], *, enabled: bool, description: str) -> Iterable[str]:
|
|
45
|
+
if not enabled:
|
|
46
|
+
return values
|
|
47
|
+
from rich.progress import track
|
|
48
|
+
|
|
49
|
+
return track(values, description=description)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prepend_source_path(value: str | None) -> None:
|
|
53
|
+
if value is None:
|
|
54
|
+
return
|
|
55
|
+
root = Path(value).expanduser().resolve()
|
|
56
|
+
for candidate in (root, root / "src"):
|
|
57
|
+
if not candidate.is_dir():
|
|
58
|
+
continue
|
|
59
|
+
candidate_text = str(candidate)
|
|
60
|
+
if candidate_text not in sys.path:
|
|
61
|
+
sys.path.insert(0, candidate_text)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve_torch_device(torch: Any, requested: str) -> Any:
|
|
65
|
+
normalized = requested.strip().lower()
|
|
66
|
+
if normalized == "auto":
|
|
67
|
+
if bool(torch.cuda.is_available()):
|
|
68
|
+
normalized = "cuda"
|
|
69
|
+
elif hasattr(torch.backends, "mps") and bool(torch.backends.mps.is_available()):
|
|
70
|
+
normalized = "mps"
|
|
71
|
+
else:
|
|
72
|
+
normalized = "cpu"
|
|
73
|
+
try:
|
|
74
|
+
device = torch.device(normalized)
|
|
75
|
+
except (RuntimeError, TypeError) as exc:
|
|
76
|
+
raise ConfigurationError(
|
|
77
|
+
"Invalid PyTorch device.",
|
|
78
|
+
code="INVALID_REPRESENTATION_DEVICE",
|
|
79
|
+
context={"device": requested},
|
|
80
|
+
) from exc
|
|
81
|
+
if device.type == "cuda" and not bool(torch.cuda.is_available()):
|
|
82
|
+
raise ConfigurationError(
|
|
83
|
+
"Requested CUDA device is unavailable.",
|
|
84
|
+
code="INVALID_REPRESENTATION_DEVICE",
|
|
85
|
+
context={"device": requested},
|
|
86
|
+
)
|
|
87
|
+
if device.type == "mps" and (
|
|
88
|
+
not hasattr(torch.backends, "mps") or not bool(torch.backends.mps.is_available())
|
|
89
|
+
):
|
|
90
|
+
raise ConfigurationError(
|
|
91
|
+
"Requested MPS device is unavailable.",
|
|
92
|
+
code="INVALID_REPRESENTATION_DEVICE",
|
|
93
|
+
context={"device": requested},
|
|
94
|
+
)
|
|
95
|
+
return device
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _torch_dtype(torch: Any, value: str) -> Any | None:
|
|
99
|
+
if value == "auto":
|
|
100
|
+
return None
|
|
101
|
+
return {
|
|
102
|
+
"float32": torch.float32,
|
|
103
|
+
"float16": torch.float16,
|
|
104
|
+
"bfloat16": torch.bfloat16,
|
|
105
|
+
}[value]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_hidden_states(outputs: Any, torch: Any) -> Any:
|
|
109
|
+
candidate = getattr(outputs, "last_hidden_state", None)
|
|
110
|
+
if candidate is not None:
|
|
111
|
+
return candidate
|
|
112
|
+
hidden_states = getattr(outputs, "hidden_states", None)
|
|
113
|
+
if hidden_states:
|
|
114
|
+
return hidden_states[-1]
|
|
115
|
+
if isinstance(outputs, tuple) and outputs and isinstance(outputs[0], torch.Tensor):
|
|
116
|
+
return outputs[0]
|
|
117
|
+
raise BackendExecutionError(
|
|
118
|
+
"Model output does not contain hidden representations.",
|
|
119
|
+
code="MISSING_MODEL_HIDDEN_STATES",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _pool_hidden_states(
|
|
124
|
+
hidden_states: Any,
|
|
125
|
+
*,
|
|
126
|
+
torch: Any,
|
|
127
|
+
pooling: str,
|
|
128
|
+
attention_mask: Any | None = None,
|
|
129
|
+
special_tokens_mask: Any | None = None,
|
|
130
|
+
) -> Any:
|
|
131
|
+
if hidden_states.ndim != 3:
|
|
132
|
+
raise BackendExecutionError(
|
|
133
|
+
"Expected hidden states with shape (batch, tokens, dimensions).",
|
|
134
|
+
code="INVALID_MODEL_HIDDEN_STATES",
|
|
135
|
+
context={"shape": tuple(int(value) for value in hidden_states.shape)},
|
|
136
|
+
)
|
|
137
|
+
if attention_mask is None:
|
|
138
|
+
mask = torch.ones(
|
|
139
|
+
hidden_states.shape[:2],
|
|
140
|
+
device=hidden_states.device,
|
|
141
|
+
dtype=torch.bool,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
mask = attention_mask.to(hidden_states.device).bool()
|
|
145
|
+
if special_tokens_mask is not None:
|
|
146
|
+
without_special = mask & (~special_tokens_mask.to(hidden_states.device).bool())
|
|
147
|
+
if without_special.any(dim=1).all():
|
|
148
|
+
mask = without_special
|
|
149
|
+
if pooling == "cls":
|
|
150
|
+
return hidden_states[:, 0, :]
|
|
151
|
+
if pooling == "last":
|
|
152
|
+
positions = torch.arange(hidden_states.shape[1], device=hidden_states.device)[None, :]
|
|
153
|
+
indices = positions.masked_fill(~mask, -1).max(dim=1).values.clamp_min(0)
|
|
154
|
+
gather_index = indices[:, None, None].expand(-1, 1, hidden_states.shape[-1])
|
|
155
|
+
return hidden_states.gather(1, gather_index).squeeze(1)
|
|
156
|
+
weights = mask.to(hidden_states.dtype).unsqueeze(-1)
|
|
157
|
+
if pooling == "mean":
|
|
158
|
+
return (hidden_states * weights).sum(dim=1) / weights.sum(dim=1).clamp_min(1.0)
|
|
159
|
+
if pooling == "max":
|
|
160
|
+
minimum = torch.finfo(hidden_states.dtype).min
|
|
161
|
+
return hidden_states.masked_fill(~mask.unsqueeze(-1), minimum).max(dim=1).values
|
|
162
|
+
raise AssertionError(f"Unsupported pooling method: {pooling}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _module_from_file(module_name: str, file_path: Path) -> Any:
|
|
166
|
+
module_spec = importlib.util.spec_from_file_location(module_name, str(file_path))
|
|
167
|
+
if module_spec is None or module_spec.loader is None:
|
|
168
|
+
raise ImportError(f"Could not construct module {module_name} from {file_path}.")
|
|
169
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
170
|
+
sys.modules[module_name] = module
|
|
171
|
+
module_spec.loader.exec_module(module)
|
|
172
|
+
return module
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _unwrap_base_model(model: Any, torch: Any) -> Any:
|
|
176
|
+
for name in ("esm", "bert", "roberta", "transformer", "base_model", "model"):
|
|
177
|
+
candidate = getattr(model, name, None)
|
|
178
|
+
if isinstance(candidate, torch.nn.Module):
|
|
179
|
+
return candidate
|
|
180
|
+
return model
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _disable_fast_paths(model: Any, spec: DNAEmbeddingModel) -> None:
|
|
184
|
+
if spec.name == "dnabert2":
|
|
185
|
+
for module in tuple(sys.modules.values()):
|
|
186
|
+
module_file = getattr(module, "__file__", "") or ""
|
|
187
|
+
if spec.cache_name not in module_file and "DNABERT-2-117M" not in module_file:
|
|
188
|
+
continue
|
|
189
|
+
if hasattr(module, "flash_attn_qkvpacked_func"):
|
|
190
|
+
module.__dict__["flash_attn_qkvpacked_func"] = None
|
|
191
|
+
if spec.name == "caduceus":
|
|
192
|
+
for module in model.modules():
|
|
193
|
+
if hasattr(module, "use_fast_path"):
|
|
194
|
+
with suppress(AttributeError, RuntimeError):
|
|
195
|
+
module.use_fast_path = False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _TransformersBackend:
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
spec: DNAEmbeddingModel,
|
|
202
|
+
checkpoint: Path,
|
|
203
|
+
config: RepresentationConfig,
|
|
204
|
+
) -> None:
|
|
205
|
+
self.spec = spec
|
|
206
|
+
self.checkpoint = checkpoint
|
|
207
|
+
self.config = config
|
|
208
|
+
self.torch = _require_module("torch", spec)
|
|
209
|
+
transformers = _require_module("transformers", spec)
|
|
210
|
+
self.device = _resolve_torch_device(self.torch, config.device)
|
|
211
|
+
self.tokenizer = self._load_tokenizer(transformers)
|
|
212
|
+
self.model = self._load_model(transformers)
|
|
213
|
+
requested_dtype = _torch_dtype(self.torch, config.dtype)
|
|
214
|
+
if requested_dtype is not None:
|
|
215
|
+
self.model = self.model.to(dtype=requested_dtype)
|
|
216
|
+
self.model = self.model.to(self.device)
|
|
217
|
+
self.model.eval()
|
|
218
|
+
self.window_length = self._window_length()
|
|
219
|
+
|
|
220
|
+
def _load_tokenizer(self, transformers: Any) -> Any:
|
|
221
|
+
if self.spec.name == "caduceus":
|
|
222
|
+
module = _module_from_file(
|
|
223
|
+
f"_dnakit_caduceus_tokenizer_{abs(hash(self.checkpoint))}",
|
|
224
|
+
self.checkpoint / "tokenization_caduceus.py",
|
|
225
|
+
)
|
|
226
|
+
tokenizer_class = module.CaduceusTokenizer
|
|
227
|
+
return tokenizer_class(
|
|
228
|
+
model_max_length=int(self.config.max_length or self.spec.chunk_length),
|
|
229
|
+
padding_side="right",
|
|
230
|
+
)
|
|
231
|
+
tokenizer = transformers.AutoTokenizer.from_pretrained(
|
|
232
|
+
str(self.checkpoint),
|
|
233
|
+
trust_remote_code=self.spec.trust_remote_code,
|
|
234
|
+
local_files_only=True,
|
|
235
|
+
)
|
|
236
|
+
tokenizer.padding_side = "right"
|
|
237
|
+
if getattr(tokenizer, "pad_token_id", None) is None:
|
|
238
|
+
fallback = (
|
|
239
|
+
getattr(tokenizer, "eos_token", None)
|
|
240
|
+
or getattr(tokenizer, "sep_token", None)
|
|
241
|
+
or getattr(tokenizer, "cls_token", None)
|
|
242
|
+
or getattr(tokenizer, "unk_token", None)
|
|
243
|
+
)
|
|
244
|
+
if fallback is None:
|
|
245
|
+
raise BackendExecutionError(
|
|
246
|
+
"The selected tokenizer has no usable padding token.",
|
|
247
|
+
code="MODEL_LOAD_FAILED",
|
|
248
|
+
context={"model": self.spec.name},
|
|
249
|
+
)
|
|
250
|
+
tokenizer.pad_token = fallback
|
|
251
|
+
return tokenizer
|
|
252
|
+
|
|
253
|
+
def _load_model(self, transformers: Any) -> Any:
|
|
254
|
+
errors: list[str] = []
|
|
255
|
+
model_config = None
|
|
256
|
+
if self.spec.name == "caduceus":
|
|
257
|
+
try:
|
|
258
|
+
model_config = transformers.AutoConfig.from_pretrained(
|
|
259
|
+
str(self.checkpoint),
|
|
260
|
+
trust_remote_code=True,
|
|
261
|
+
local_files_only=True,
|
|
262
|
+
)
|
|
263
|
+
model_config.fused_add_norm = False
|
|
264
|
+
except Exception as exc: # third-party configuration errors are reported together.
|
|
265
|
+
errors.append(f"AutoConfig: {type(exc).__name__}: {exc}")
|
|
266
|
+
kwargs = {
|
|
267
|
+
"trust_remote_code": self.spec.trust_remote_code,
|
|
268
|
+
"local_files_only": True,
|
|
269
|
+
}
|
|
270
|
+
if model_config is not None:
|
|
271
|
+
kwargs["config"] = model_config
|
|
272
|
+
for loader_name in ("AutoModel", "AutoModelForMaskedLM"):
|
|
273
|
+
loader = getattr(transformers, loader_name)
|
|
274
|
+
try:
|
|
275
|
+
model = loader.from_pretrained(str(self.checkpoint), **kwargs)
|
|
276
|
+
if loader_name == "AutoModelForMaskedLM":
|
|
277
|
+
model = _unwrap_base_model(model, self.torch)
|
|
278
|
+
_disable_fast_paths(model, self.spec)
|
|
279
|
+
return model
|
|
280
|
+
except Exception as exc: # preserve both official loader diagnostics.
|
|
281
|
+
errors.append(f"{loader_name}: {type(exc).__name__}: {exc}")
|
|
282
|
+
raise BackendExecutionError(
|
|
283
|
+
"Could not load the Transformers checkpoint.",
|
|
284
|
+
code="MODEL_LOAD_FAILED",
|
|
285
|
+
context={"model": self.spec.name, "errors": tuple(errors)},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def _window_length(self) -> int:
|
|
289
|
+
configured = int(self.config.max_length or self.spec.chunk_length)
|
|
290
|
+
model_max = getattr(self.tokenizer, "model_max_length", configured)
|
|
291
|
+
try:
|
|
292
|
+
model_max_int = int(model_max)
|
|
293
|
+
except (TypeError, ValueError, OverflowError):
|
|
294
|
+
model_max_int = configured
|
|
295
|
+
if not 0 < model_max_int <= 10_000_000:
|
|
296
|
+
model_max_int = configured
|
|
297
|
+
special_count = 0
|
|
298
|
+
with suppress(AttributeError, TypeError, ValueError):
|
|
299
|
+
special_count = int(self.tokenizer.num_special_tokens_to_add(pair=False))
|
|
300
|
+
return max(1, min(configured, model_max_int) - special_count)
|
|
301
|
+
|
|
302
|
+
def _special_token_count(self) -> int:
|
|
303
|
+
method = getattr(self.tokenizer, "num_special_tokens_to_add", None)
|
|
304
|
+
if method is None:
|
|
305
|
+
return 0
|
|
306
|
+
try:
|
|
307
|
+
return int(method(pair=False))
|
|
308
|
+
except (TypeError, ValueError):
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
def _chunks(self, sequence: str) -> tuple[list[Any], str]:
|
|
312
|
+
if self.spec.chunk_unit == "bp":
|
|
313
|
+
bp_chunks: list[Any] = [
|
|
314
|
+
sequence[start : start + self.window_length]
|
|
315
|
+
for start in range(0, len(sequence), self.window_length)
|
|
316
|
+
]
|
|
317
|
+
return bp_chunks, "bp"
|
|
318
|
+
encoded = self.tokenizer(
|
|
319
|
+
sequence,
|
|
320
|
+
add_special_tokens=False,
|
|
321
|
+
return_attention_mask=False,
|
|
322
|
+
return_token_type_ids=False,
|
|
323
|
+
)
|
|
324
|
+
token_ids = list(encoded["input_ids"])
|
|
325
|
+
if not token_ids:
|
|
326
|
+
raise BackendExecutionError(
|
|
327
|
+
"The selected tokenizer produced no tokens for a non-empty sequence.",
|
|
328
|
+
code="EMPTY_MODEL_TOKENIZATION",
|
|
329
|
+
context={"model": self.spec.name},
|
|
330
|
+
)
|
|
331
|
+
token_chunks: list[Any] = [
|
|
332
|
+
token_ids[start : start + self.window_length]
|
|
333
|
+
for start in range(0, len(token_ids), self.window_length)
|
|
334
|
+
]
|
|
335
|
+
return token_chunks, "token"
|
|
336
|
+
|
|
337
|
+
def _tokenize(self, chunks: list[Any], kind: str) -> dict[str, Any]:
|
|
338
|
+
if kind == "bp":
|
|
339
|
+
kwargs = {
|
|
340
|
+
"add_special_tokens": True,
|
|
341
|
+
"padding": True,
|
|
342
|
+
"truncation": True,
|
|
343
|
+
"max_length": self.window_length + self._special_token_count(),
|
|
344
|
+
"return_attention_mask": True,
|
|
345
|
+
"return_special_tokens_mask": True,
|
|
346
|
+
"return_tensors": "pt",
|
|
347
|
+
}
|
|
348
|
+
try:
|
|
349
|
+
batch = self.tokenizer(chunks, **kwargs)
|
|
350
|
+
except (TypeError, NotImplementedError):
|
|
351
|
+
kwargs.pop("return_special_tokens_mask")
|
|
352
|
+
batch = self.tokenizer(chunks, **kwargs)
|
|
353
|
+
else:
|
|
354
|
+
prepared = [
|
|
355
|
+
self.tokenizer.prepare_for_model(
|
|
356
|
+
token_ids,
|
|
357
|
+
add_special_tokens=True,
|
|
358
|
+
return_attention_mask=True,
|
|
359
|
+
return_token_type_ids=False,
|
|
360
|
+
truncation=True,
|
|
361
|
+
max_length=self.window_length + self._special_token_count(),
|
|
362
|
+
)
|
|
363
|
+
for token_ids in chunks
|
|
364
|
+
]
|
|
365
|
+
batch = self.tokenizer.pad(prepared, padding=True, return_tensors="pt")
|
|
366
|
+
values = dict(batch)
|
|
367
|
+
values.pop("token_type_ids", None)
|
|
368
|
+
if "special_tokens_mask" not in values:
|
|
369
|
+
special_ids = tuple(getattr(self.tokenizer, "all_special_ids", ()) or ())
|
|
370
|
+
special_mask = self.torch.zeros_like(values["input_ids"], dtype=self.torch.long)
|
|
371
|
+
for token_id in special_ids:
|
|
372
|
+
special_mask |= values["input_ids"] == int(token_id)
|
|
373
|
+
values["special_tokens_mask"] = special_mask.long()
|
|
374
|
+
return values
|
|
375
|
+
|
|
376
|
+
def _encode_chunk_batch(self, chunks: list[Any], kind: str) -> Any:
|
|
377
|
+
batch = self._tokenize(chunks, kind)
|
|
378
|
+
special_tokens_mask = batch.pop("special_tokens_mask", None)
|
|
379
|
+
inputs = {
|
|
380
|
+
key: value.to(self.device)
|
|
381
|
+
for key, value in batch.items()
|
|
382
|
+
if isinstance(value, self.torch.Tensor)
|
|
383
|
+
}
|
|
384
|
+
with self.torch.inference_mode():
|
|
385
|
+
try:
|
|
386
|
+
outputs = self.model(**inputs)
|
|
387
|
+
except TypeError as exc:
|
|
388
|
+
if "attention_mask" not in str(exc):
|
|
389
|
+
raise
|
|
390
|
+
outputs = self.model(input_ids=inputs["input_ids"])
|
|
391
|
+
hidden_states = _resolve_hidden_states(outputs, self.torch)
|
|
392
|
+
return _pool_hidden_states(
|
|
393
|
+
hidden_states,
|
|
394
|
+
torch=self.torch,
|
|
395
|
+
pooling=self.config.pooling,
|
|
396
|
+
attention_mask=inputs.get("attention_mask"),
|
|
397
|
+
special_tokens_mask=(
|
|
398
|
+
None if special_tokens_mask is None else special_tokens_mask.to(self.device)
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _encode_sequence(self, sequence: str) -> Any:
|
|
403
|
+
chunks, kind = self._chunks(sequence)
|
|
404
|
+
vectors: list[Any] = []
|
|
405
|
+
for start in range(0, len(chunks), self.config.batch_size):
|
|
406
|
+
vectors.append(
|
|
407
|
+
self._encode_chunk_batch(chunks[start : start + self.config.batch_size], kind)
|
|
408
|
+
)
|
|
409
|
+
return self.torch.cat(vectors, dim=0).mean(dim=0)
|
|
410
|
+
|
|
411
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
412
|
+
vectors = [
|
|
413
|
+
self._encode_sequence(sequence)
|
|
414
|
+
for sequence in _progress(
|
|
415
|
+
sequences,
|
|
416
|
+
enabled=show_progress,
|
|
417
|
+
description=f"Extracting {self.spec.display_name} rep",
|
|
418
|
+
)
|
|
419
|
+
]
|
|
420
|
+
return self.torch.stack(vectors, dim=0).detach().float().cpu().numpy()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class _EnformerBackend:
|
|
424
|
+
def __init__(
|
|
425
|
+
self,
|
|
426
|
+
spec: DNAEmbeddingModel,
|
|
427
|
+
checkpoint: Path,
|
|
428
|
+
config: RepresentationConfig,
|
|
429
|
+
) -> None:
|
|
430
|
+
self.spec = spec
|
|
431
|
+
self.config = config
|
|
432
|
+
self.torch = _require_module("torch", spec)
|
|
433
|
+
enformer_pytorch = _require_module("enformer_pytorch", spec)
|
|
434
|
+
self.device = _resolve_torch_device(self.torch, config.device)
|
|
435
|
+
try:
|
|
436
|
+
self.model = enformer_pytorch.from_pretrained(
|
|
437
|
+
str(checkpoint),
|
|
438
|
+
)
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
raise BackendExecutionError(
|
|
441
|
+
"Could not load the Enformer checkpoint.",
|
|
442
|
+
code="MODEL_LOAD_FAILED",
|
|
443
|
+
context={"model": spec.name},
|
|
444
|
+
) from exc
|
|
445
|
+
# ``enformer_pytorch.from_pretrained`` applies this correction only when
|
|
446
|
+
# it receives the remote repository ID. DNAKit intentionally loads from
|
|
447
|
+
# the resolved local directory, so enable the same official setting here.
|
|
448
|
+
for module in self.model.modules():
|
|
449
|
+
if hasattr(module, "use_tf_gamma"):
|
|
450
|
+
module.use_tf_gamma = True
|
|
451
|
+
dtype = _torch_dtype(self.torch, config.dtype)
|
|
452
|
+
if dtype is not None:
|
|
453
|
+
self.model = self.model.to(dtype=dtype)
|
|
454
|
+
self.model = self.model.to(self.device).eval()
|
|
455
|
+
self.chunk_length = int(config.max_length or spec.chunk_length)
|
|
456
|
+
lookup = self.torch.zeros(256, 4, dtype=self.torch.float32, device=self.device)
|
|
457
|
+
for base, vector in {
|
|
458
|
+
"A": (1.0, 0.0, 0.0, 0.0),
|
|
459
|
+
"C": (0.0, 1.0, 0.0, 0.0),
|
|
460
|
+
"G": (0.0, 0.0, 1.0, 0.0),
|
|
461
|
+
"T": (0.0, 0.0, 0.0, 1.0),
|
|
462
|
+
"N": (0.0, 0.0, 0.0, 0.0),
|
|
463
|
+
}.items():
|
|
464
|
+
lookup[ord(base)] = self.torch.tensor(vector, device=self.device)
|
|
465
|
+
self.lookup = lookup
|
|
466
|
+
|
|
467
|
+
def _encode_sequence(self, sequence: str) -> Any:
|
|
468
|
+
raw_chunks = [
|
|
469
|
+
sequence[start : start + self.chunk_length]
|
|
470
|
+
for start in range(0, len(sequence), self.chunk_length)
|
|
471
|
+
]
|
|
472
|
+
chunks = [chunk + "N" * (self.chunk_length - len(chunk)) for chunk in raw_chunks]
|
|
473
|
+
vectors: list[Any] = []
|
|
474
|
+
for start in range(0, len(chunks), self.config.batch_size):
|
|
475
|
+
batch_chunks = chunks[start : start + self.config.batch_size]
|
|
476
|
+
encoded = self.torch.tensor(
|
|
477
|
+
[list(chunk.encode("ascii")) for chunk in batch_chunks],
|
|
478
|
+
dtype=self.torch.long,
|
|
479
|
+
device=self.device,
|
|
480
|
+
)
|
|
481
|
+
one_hot = self.lookup[encoded]
|
|
482
|
+
with self.torch.inference_mode():
|
|
483
|
+
hidden = self.model(one_hot, return_only_embeddings=True)
|
|
484
|
+
vectors.append(
|
|
485
|
+
_pool_hidden_states(
|
|
486
|
+
hidden,
|
|
487
|
+
torch=self.torch,
|
|
488
|
+
pooling=self.config.pooling,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
return self.torch.cat(vectors, dim=0).mean(dim=0)
|
|
492
|
+
|
|
493
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
494
|
+
vectors = [
|
|
495
|
+
self._encode_sequence(sequence)
|
|
496
|
+
for sequence in _progress(
|
|
497
|
+
sequences,
|
|
498
|
+
enabled=show_progress,
|
|
499
|
+
description="Extracting Enformer rep",
|
|
500
|
+
)
|
|
501
|
+
]
|
|
502
|
+
return self.torch.stack(vectors).detach().float().cpu().numpy()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _clean_janusdna_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
506
|
+
cleaned = dict(config)
|
|
507
|
+
cleaned.pop("_target_", None)
|
|
508
|
+
for key in (
|
|
509
|
+
"bidirectional",
|
|
510
|
+
"bidirectional_weight_tie",
|
|
511
|
+
"layer_fusion",
|
|
512
|
+
"final_attention",
|
|
513
|
+
"mid_single_direction_attention",
|
|
514
|
+
"bidirectional_attn_tie",
|
|
515
|
+
"gradient_checkpointing",
|
|
516
|
+
"output_router_logits",
|
|
517
|
+
):
|
|
518
|
+
value = cleaned.get(key)
|
|
519
|
+
if isinstance(value, str):
|
|
520
|
+
cleaned[key] = value.lower().strip(", ") == "true"
|
|
521
|
+
cleaned.update(
|
|
522
|
+
{
|
|
523
|
+
"gradient_checkpointing": False,
|
|
524
|
+
"output_router_logits": False,
|
|
525
|
+
"use_cache": False,
|
|
526
|
+
"use_mamba_kernels": False,
|
|
527
|
+
"attn_implementation": "eager",
|
|
528
|
+
"final_attention_class": "eager",
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
return cleaned
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class _JanusDNABackend:
|
|
535
|
+
def __init__(
|
|
536
|
+
self,
|
|
537
|
+
spec: DNAEmbeddingModel,
|
|
538
|
+
checkpoint: Path,
|
|
539
|
+
config: RepresentationConfig,
|
|
540
|
+
) -> None:
|
|
541
|
+
self.spec = spec
|
|
542
|
+
self.config = config
|
|
543
|
+
source_path = None if config.model_source_path is None else str(config.model_source_path)
|
|
544
|
+
_prepend_source_path(source_path)
|
|
545
|
+
self.torch = _require_module("torch", spec)
|
|
546
|
+
try:
|
|
547
|
+
configuration_module = importlib.import_module("janusdna.configuration_janusdna")
|
|
548
|
+
modeling_module = importlib.import_module("janusdna.modeling_janusdna")
|
|
549
|
+
except ImportError as exc:
|
|
550
|
+
raise BackendUnavailableError(
|
|
551
|
+
"JanusDNA source code is required to load its official checkpoint.",
|
|
552
|
+
code="MISSING_NEURAL_DEPENDENCY",
|
|
553
|
+
hint=(
|
|
554
|
+
"Clone https://github.com/Qihao-Duan/JanusDNA and pass "
|
|
555
|
+
"model_source_path in RepresentationConfig."
|
|
556
|
+
),
|
|
557
|
+
) from exc
|
|
558
|
+
payload = json.loads((checkpoint / "model_config.json").read_text(encoding="utf-8"))
|
|
559
|
+
model_config = configuration_module.JanusDNAConfig(
|
|
560
|
+
**_clean_janusdna_config(dict(payload["config"]))
|
|
561
|
+
)
|
|
562
|
+
model_config._attn_implementation = "eager"
|
|
563
|
+
model_config.final_attention_class = "eager"
|
|
564
|
+
causal_model = modeling_module.JanusDNAForCausalLM(model_config)
|
|
565
|
+
state = self.torch.load(checkpoint / "checkpoints" / "last.ckpt", map_location="cpu")
|
|
566
|
+
state_dict = dict(state["state_dict"])
|
|
567
|
+
self.torch.nn.modules.utils.consume_prefix_in_state_dict_if_present(state_dict, "model.")
|
|
568
|
+
for key in tuple(state_dict):
|
|
569
|
+
if ".self_attn.o_projs.0." in key:
|
|
570
|
+
state_dict[key.replace(".self_attn.o_projs.0.", ".self_attn.o_proj.")] = (
|
|
571
|
+
state_dict.pop(key)
|
|
572
|
+
)
|
|
573
|
+
elif "torchmetrics" in key:
|
|
574
|
+
state_dict.pop(key)
|
|
575
|
+
missing, unexpected = causal_model.load_state_dict(state_dict, strict=False)
|
|
576
|
+
missing = [key for key in missing if not key.startswith("lm_head.")]
|
|
577
|
+
if missing or unexpected:
|
|
578
|
+
raise BackendExecutionError(
|
|
579
|
+
"JanusDNA checkpoint did not match the official model architecture.",
|
|
580
|
+
code="MODEL_LOAD_FAILED",
|
|
581
|
+
context={"missing": tuple(missing[:10]), "unexpected": tuple(unexpected[:10])},
|
|
582
|
+
)
|
|
583
|
+
self.device = _resolve_torch_device(self.torch, config.device)
|
|
584
|
+
self.model = causal_model.model
|
|
585
|
+
dtype = _torch_dtype(self.torch, config.dtype)
|
|
586
|
+
if dtype is not None:
|
|
587
|
+
self.model = self.model.to(dtype=dtype)
|
|
588
|
+
self.model = self.model.to(self.device).eval()
|
|
589
|
+
self.chunk_length = int(config.max_length or spec.chunk_length)
|
|
590
|
+
self.base_ids = {"A": 7, "C": 8, "G": 9, "T": 10, "N": 11}
|
|
591
|
+
|
|
592
|
+
def _encode_sequence(self, sequence: str) -> Any:
|
|
593
|
+
chunks = [
|
|
594
|
+
sequence[start : start + self.chunk_length]
|
|
595
|
+
for start in range(0, len(sequence), self.chunk_length)
|
|
596
|
+
]
|
|
597
|
+
vectors: list[Any] = []
|
|
598
|
+
for start in range(0, len(chunks), self.config.batch_size):
|
|
599
|
+
batch_chunks = chunks[start : start + self.config.batch_size]
|
|
600
|
+
max_size = max(len(chunk) for chunk in batch_chunks) + 2
|
|
601
|
+
input_ids = self.torch.full(
|
|
602
|
+
(len(batch_chunks), max_size),
|
|
603
|
+
4,
|
|
604
|
+
dtype=self.torch.long,
|
|
605
|
+
device=self.device,
|
|
606
|
+
)
|
|
607
|
+
attention_mask = self.torch.zeros_like(input_ids)
|
|
608
|
+
special_mask = self.torch.ones_like(input_ids)
|
|
609
|
+
for row, chunk in enumerate(batch_chunks):
|
|
610
|
+
ids = [0, *(self.base_ids[base] for base in chunk), 1]
|
|
611
|
+
input_ids[row, : len(ids)] = self.torch.tensor(ids, device=self.device)
|
|
612
|
+
attention_mask[row, : len(ids)] = 1
|
|
613
|
+
special_mask[row, 1 : len(ids) - 1] = 0
|
|
614
|
+
with self.torch.inference_mode():
|
|
615
|
+
outputs = self.model(input_ids=input_ids, attention_mask=attention_mask)
|
|
616
|
+
hidden = _resolve_hidden_states(outputs, self.torch)
|
|
617
|
+
vectors.append(
|
|
618
|
+
_pool_hidden_states(
|
|
619
|
+
hidden,
|
|
620
|
+
torch=self.torch,
|
|
621
|
+
pooling=self.config.pooling,
|
|
622
|
+
attention_mask=attention_mask,
|
|
623
|
+
special_tokens_mask=special_mask,
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
return self.torch.cat(vectors).mean(dim=0)
|
|
627
|
+
|
|
628
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
629
|
+
vectors = [
|
|
630
|
+
self._encode_sequence(sequence)
|
|
631
|
+
for sequence in _progress(
|
|
632
|
+
sequences,
|
|
633
|
+
enabled=show_progress,
|
|
634
|
+
description="Extracting JanusDNA rep",
|
|
635
|
+
)
|
|
636
|
+
]
|
|
637
|
+
return self.torch.stack(vectors).detach().float().cpu().numpy()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class _Evo2Backend:
|
|
641
|
+
def __init__(
|
|
642
|
+
self,
|
|
643
|
+
spec: DNAEmbeddingModel,
|
|
644
|
+
checkpoint: Path,
|
|
645
|
+
config: RepresentationConfig,
|
|
646
|
+
) -> None:
|
|
647
|
+
self.spec = spec
|
|
648
|
+
self.config = config
|
|
649
|
+
_prepend_source_path(
|
|
650
|
+
None if config.model_source_path is None else str(config.model_source_path)
|
|
651
|
+
)
|
|
652
|
+
self.torch = _require_module("torch", spec)
|
|
653
|
+
evo2_module = _require_module("evo2", spec)
|
|
654
|
+
checkpoint_file = checkpoint / "evo2_7b.pt"
|
|
655
|
+
try:
|
|
656
|
+
self.wrapper = evo2_module.Evo2(
|
|
657
|
+
model_name="evo2_7b",
|
|
658
|
+
local_path=str(checkpoint_file),
|
|
659
|
+
)
|
|
660
|
+
except Exception as exc:
|
|
661
|
+
raise BackendExecutionError(
|
|
662
|
+
"Could not load the Evo 2 checkpoint.",
|
|
663
|
+
code="MODEL_LOAD_FAILED",
|
|
664
|
+
) from exc
|
|
665
|
+
self.model = self.wrapper.model.eval()
|
|
666
|
+
self.device = _resolve_torch_device(self.torch, config.device)
|
|
667
|
+
dtype = _torch_dtype(self.torch, config.dtype)
|
|
668
|
+
if dtype is not None:
|
|
669
|
+
self.model = self.model.to(dtype=dtype)
|
|
670
|
+
self.model = self.model.to(self.device)
|
|
671
|
+
self.chunk_length = int(config.max_length or spec.chunk_length)
|
|
672
|
+
if spec.evo2_layer is None:
|
|
673
|
+
raise AssertionError("Evo 2 registry entry is missing an embedding layer.")
|
|
674
|
+
self.layer = spec.evo2_layer
|
|
675
|
+
|
|
676
|
+
def _encode_sequence(self, sequence: str) -> Any:
|
|
677
|
+
chunks = [
|
|
678
|
+
sequence[start : start + self.chunk_length]
|
|
679
|
+
for start in range(0, len(sequence), self.chunk_length)
|
|
680
|
+
]
|
|
681
|
+
vectors: list[Any] = []
|
|
682
|
+
for chunk in chunks:
|
|
683
|
+
input_ids = self.torch.tensor(
|
|
684
|
+
self.wrapper.tokenizer.tokenize(chunk),
|
|
685
|
+
dtype=self.torch.int,
|
|
686
|
+
device=self.device,
|
|
687
|
+
).unsqueeze(0)
|
|
688
|
+
with self.torch.inference_mode():
|
|
689
|
+
_, embeddings = self.wrapper(
|
|
690
|
+
input_ids,
|
|
691
|
+
return_embeddings=True,
|
|
692
|
+
layer_names=[self.layer],
|
|
693
|
+
)
|
|
694
|
+
vectors.append(
|
|
695
|
+
_pool_hidden_states(
|
|
696
|
+
embeddings[self.layer],
|
|
697
|
+
torch=self.torch,
|
|
698
|
+
pooling=self.config.pooling,
|
|
699
|
+
).squeeze(0)
|
|
700
|
+
)
|
|
701
|
+
return self.torch.stack(vectors).mean(dim=0)
|
|
702
|
+
|
|
703
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
704
|
+
vectors = [
|
|
705
|
+
self._encode_sequence(sequence)
|
|
706
|
+
for sequence in _progress(
|
|
707
|
+
sequences,
|
|
708
|
+
enabled=show_progress,
|
|
709
|
+
description="Extracting Evo 2 rep",
|
|
710
|
+
)
|
|
711
|
+
]
|
|
712
|
+
return self.torch.stack(vectors).detach().float().cpu().numpy()
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _build_alphagenome_apply_fn(module: Any, metadata: Any, *, force_float32: bool) -> Any:
|
|
716
|
+
if not force_float32:
|
|
717
|
+
_, apply_fn, _ = module.create_model(metadata)
|
|
718
|
+
return apply_fn
|
|
719
|
+
hk = module.hk
|
|
720
|
+
jmp = module.jmp
|
|
721
|
+
research_model = module.model
|
|
722
|
+
policy = jmp.get_policy("params=float32,compute=float32,output=float32")
|
|
723
|
+
|
|
724
|
+
@hk.transform_with_state # type: ignore[untyped-decorator]
|
|
725
|
+
def forward(dna_sequence: Any, organism_index: Any) -> Any:
|
|
726
|
+
with hk.mixed_precision.push_policy(research_model.AlphaGenome, policy):
|
|
727
|
+
return research_model.AlphaGenome(
|
|
728
|
+
metadata,
|
|
729
|
+
num_splice_sites=research_model.DEFAULT_NUM_SPLICE_SITES,
|
|
730
|
+
splice_site_threshold=research_model.DEFAULT_SPLICE_SITE_THRESHOLD,
|
|
731
|
+
)(dna_sequence, organism_index)
|
|
732
|
+
|
|
733
|
+
def apply(params: Any, state: Any, dna_sequence: Any, organism_index: Any) -> Any:
|
|
734
|
+
(predictions, _), _ = forward.apply(
|
|
735
|
+
params,
|
|
736
|
+
state,
|
|
737
|
+
None,
|
|
738
|
+
dna_sequence,
|
|
739
|
+
organism_index,
|
|
740
|
+
)
|
|
741
|
+
return predictions
|
|
742
|
+
|
|
743
|
+
return apply
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _cast_jax_float32(tree: Any, *, jax: Any, jnp: Any) -> Any:
|
|
747
|
+
def cast(value: Any) -> Any:
|
|
748
|
+
dtype = getattr(value, "dtype", None)
|
|
749
|
+
if dtype is None:
|
|
750
|
+
return value
|
|
751
|
+
try:
|
|
752
|
+
if jnp.issubdtype(dtype, jnp.floating):
|
|
753
|
+
return value.astype(jnp.float32)
|
|
754
|
+
except TypeError:
|
|
755
|
+
pass
|
|
756
|
+
return value
|
|
757
|
+
|
|
758
|
+
return jax.tree_util.tree_map(cast, tree)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class _AlphaGenomeBackend:
|
|
762
|
+
def __init__(
|
|
763
|
+
self,
|
|
764
|
+
spec: DNAEmbeddingModel,
|
|
765
|
+
checkpoint: Path,
|
|
766
|
+
config: RepresentationConfig,
|
|
767
|
+
) -> None:
|
|
768
|
+
self.spec = spec
|
|
769
|
+
self.config = config
|
|
770
|
+
_prepend_source_path(
|
|
771
|
+
None if config.model_source_path is None else str(config.model_source_path)
|
|
772
|
+
)
|
|
773
|
+
try:
|
|
774
|
+
jax = importlib.import_module("jax")
|
|
775
|
+
jnp = importlib.import_module("jax.numpy")
|
|
776
|
+
ocp = importlib.import_module("orbax.checkpoint")
|
|
777
|
+
api_dna_model = importlib.import_module("alphagenome.models.dna_model")
|
|
778
|
+
dna_model = importlib.import_module("alphagenome_research.model.dna_model")
|
|
779
|
+
metadata_lib = importlib.import_module("alphagenome_research.model.metadata.metadata")
|
|
780
|
+
except ImportError as exc:
|
|
781
|
+
raise _missing_dependency(spec, "alphagenome_research") from exc
|
|
782
|
+
self.jax = jax
|
|
783
|
+
self.jnp = jnp
|
|
784
|
+
self.api_dna_model = api_dna_model
|
|
785
|
+
devices = list(jax.devices())
|
|
786
|
+
requested = config.device.strip().lower()
|
|
787
|
+
if requested == "auto":
|
|
788
|
+
self.device = next(
|
|
789
|
+
(device for device in devices if device.platform == "gpu"),
|
|
790
|
+
devices[0],
|
|
791
|
+
)
|
|
792
|
+
else:
|
|
793
|
+
platform, separator, index_text = requested.partition(":")
|
|
794
|
+
if platform == "cuda":
|
|
795
|
+
platform = "gpu"
|
|
796
|
+
matching = [device for device in devices if device.platform == platform]
|
|
797
|
+
try:
|
|
798
|
+
device_index = 0 if not separator else int(index_text)
|
|
799
|
+
except ValueError as exc:
|
|
800
|
+
raise ConfigurationError(
|
|
801
|
+
"Invalid JAX device index.",
|
|
802
|
+
code="INVALID_REPRESENTATION_DEVICE",
|
|
803
|
+
context={"device": config.device},
|
|
804
|
+
) from exc
|
|
805
|
+
if not matching or not 0 <= device_index < len(matching):
|
|
806
|
+
raise ConfigurationError(
|
|
807
|
+
"Requested JAX device is unavailable.",
|
|
808
|
+
code="INVALID_REPRESENTATION_DEVICE",
|
|
809
|
+
context={"device": config.device},
|
|
810
|
+
)
|
|
811
|
+
self.device = matching[device_index]
|
|
812
|
+
if config.dtype not in {"auto", "float32"}:
|
|
813
|
+
raise ConfigurationError(
|
|
814
|
+
"AlphaGenome controls mixed precision in its official JAX implementation.",
|
|
815
|
+
code="INVALID_REPRESENTATION_DTYPE",
|
|
816
|
+
hint='Use dtype="auto" or dtype="float32" for AlphaGenome.',
|
|
817
|
+
)
|
|
818
|
+
metadata = {organism: metadata_lib.load(organism) for organism in api_dna_model.Organism}
|
|
819
|
+
params, state = ocp.StandardCheckpointer().restore(
|
|
820
|
+
str(checkpoint),
|
|
821
|
+
target=None,
|
|
822
|
+
strict=False,
|
|
823
|
+
)
|
|
824
|
+
if self.device.platform != "gpu":
|
|
825
|
+
params = _cast_jax_float32(params, jax=jax, jnp=jnp)
|
|
826
|
+
state = _cast_jax_float32(state, jax=jax, jnp=jnp)
|
|
827
|
+
self.params = jax.device_put(params, self.device)
|
|
828
|
+
self.state = jax.device_put(state, self.device)
|
|
829
|
+
apply_fn = _build_alphagenome_apply_fn(
|
|
830
|
+
dna_model,
|
|
831
|
+
metadata,
|
|
832
|
+
force_float32=self.device.platform != "gpu",
|
|
833
|
+
)
|
|
834
|
+
self.apply_fn = jax.jit(apply_fn, device=self.device)
|
|
835
|
+
self.chunk_length = int(config.max_length or spec.chunk_length)
|
|
836
|
+
self.minimum_length = 2_048
|
|
837
|
+
self.alignment = 2_048
|
|
838
|
+
|
|
839
|
+
def _pad(self, sequence: str) -> str:
|
|
840
|
+
size = max(len(sequence), self.minimum_length)
|
|
841
|
+
size = int(math.ceil(size / self.alignment) * self.alignment)
|
|
842
|
+
size = min(size, self.chunk_length)
|
|
843
|
+
return sequence[:size] + "N" * max(0, size - len(sequence))
|
|
844
|
+
|
|
845
|
+
@staticmethod
|
|
846
|
+
def _one_hot(sequence: str) -> Any:
|
|
847
|
+
import numpy as np
|
|
848
|
+
|
|
849
|
+
lookup = {
|
|
850
|
+
"A": (1.0, 0.0, 0.0, 0.0),
|
|
851
|
+
"C": (0.0, 1.0, 0.0, 0.0),
|
|
852
|
+
"G": (0.0, 0.0, 1.0, 0.0),
|
|
853
|
+
"T": (0.0, 0.0, 0.0, 1.0),
|
|
854
|
+
"N": (0.0, 0.0, 0.0, 0.0),
|
|
855
|
+
}
|
|
856
|
+
return np.asarray([lookup[base] for base in sequence], dtype=np.float32)[None, ...]
|
|
857
|
+
|
|
858
|
+
def _pool(self, embeddings: Any) -> Any:
|
|
859
|
+
import numpy as np
|
|
860
|
+
|
|
861
|
+
values = np.asarray(embeddings, dtype=np.float32)
|
|
862
|
+
if self.config.pooling == "mean":
|
|
863
|
+
return values.mean(axis=1).squeeze(0)
|
|
864
|
+
if self.config.pooling == "max":
|
|
865
|
+
return values.max(axis=1).squeeze(0)
|
|
866
|
+
if self.config.pooling == "cls":
|
|
867
|
+
return values[:, 0, :].squeeze(0)
|
|
868
|
+
return values[:, -1, :].squeeze(0)
|
|
869
|
+
|
|
870
|
+
def _encode_sequence(self, sequence: str) -> Any:
|
|
871
|
+
import numpy as np
|
|
872
|
+
|
|
873
|
+
chunks = [
|
|
874
|
+
sequence[start : start + self.chunk_length]
|
|
875
|
+
for start in range(0, len(sequence), self.chunk_length)
|
|
876
|
+
]
|
|
877
|
+
vectors = []
|
|
878
|
+
for chunk in chunks:
|
|
879
|
+
padded = self._pad(chunk)
|
|
880
|
+
dna = self.jax.device_put(self._one_hot(padded), self.device)
|
|
881
|
+
organism = self.jax.device_put(
|
|
882
|
+
self.jnp.asarray(
|
|
883
|
+
[self.api_dna_model.Organism.HOMO_SAPIENS.value],
|
|
884
|
+
dtype=self.jnp.int32,
|
|
885
|
+
),
|
|
886
|
+
self.device,
|
|
887
|
+
)
|
|
888
|
+
predictions = self.apply_fn(self.params, self.state, dna, organism)
|
|
889
|
+
vectors.append(self._pool(predictions["embeddings_1bp"]))
|
|
890
|
+
return np.stack(vectors).mean(axis=0)
|
|
891
|
+
|
|
892
|
+
def extract(self, sequences: Sequence[str], *, show_progress: bool) -> Any:
|
|
893
|
+
import numpy as np
|
|
894
|
+
|
|
895
|
+
return np.stack(
|
|
896
|
+
[
|
|
897
|
+
self._encode_sequence(sequence)
|
|
898
|
+
for sequence in _progress(
|
|
899
|
+
sequences,
|
|
900
|
+
enabled=show_progress,
|
|
901
|
+
description="Extracting AlphaGenome rep",
|
|
902
|
+
)
|
|
903
|
+
]
|
|
904
|
+
).astype(np.float32, copy=False)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def create_representation_backend(
|
|
908
|
+
spec: DNAEmbeddingModel,
|
|
909
|
+
checkpoint: str | Path,
|
|
910
|
+
config: RepresentationConfig,
|
|
911
|
+
) -> RepresentationBackend:
|
|
912
|
+
"""Create one lazy backend for a locally resolved official checkpoint."""
|
|
913
|
+
|
|
914
|
+
path = Path(checkpoint).expanduser().resolve()
|
|
915
|
+
if spec.loader == "transformers":
|
|
916
|
+
return _TransformersBackend(spec, path, config)
|
|
917
|
+
if spec.loader == "enformer":
|
|
918
|
+
return _EnformerBackend(spec, path, config)
|
|
919
|
+
if spec.loader == "janusdna":
|
|
920
|
+
return _JanusDNABackend(spec, path, config)
|
|
921
|
+
if spec.loader == "evo2":
|
|
922
|
+
return _Evo2Backend(spec, path, config)
|
|
923
|
+
if spec.loader == "alphagenome":
|
|
924
|
+
return _AlphaGenomeBackend(spec, path, config)
|
|
925
|
+
raise AssertionError(f"Unsupported model loader: {spec.loader}")
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
__all__ = ["RepresentationBackend", "create_representation_backend"]
|