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,356 @@
|
|
|
1
|
+
"""On-demand checkpoint downloads into a user-visible ``ckpt`` directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import tarfile
|
|
11
|
+
import tempfile
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from dnakit.download import DownloadConfig, DownloadProgress, RemoteFile, download_file
|
|
17
|
+
from dnakit.exceptions import BackendUnavailableError, DownloadError
|
|
18
|
+
|
|
19
|
+
from .models import DNAEmbeddingModel, get_embedding_model
|
|
20
|
+
|
|
21
|
+
_MANIFEST_NAME = ".dnakit-checkpoint.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class CheckpointInfo:
|
|
26
|
+
"""Resolved checkpoint path and whether this call downloaded it."""
|
|
27
|
+
|
|
28
|
+
model_name: str
|
|
29
|
+
path: str
|
|
30
|
+
source: str
|
|
31
|
+
source_repository: str
|
|
32
|
+
downloaded: bool
|
|
33
|
+
gated: bool
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, object]:
|
|
36
|
+
"""Return JSON-compatible checkpoint provenance."""
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"model_name": self.model_name,
|
|
40
|
+
"path": self.path,
|
|
41
|
+
"source": self.source,
|
|
42
|
+
"source_repository": self.source_repository,
|
|
43
|
+
"downloaded": self.downloaded,
|
|
44
|
+
"gated": self.gated,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def default_checkpoint_root() -> Path:
|
|
49
|
+
"""Return ``./ckpt`` using the working directory at call time."""
|
|
50
|
+
|
|
51
|
+
return (Path.cwd() / "ckpt").resolve()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _checkpoint_root(value: str | os.PathLike[str] | None) -> Path:
|
|
55
|
+
return default_checkpoint_root() if value is None else Path(value).expanduser().resolve()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
59
|
+
try:
|
|
60
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
62
|
+
return None
|
|
63
|
+
return payload if isinstance(payload, dict) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _indexed_weights_are_complete(target: Path) -> bool:
|
|
67
|
+
for index_path in (
|
|
68
|
+
target / "model.safetensors.index.json",
|
|
69
|
+
target / "pytorch_model.bin.index.json",
|
|
70
|
+
):
|
|
71
|
+
if not index_path.is_file():
|
|
72
|
+
continue
|
|
73
|
+
payload = _read_json(index_path)
|
|
74
|
+
weight_map = None if payload is None else payload.get("weight_map")
|
|
75
|
+
if not isinstance(weight_map, dict) or not weight_map:
|
|
76
|
+
return False
|
|
77
|
+
filenames = {value for value in weight_map.values() if isinstance(value, str)}
|
|
78
|
+
return bool(filenames) and all((target / filename).is_file() for filename in filenames)
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _checkpoint_ready(spec: DNAEmbeddingModel, target: Path) -> bool:
|
|
83
|
+
if not target.is_dir():
|
|
84
|
+
return False
|
|
85
|
+
if spec.loader == "janusdna":
|
|
86
|
+
return (target / "model_config.json").is_file() and (
|
|
87
|
+
target / "checkpoints" / "last.ckpt"
|
|
88
|
+
).is_file()
|
|
89
|
+
if spec.loader == "alphagenome":
|
|
90
|
+
return (target / "_CHECKPOINT_METADATA").is_file() and (target / "manifest.ocdbt").is_file()
|
|
91
|
+
if spec.loader == "evo2":
|
|
92
|
+
return (target / "evo2_7b.pt").is_file()
|
|
93
|
+
if not (target / "config.json").is_file():
|
|
94
|
+
return False
|
|
95
|
+
if _indexed_weights_are_complete(target):
|
|
96
|
+
return True
|
|
97
|
+
weight_patterns = ("*.safetensors", "*.bin", "*.pt", "*.ckpt")
|
|
98
|
+
return any(any(target.glob(pattern)) for pattern in weight_patterns)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _write_manifest(target: Path, spec: DNAEmbeddingModel) -> None:
|
|
102
|
+
payload = {
|
|
103
|
+
"schema_version": 1,
|
|
104
|
+
"model_name": spec.name,
|
|
105
|
+
"checkpoint_kind": spec.checkpoint_kind,
|
|
106
|
+
"checkpoint_id": spec.checkpoint_id,
|
|
107
|
+
"checkpoint_url": spec.checkpoint_url,
|
|
108
|
+
"source_repository": spec.source_repository,
|
|
109
|
+
"archive_md5": spec.archive_md5,
|
|
110
|
+
"gated": spec.gated,
|
|
111
|
+
}
|
|
112
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
with tempfile.NamedTemporaryFile(
|
|
114
|
+
mode="w",
|
|
115
|
+
encoding="utf-8",
|
|
116
|
+
prefix=f".{_MANIFEST_NAME}.",
|
|
117
|
+
suffix=".part",
|
|
118
|
+
dir=target,
|
|
119
|
+
delete=False,
|
|
120
|
+
) as output:
|
|
121
|
+
json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True)
|
|
122
|
+
output.write("\n")
|
|
123
|
+
temp_path = Path(output.name)
|
|
124
|
+
os.replace(temp_path, target / _MANIFEST_NAME)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _missing_huggingface_dependency() -> BackendUnavailableError:
|
|
128
|
+
return BackendUnavailableError(
|
|
129
|
+
"Checkpoint download requires huggingface-hub.",
|
|
130
|
+
code="MISSING_NEURAL_DEPENDENCY",
|
|
131
|
+
hint='Install the neural extra with: python -m pip install "dnakit[neural]"',
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _download_huggingface(
|
|
136
|
+
spec: DNAEmbeddingModel,
|
|
137
|
+
target: Path,
|
|
138
|
+
*,
|
|
139
|
+
token: str | None,
|
|
140
|
+
show_progress: bool,
|
|
141
|
+
) -> None:
|
|
142
|
+
try:
|
|
143
|
+
huggingface_hub = importlib.import_module("huggingface_hub")
|
|
144
|
+
except ImportError as exc:
|
|
145
|
+
raise _missing_huggingface_dependency() from exc
|
|
146
|
+
|
|
147
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
progress_was_disabled = False
|
|
149
|
+
progress_utils: Any | None = None
|
|
150
|
+
if not show_progress:
|
|
151
|
+
try:
|
|
152
|
+
progress_utils = importlib.import_module("huggingface_hub.utils")
|
|
153
|
+
progress_was_disabled = bool(progress_utils.are_progress_bars_disabled())
|
|
154
|
+
progress_utils.disable_progress_bars()
|
|
155
|
+
except (ImportError, AttributeError):
|
|
156
|
+
progress_utils = None
|
|
157
|
+
try:
|
|
158
|
+
huggingface_hub.snapshot_download(
|
|
159
|
+
repo_id=spec.checkpoint_id,
|
|
160
|
+
repo_type="model",
|
|
161
|
+
local_dir=str(target),
|
|
162
|
+
allow_patterns=(None if spec.allow_patterns is None else list(spec.allow_patterns)),
|
|
163
|
+
token=token,
|
|
164
|
+
max_workers=4,
|
|
165
|
+
)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
hint = (
|
|
168
|
+
"Accept the model terms on Hugging Face and provide hf_token."
|
|
169
|
+
if spec.gated
|
|
170
|
+
else "Check network access and the official checkpoint page."
|
|
171
|
+
)
|
|
172
|
+
raise DownloadError(
|
|
173
|
+
"Could not download the model checkpoint from Hugging Face.",
|
|
174
|
+
code="MODEL_CHECKPOINT_DOWNLOAD_FAILED",
|
|
175
|
+
context={"model": spec.name, "checkpoint": spec.checkpoint_id},
|
|
176
|
+
hint=hint,
|
|
177
|
+
) from exc
|
|
178
|
+
finally:
|
|
179
|
+
if not show_progress and not progress_was_disabled and progress_utils is not None:
|
|
180
|
+
progress_utils.enable_progress_bars()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _md5(path: Path) -> str:
|
|
184
|
+
digest = hashlib.md5(usedforsecurity=False)
|
|
185
|
+
with path.open("rb") as source:
|
|
186
|
+
while chunk := source.read(1_048_576):
|
|
187
|
+
digest.update(chunk)
|
|
188
|
+
return digest.hexdigest()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _janusdna_members(spec: DNAEmbeddingModel) -> dict[str, str]:
|
|
192
|
+
if spec.archive_name is None:
|
|
193
|
+
raise AssertionError("JanusDNA registry entry is missing archive_name.")
|
|
194
|
+
root_name = spec.archive_name.removesuffix(".tar.bz2")
|
|
195
|
+
return {
|
|
196
|
+
f"{root_name}/model_config.json": "model_config.json",
|
|
197
|
+
f"{root_name}/config.json": "config.json",
|
|
198
|
+
f"{root_name}/checkpoints/last.ckpt": "checkpoints/last.ckpt",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _extract_janusdna_archive(archive: Path, target: Path, spec: DNAEmbeddingModel) -> None:
|
|
203
|
+
selected = _janusdna_members(spec)
|
|
204
|
+
extracted: set[str] = set()
|
|
205
|
+
with tarfile.open(archive, mode="r:bz2") as bundle:
|
|
206
|
+
members = {member.name: member for member in bundle.getmembers()}
|
|
207
|
+
for archive_name, relative_name in selected.items():
|
|
208
|
+
member = members.get(archive_name)
|
|
209
|
+
if member is None or not member.isfile():
|
|
210
|
+
raise DownloadError(
|
|
211
|
+
"JanusDNA archive is missing a required checkpoint member.",
|
|
212
|
+
code="INVALID_MODEL_CHECKPOINT_ARCHIVE",
|
|
213
|
+
context={"member": archive_name},
|
|
214
|
+
)
|
|
215
|
+
source = bundle.extractfile(member)
|
|
216
|
+
if source is None:
|
|
217
|
+
raise DownloadError(
|
|
218
|
+
"Could not read a required JanusDNA archive member.",
|
|
219
|
+
code="INVALID_MODEL_CHECKPOINT_ARCHIVE",
|
|
220
|
+
context={"member": archive_name},
|
|
221
|
+
)
|
|
222
|
+
destination = target / relative_name
|
|
223
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
with tempfile.NamedTemporaryFile(
|
|
225
|
+
prefix=f".{destination.name}.",
|
|
226
|
+
suffix=".part",
|
|
227
|
+
dir=destination.parent,
|
|
228
|
+
delete=False,
|
|
229
|
+
) as output:
|
|
230
|
+
shutil.copyfileobj(source, output)
|
|
231
|
+
temp_path = Path(output.name)
|
|
232
|
+
os.replace(temp_path, destination)
|
|
233
|
+
extracted.add(relative_name)
|
|
234
|
+
if extracted != set(selected.values()):
|
|
235
|
+
raise DownloadError(
|
|
236
|
+
"JanusDNA checkpoint extraction did not complete.",
|
|
237
|
+
code="INVALID_MODEL_CHECKPOINT_ARCHIVE",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _download_janusdna(
|
|
242
|
+
spec: DNAEmbeddingModel,
|
|
243
|
+
target: Path,
|
|
244
|
+
*,
|
|
245
|
+
show_progress: bool,
|
|
246
|
+
) -> None:
|
|
247
|
+
if spec.archive_name is None or spec.archive_md5 is None:
|
|
248
|
+
raise AssertionError("JanusDNA registry entry is incomplete.")
|
|
249
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
archive = target / spec.archive_name
|
|
251
|
+
if archive.exists():
|
|
252
|
+
if not archive.is_file() or _md5(archive) != spec.archive_md5:
|
|
253
|
+
raise DownloadError(
|
|
254
|
+
"Existing JanusDNA archive failed its official MD5 checksum.",
|
|
255
|
+
code="CHECKSUM_MISMATCH",
|
|
256
|
+
context={"path": str(archive)},
|
|
257
|
+
hint="Move the invalid archive aside, then retry the download.",
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
progress_ui: Any | None = None
|
|
261
|
+
callback = None
|
|
262
|
+
if show_progress:
|
|
263
|
+
from rich.progress import Progress
|
|
264
|
+
|
|
265
|
+
progress_ui = Progress()
|
|
266
|
+
progress_ui.start()
|
|
267
|
+
task_id = progress_ui.add_task(f"Downloading {spec.display_name}", total=None)
|
|
268
|
+
|
|
269
|
+
def update(event: DownloadProgress) -> None:
|
|
270
|
+
progress_ui.update(
|
|
271
|
+
task_id,
|
|
272
|
+
completed=event.bytes_completed,
|
|
273
|
+
total=event.total_bytes,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
callback = update
|
|
277
|
+
try:
|
|
278
|
+
download_file(
|
|
279
|
+
RemoteFile(
|
|
280
|
+
spec.checkpoint_url,
|
|
281
|
+
filename=spec.archive_name,
|
|
282
|
+
expected_md5=spec.archive_md5,
|
|
283
|
+
),
|
|
284
|
+
archive,
|
|
285
|
+
config=DownloadConfig(
|
|
286
|
+
timeout=300.0,
|
|
287
|
+
max_file_bytes=2_000_000_000,
|
|
288
|
+
max_total_bytes=2_000_000_000,
|
|
289
|
+
),
|
|
290
|
+
progress=callback,
|
|
291
|
+
)
|
|
292
|
+
finally:
|
|
293
|
+
if progress_ui is not None:
|
|
294
|
+
progress_ui.stop()
|
|
295
|
+
_extract_janusdna_archive(archive, target, spec)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def ensure_model_checkpoint(
|
|
299
|
+
model: str,
|
|
300
|
+
*,
|
|
301
|
+
checkpoint_dir: str | os.PathLike[str] | None = None,
|
|
302
|
+
hf_token: str | None = None,
|
|
303
|
+
show_progress: bool = True,
|
|
304
|
+
) -> CheckpointInfo:
|
|
305
|
+
"""Return a local checkpoint, downloading it to ``./ckpt`` only when absent.
|
|
306
|
+
|
|
307
|
+
The model is stored under ``<checkpoint_dir>/<model-cache-name>``. A complete
|
|
308
|
+
existing checkpoint is never downloaded again. Incomplete directories are
|
|
309
|
+
resumed by the provider downloader instead of being reported as complete.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
spec = get_embedding_model(model)
|
|
313
|
+
root = _checkpoint_root(checkpoint_dir)
|
|
314
|
+
target = root / spec.cache_name
|
|
315
|
+
if _checkpoint_ready(spec, target):
|
|
316
|
+
if not (target / _MANIFEST_NAME).is_file():
|
|
317
|
+
_write_manifest(target, spec)
|
|
318
|
+
return CheckpointInfo(
|
|
319
|
+
spec.name,
|
|
320
|
+
str(target),
|
|
321
|
+
spec.checkpoint_url,
|
|
322
|
+
spec.source_repository,
|
|
323
|
+
False,
|
|
324
|
+
spec.gated,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if spec.checkpoint_kind == "huggingface":
|
|
328
|
+
_download_huggingface(
|
|
329
|
+
spec,
|
|
330
|
+
target,
|
|
331
|
+
token=hf_token,
|
|
332
|
+
show_progress=show_progress,
|
|
333
|
+
)
|
|
334
|
+
elif spec.checkpoint_kind == "dataverse":
|
|
335
|
+
_download_janusdna(spec, target, show_progress=show_progress)
|
|
336
|
+
else: # pragma: no cover - registry type and tests guard this branch.
|
|
337
|
+
raise AssertionError(f"Unsupported checkpoint kind: {spec.checkpoint_kind}")
|
|
338
|
+
|
|
339
|
+
if not _checkpoint_ready(spec, target):
|
|
340
|
+
raise DownloadError(
|
|
341
|
+
"Downloaded checkpoint did not contain the required model files.",
|
|
342
|
+
code="INCOMPLETE_MODEL_CHECKPOINT",
|
|
343
|
+
context={"model": spec.name, "path": str(target)},
|
|
344
|
+
)
|
|
345
|
+
_write_manifest(target, spec)
|
|
346
|
+
return CheckpointInfo(
|
|
347
|
+
spec.name,
|
|
348
|
+
str(target),
|
|
349
|
+
spec.checkpoint_url,
|
|
350
|
+
spec.source_repository,
|
|
351
|
+
True,
|
|
352
|
+
spec.gated,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
__all__ = ["CheckpointInfo", "default_checkpoint_root", "ensure_model_checkpoint"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Public sequence-level representation extraction API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from dnakit.core import DNARecord
|
|
9
|
+
from dnakit.exceptions import (
|
|
10
|
+
BackendExecutionError,
|
|
11
|
+
BackendUnavailableError,
|
|
12
|
+
ConfigurationError,
|
|
13
|
+
SequenceError,
|
|
14
|
+
UnsupportedGapOperationError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .backends import RepresentationBackend, create_representation_backend
|
|
18
|
+
from .checkpoints import ensure_model_checkpoint
|
|
19
|
+
from .models import RepresentationConfig
|
|
20
|
+
from .results import RepresentationResult
|
|
21
|
+
|
|
22
|
+
_CANONICAL_BASES = frozenset("ACGT")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _materialize_records(
|
|
26
|
+
records: Iterable[DNARecord],
|
|
27
|
+
*,
|
|
28
|
+
max_records: int,
|
|
29
|
+
) -> tuple[DNARecord, ...]:
|
|
30
|
+
iterator = iter(records)
|
|
31
|
+
materialized: list[DNARecord] = []
|
|
32
|
+
for index, record in enumerate(iterator):
|
|
33
|
+
if index >= max_records:
|
|
34
|
+
raise ConfigurationError(
|
|
35
|
+
"Representation extraction exceeded max_records.",
|
|
36
|
+
code="REPRESENTATION_RECORD_LIMIT",
|
|
37
|
+
context={"max_records": max_records},
|
|
38
|
+
)
|
|
39
|
+
if not isinstance(record, DNARecord):
|
|
40
|
+
raise SequenceError(
|
|
41
|
+
"Representation extraction accepts only DNARecord objects.",
|
|
42
|
+
code="INVALID_RECORD_SEQUENCE",
|
|
43
|
+
context={"index": index, "type": type(record).__name__},
|
|
44
|
+
)
|
|
45
|
+
materialized.append(record)
|
|
46
|
+
if not materialized:
|
|
47
|
+
raise ConfigurationError(
|
|
48
|
+
"At least one DNARecord is required for representation extraction.",
|
|
49
|
+
code="EMPTY_REPRESENTATION_INPUT",
|
|
50
|
+
)
|
|
51
|
+
return tuple(materialized)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _model_sequence(record: DNARecord, *, ambiguity_policy: str) -> str:
|
|
55
|
+
sequence = record.sequence
|
|
56
|
+
if sequence.is_gapped:
|
|
57
|
+
raise UnsupportedGapOperationError(
|
|
58
|
+
"Neural representation extraction does not accept explicit Gap objects.",
|
|
59
|
+
code="MODEL_GAPPED_INPUT",
|
|
60
|
+
context={"record_id": record.id},
|
|
61
|
+
hint="Resolve/remove gaps before extracting model representations.",
|
|
62
|
+
)
|
|
63
|
+
symbols = sequence.symbols
|
|
64
|
+
if not symbols:
|
|
65
|
+
raise SequenceError(
|
|
66
|
+
"Neural representation extraction does not accept empty sequences.",
|
|
67
|
+
code="MODEL_EMPTY_INPUT",
|
|
68
|
+
context={"record_id": record.id},
|
|
69
|
+
)
|
|
70
|
+
ambiguous = sorted(set(symbols) - _CANONICAL_BASES)
|
|
71
|
+
if ambiguous and ambiguity_policy == "error":
|
|
72
|
+
raise SequenceError(
|
|
73
|
+
"The selected ambiguity policy rejects non-ACGT symbols.",
|
|
74
|
+
code="MODEL_AMBIGUOUS_INPUT",
|
|
75
|
+
context={"record_id": record.id, "symbols": tuple(ambiguous)},
|
|
76
|
+
hint='Use ambiguity_policy="replace_with_n" to map IUPAC symbols to N.',
|
|
77
|
+
)
|
|
78
|
+
if not ambiguous:
|
|
79
|
+
return symbols
|
|
80
|
+
return "".join(base if base in _CANONICAL_BASES else "N" for base in symbols)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _require_numpy() -> object:
|
|
84
|
+
try:
|
|
85
|
+
import numpy as np
|
|
86
|
+
except ImportError as exc:
|
|
87
|
+
raise BackendUnavailableError(
|
|
88
|
+
"Representation extraction requires NumPy.",
|
|
89
|
+
code="MISSING_NEURAL_DEPENDENCY",
|
|
90
|
+
hint='Install the neural extra with: python -m pip install "dnakit[neural]"',
|
|
91
|
+
) from exc
|
|
92
|
+
return np
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_representations(
|
|
96
|
+
records: Iterable[DNARecord],
|
|
97
|
+
*,
|
|
98
|
+
config: RepresentationConfig | None = None,
|
|
99
|
+
backend: RepresentationBackend | None = None,
|
|
100
|
+
) -> RepresentationResult:
|
|
101
|
+
"""Extract one fixed-size representation per DNA record.
|
|
102
|
+
|
|
103
|
+
With the standard backend, a missing checkpoint is downloaded once to
|
|
104
|
+
``./ckpt/<model>``. Passing ``backend`` supports controlled custom models and
|
|
105
|
+
unit tests without downloading or importing a foundation-model runtime.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
resolved = RepresentationConfig() if config is None else config
|
|
109
|
+
if not isinstance(resolved, RepresentationConfig):
|
|
110
|
+
raise ConfigurationError(
|
|
111
|
+
"config must be RepresentationConfig or None.",
|
|
112
|
+
code="INVALID_REPRESENTATION_CONFIG",
|
|
113
|
+
)
|
|
114
|
+
materialized = _materialize_records(records, max_records=resolved.max_records)
|
|
115
|
+
sequences = tuple(
|
|
116
|
+
_model_sequence(record, ambiguity_policy=resolved.ambiguity_policy)
|
|
117
|
+
for record in materialized
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
checkpoint_path: str | None = None
|
|
121
|
+
selected_backend = backend
|
|
122
|
+
if selected_backend is None:
|
|
123
|
+
if resolved.model_spec.trust_remote_code and not resolved.allow_remote_code:
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"The selected model requires loading checkpoint-provided Python code.",
|
|
126
|
+
code="MODEL_REMOTE_CODE_NOT_ALLOWED",
|
|
127
|
+
context={"model": resolved.model},
|
|
128
|
+
hint=(
|
|
129
|
+
"Review the official repository and checkpoint, then set "
|
|
130
|
+
"allow_remote_code=True to opt in."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
if resolved.checkpoint_path is None:
|
|
134
|
+
checkpoint = ensure_model_checkpoint(
|
|
135
|
+
resolved.model,
|
|
136
|
+
checkpoint_dir=resolved.checkpoint_dir,
|
|
137
|
+
hf_token=resolved.hf_token,
|
|
138
|
+
show_progress=resolved.show_progress,
|
|
139
|
+
)
|
|
140
|
+
checkpoint_path = checkpoint.path
|
|
141
|
+
else:
|
|
142
|
+
path = Path(resolved.checkpoint_path).expanduser().resolve()
|
|
143
|
+
if not path.is_dir():
|
|
144
|
+
raise ConfigurationError(
|
|
145
|
+
"checkpoint_path must be an existing checkpoint directory.",
|
|
146
|
+
code="MODEL_CHECKPOINT_NOT_FOUND",
|
|
147
|
+
context={"path": str(path)},
|
|
148
|
+
)
|
|
149
|
+
checkpoint_path = str(path)
|
|
150
|
+
try:
|
|
151
|
+
selected_backend = create_representation_backend(
|
|
152
|
+
resolved.model_spec,
|
|
153
|
+
checkpoint_path,
|
|
154
|
+
resolved,
|
|
155
|
+
)
|
|
156
|
+
except (
|
|
157
|
+
BackendExecutionError,
|
|
158
|
+
BackendUnavailableError,
|
|
159
|
+
ConfigurationError,
|
|
160
|
+
SequenceError,
|
|
161
|
+
):
|
|
162
|
+
raise
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
raise BackendExecutionError(
|
|
165
|
+
"Could not initialize the representation backend.",
|
|
166
|
+
code="MODEL_LOAD_FAILED",
|
|
167
|
+
context={"model": resolved.model, "error_type": type(exc).__name__},
|
|
168
|
+
) from exc
|
|
169
|
+
elif not hasattr(selected_backend, "extract"):
|
|
170
|
+
raise ConfigurationError(
|
|
171
|
+
"backend must provide extract(sequences, show_progress=...).",
|
|
172
|
+
code="INVALID_REPRESENTATION_BACKEND",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
raw_matrix = selected_backend.extract(sequences, show_progress=resolved.show_progress)
|
|
177
|
+
except (BackendExecutionError, BackendUnavailableError, ConfigurationError, SequenceError):
|
|
178
|
+
raise
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
raise BackendExecutionError(
|
|
181
|
+
"The representation backend failed while encoding DNA sequences.",
|
|
182
|
+
code="REPRESENTATION_EXTRACTION_FAILED",
|
|
183
|
+
context={"model": resolved.model, "error_type": type(exc).__name__},
|
|
184
|
+
) from exc
|
|
185
|
+
|
|
186
|
+
np = _require_numpy()
|
|
187
|
+
try:
|
|
188
|
+
matrix = np.asarray(raw_matrix, dtype=np.float32) # type: ignore[attr-defined]
|
|
189
|
+
except (TypeError, ValueError) as exc:
|
|
190
|
+
raise BackendExecutionError(
|
|
191
|
+
"The representation backend returned a non-numeric matrix.",
|
|
192
|
+
code="INVALID_REPRESENTATION_MATRIX",
|
|
193
|
+
) from exc
|
|
194
|
+
if matrix.ndim != 2 or matrix.shape[0] != len(materialized) or matrix.shape[1] < 1:
|
|
195
|
+
raise BackendExecutionError(
|
|
196
|
+
"The representation backend returned an invalid matrix shape.",
|
|
197
|
+
code="INVALID_REPRESENTATION_MATRIX",
|
|
198
|
+
context={
|
|
199
|
+
"shape": tuple(int(value) for value in matrix.shape),
|
|
200
|
+
"expected_rows": len(materialized),
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
if not bool(np.isfinite(matrix).all()): # type: ignore[attr-defined]
|
|
204
|
+
raise BackendExecutionError(
|
|
205
|
+
"The representation backend returned non-finite values.",
|
|
206
|
+
code="INVALID_REPRESENTATION_MATRIX",
|
|
207
|
+
)
|
|
208
|
+
return RepresentationResult(
|
|
209
|
+
tuple(record.id for record in materialized),
|
|
210
|
+
matrix,
|
|
211
|
+
resolved.model,
|
|
212
|
+
checkpoint_path,
|
|
213
|
+
resolved.pooling,
|
|
214
|
+
int(matrix.shape[1]),
|
|
215
|
+
len(materialized),
|
|
216
|
+
resolved.ambiguity_policy,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = ["extract_representations"]
|