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,229 @@
|
|
|
1
|
+
"""Fixed-length hashed k-mer and named-panel bit fingerprints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from dnakit.core import Gap
|
|
10
|
+
from dnakit.core._json import FrozenDict
|
|
11
|
+
from dnakit.descriptors import kmer_statistics
|
|
12
|
+
from dnakit.exceptions import ConfigurationError
|
|
13
|
+
from dnakit.patterns import scan_motif
|
|
14
|
+
|
|
15
|
+
from ._shared import (
|
|
16
|
+
FingerprintAmbiguityPolicy,
|
|
17
|
+
FingerprintRepresentation,
|
|
18
|
+
SequenceInput,
|
|
19
|
+
coerce_enum,
|
|
20
|
+
sequence_and_id,
|
|
21
|
+
validate_bool,
|
|
22
|
+
validate_positive_int,
|
|
23
|
+
)
|
|
24
|
+
from .results import BitFingerprintResult, BitFingerprintValues
|
|
25
|
+
|
|
26
|
+
PanelMode = Literal["exact", "iupac"]
|
|
27
|
+
DEFAULT_HASHED_KMER_BITS = 2_048
|
|
28
|
+
MAX_BIT_FINGERPRINT_DIMENSION = 1_000_000
|
|
29
|
+
DEFAULT_MAX_PANEL_SIZE = 10_000
|
|
30
|
+
_UINT64_SPACE = 1 << 64
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _hash_bit(word: str, *, seed: int, n_bits: int) -> int:
|
|
34
|
+
payload = seed.to_bytes(8, "little", signed=False) + word.encode("ascii")
|
|
35
|
+
value = int.from_bytes(hashlib.sha256(payload).digest()[:8], "big")
|
|
36
|
+
return value % n_bits
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _materialize_bits(
|
|
40
|
+
feature_names: tuple[str, ...],
|
|
41
|
+
set_indices: set[int],
|
|
42
|
+
representation: FingerprintRepresentation,
|
|
43
|
+
) -> BitFingerprintValues:
|
|
44
|
+
if representation is FingerprintRepresentation.DENSE:
|
|
45
|
+
return tuple(int(index in set_indices) for index in range(len(feature_names)))
|
|
46
|
+
return FrozenDict({feature_names[index]: 1 for index in sorted(set_indices)})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def hashed_kmer_fingerprint(
|
|
50
|
+
value: SequenceInput,
|
|
51
|
+
*,
|
|
52
|
+
k: int,
|
|
53
|
+
n_bits: int = DEFAULT_HASHED_KMER_BITS,
|
|
54
|
+
canonical: bool = True,
|
|
55
|
+
seed: int = 0,
|
|
56
|
+
representation: FingerprintRepresentation | str = FingerprintRepresentation.DENSE,
|
|
57
|
+
ambiguity_policy: FingerprintAmbiguityPolicy | str = FingerprintAmbiguityPolicy.ERROR,
|
|
58
|
+
overlapping: bool = True,
|
|
59
|
+
cross_gaps: bool = False,
|
|
60
|
+
) -> BitFingerprintResult:
|
|
61
|
+
"""Hash observed k-mers into a deterministic fixed-length binary vector."""
|
|
62
|
+
|
|
63
|
+
validate_positive_int(k, "k")
|
|
64
|
+
validate_positive_int(n_bits, "n_bits")
|
|
65
|
+
if n_bits > MAX_BIT_FINGERPRINT_DIMENSION:
|
|
66
|
+
raise ConfigurationError(
|
|
67
|
+
"n_bits exceeds the bit fingerprint dimension limit.",
|
|
68
|
+
code="BIT_FINGERPRINT_DIMENSION_LIMIT",
|
|
69
|
+
context={
|
|
70
|
+
"n_bits": n_bits,
|
|
71
|
+
"max_dimension": MAX_BIT_FINGERPRINT_DIMENSION,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
validate_bool(canonical, "canonical")
|
|
75
|
+
validate_bool(overlapping, "overlapping")
|
|
76
|
+
validate_bool(cross_gaps, "cross_gaps")
|
|
77
|
+
if isinstance(seed, bool) or not isinstance(seed, int) or not 0 <= seed < _UINT64_SPACE:
|
|
78
|
+
raise ConfigurationError(
|
|
79
|
+
"seed must be an integer in [0, 2**64).",
|
|
80
|
+
code="INVALID_BIT_FINGERPRINT_SEED",
|
|
81
|
+
)
|
|
82
|
+
resolved_representation = coerce_enum(
|
|
83
|
+
representation,
|
|
84
|
+
FingerprintRepresentation,
|
|
85
|
+
"fingerprint representation",
|
|
86
|
+
)
|
|
87
|
+
resolved_ambiguity = coerce_enum(
|
|
88
|
+
ambiguity_policy,
|
|
89
|
+
FingerprintAmbiguityPolicy,
|
|
90
|
+
"fingerprint ambiguity policy",
|
|
91
|
+
)
|
|
92
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
93
|
+
statistics = kmer_statistics(
|
|
94
|
+
value,
|
|
95
|
+
k,
|
|
96
|
+
overlapping=overlapping,
|
|
97
|
+
canonical=canonical,
|
|
98
|
+
ambiguity_policy=resolved_ambiguity.value,
|
|
99
|
+
cross_gaps=cross_gaps,
|
|
100
|
+
)
|
|
101
|
+
set_indices = {_hash_bit(word, seed=seed, n_bits=n_bits) for word in statistics.counts}
|
|
102
|
+
feature_names = tuple(f"bit:{index}" for index in range(n_bits))
|
|
103
|
+
return BitFingerprintResult(
|
|
104
|
+
name="hashed_kmer_fingerprint",
|
|
105
|
+
method="sha256-kmer-modulo-bitset",
|
|
106
|
+
schema_version="dnakit.hashed-kmer-bit.v1",
|
|
107
|
+
sequence_id=sequence_id,
|
|
108
|
+
symbol_length=sequence.symbol_length,
|
|
109
|
+
gap_count=statistics.gap_count,
|
|
110
|
+
unknown_gap_count=statistics.unknown_gap_count,
|
|
111
|
+
representation=resolved_representation,
|
|
112
|
+
feature_names=feature_names,
|
|
113
|
+
values=_materialize_bits(feature_names, set_indices, resolved_representation),
|
|
114
|
+
parameters=FrozenDict(
|
|
115
|
+
{
|
|
116
|
+
"k": k,
|
|
117
|
+
"n_bits": n_bits,
|
|
118
|
+
"canonical": canonical,
|
|
119
|
+
"seed": seed,
|
|
120
|
+
"hash": "sha256-first-64-big-endian-modulo-n-bits",
|
|
121
|
+
"ambiguity_policy": resolved_ambiguity.value,
|
|
122
|
+
"overlapping": overlapping,
|
|
123
|
+
"cross_gaps": cross_gaps,
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
observation_count=statistics.denominator,
|
|
127
|
+
ignored_ambiguity_count=statistics.ignored_ambiguity_count,
|
|
128
|
+
max_dimension=MAX_BIT_FINGERPRINT_DIMENSION,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def panel_fingerprint(
|
|
133
|
+
value: SequenceInput,
|
|
134
|
+
panel: Mapping[str, str],
|
|
135
|
+
*,
|
|
136
|
+
mode: PanelMode = "iupac",
|
|
137
|
+
overlapping: bool = True,
|
|
138
|
+
representation: FingerprintRepresentation | str = FingerprintRepresentation.DENSE,
|
|
139
|
+
max_panel_size: int = DEFAULT_MAX_PANEL_SIZE,
|
|
140
|
+
max_matches_per_pattern: int = 100_000,
|
|
141
|
+
) -> BitFingerprintResult:
|
|
142
|
+
"""Encode the presence of each named exact/IUPAC pattern in a panel."""
|
|
143
|
+
|
|
144
|
+
if not isinstance(panel, Mapping) or not panel:
|
|
145
|
+
raise ConfigurationError("panel must be a non-empty name-to-pattern mapping.")
|
|
146
|
+
validate_positive_int(max_panel_size, "max_panel_size")
|
|
147
|
+
if max_panel_size > DEFAULT_MAX_PANEL_SIZE:
|
|
148
|
+
raise ConfigurationError(
|
|
149
|
+
"max_panel_size exceeds the hard safety ceiling.",
|
|
150
|
+
code="PANEL_SIZE_LIMIT_INVALID",
|
|
151
|
+
context={
|
|
152
|
+
"max_panel_size": max_panel_size,
|
|
153
|
+
"hard_limit": DEFAULT_MAX_PANEL_SIZE,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
if len(panel) > max_panel_size:
|
|
157
|
+
raise ConfigurationError(
|
|
158
|
+
"panel exceeds max_panel_size.",
|
|
159
|
+
code="PANEL_SIZE_LIMIT_EXCEEDED",
|
|
160
|
+
context={"panel_size": len(panel), "max_panel_size": max_panel_size},
|
|
161
|
+
)
|
|
162
|
+
if any(not isinstance(name, str) or not name.strip() for name in panel):
|
|
163
|
+
raise ConfigurationError("panel names must be non-empty strings.")
|
|
164
|
+
if any(not isinstance(pattern, str) or not pattern.strip() for pattern in panel.values()):
|
|
165
|
+
raise ConfigurationError("panel patterns must be non-empty strings.")
|
|
166
|
+
if mode not in ("exact", "iupac"):
|
|
167
|
+
raise ConfigurationError(
|
|
168
|
+
"panel mode must be 'exact' or 'iupac'.",
|
|
169
|
+
context={"mode": mode},
|
|
170
|
+
)
|
|
171
|
+
validate_bool(overlapping, "overlapping")
|
|
172
|
+
validate_positive_int(max_matches_per_pattern, "max_matches_per_pattern")
|
|
173
|
+
resolved_representation = coerce_enum(
|
|
174
|
+
representation,
|
|
175
|
+
FingerprintRepresentation,
|
|
176
|
+
"fingerprint representation",
|
|
177
|
+
)
|
|
178
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
179
|
+
names = tuple(sorted(panel))
|
|
180
|
+
results = tuple(
|
|
181
|
+
scan_motif(
|
|
182
|
+
value,
|
|
183
|
+
panel[name],
|
|
184
|
+
mode=mode,
|
|
185
|
+
name=name,
|
|
186
|
+
strand="both",
|
|
187
|
+
overlapping=overlapping,
|
|
188
|
+
merge_strands=True,
|
|
189
|
+
max_matches=max_matches_per_pattern,
|
|
190
|
+
)
|
|
191
|
+
for name in names
|
|
192
|
+
)
|
|
193
|
+
set_indices = {index for index, result in enumerate(results) if result.hits}
|
|
194
|
+
feature_names = tuple(f"panel:{name}" for name in names)
|
|
195
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
196
|
+
return BitFingerprintResult(
|
|
197
|
+
name="panel_fingerprint",
|
|
198
|
+
method=f"named-{mode}-pattern-presence",
|
|
199
|
+
schema_version="dnakit.panel-presence-bit.v1",
|
|
200
|
+
sequence_id=sequence_id,
|
|
201
|
+
symbol_length=sequence.symbol_length,
|
|
202
|
+
gap_count=len(gaps),
|
|
203
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
204
|
+
representation=resolved_representation,
|
|
205
|
+
feature_names=feature_names,
|
|
206
|
+
values=_materialize_bits(feature_names, set_indices, resolved_representation),
|
|
207
|
+
parameters=FrozenDict(
|
|
208
|
+
{
|
|
209
|
+
"panel": {name: panel[name] for name in names},
|
|
210
|
+
"mode": mode,
|
|
211
|
+
"strand": "both",
|
|
212
|
+
"merge_strands": True,
|
|
213
|
+
"overlapping": overlapping,
|
|
214
|
+
}
|
|
215
|
+
),
|
|
216
|
+
observation_count=sum(len(result.hits) for result in results),
|
|
217
|
+
ignored_ambiguity_count=0,
|
|
218
|
+
max_dimension=max_panel_size,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
"DEFAULT_HASHED_KMER_BITS",
|
|
224
|
+
"DEFAULT_MAX_PANEL_SIZE",
|
|
225
|
+
"MAX_BIT_FINGERPRINT_DIMENSION",
|
|
226
|
+
"PanelMode",
|
|
227
|
+
"hashed_kmer_fingerprint",
|
|
228
|
+
"panel_fingerprint",
|
|
229
|
+
]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Dependency-free position encodings for DNA sequences."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from dnakit.core._json import FrozenDict
|
|
8
|
+
from dnakit.core.gap import Gap
|
|
9
|
+
from dnakit.exceptions import (
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
InvalidAlphabetError,
|
|
12
|
+
UnknownLengthError,
|
|
13
|
+
UnsupportedGapOperationError,
|
|
14
|
+
)
|
|
15
|
+
from dnakit.fingerprints._shared import (
|
|
16
|
+
GapEncodingPolicy,
|
|
17
|
+
IntegerAmbiguityPolicy,
|
|
18
|
+
OneHotAmbiguityPolicy,
|
|
19
|
+
SequenceInput,
|
|
20
|
+
coerce_enum,
|
|
21
|
+
sequence_and_id,
|
|
22
|
+
)
|
|
23
|
+
from dnakit.fingerprints.results import IntegerEncodingResult, OneHotEncodingResult
|
|
24
|
+
|
|
25
|
+
CANONICAL_BASE_ORDER = ("A", "C", "G", "T")
|
|
26
|
+
IUPAC_INTEGER_ORDER = (
|
|
27
|
+
"A",
|
|
28
|
+
"C",
|
|
29
|
+
"G",
|
|
30
|
+
"T",
|
|
31
|
+
"R",
|
|
32
|
+
"Y",
|
|
33
|
+
"S",
|
|
34
|
+
"W",
|
|
35
|
+
"K",
|
|
36
|
+
"M",
|
|
37
|
+
"B",
|
|
38
|
+
"D",
|
|
39
|
+
"H",
|
|
40
|
+
"V",
|
|
41
|
+
"N",
|
|
42
|
+
)
|
|
43
|
+
INTEGER_GAP_CODE = -1
|
|
44
|
+
INTEGER_AMBIGUITY_CODE = -2
|
|
45
|
+
DEFAULT_MAX_OUTPUT_LENGTH = 10_000_000
|
|
46
|
+
|
|
47
|
+
_IUPAC_BASES: dict[str, frozenset[str]] = {
|
|
48
|
+
"A": frozenset("A"),
|
|
49
|
+
"C": frozenset("C"),
|
|
50
|
+
"G": frozenset("G"),
|
|
51
|
+
"T": frozenset("T"),
|
|
52
|
+
"R": frozenset("AG"),
|
|
53
|
+
"Y": frozenset("CT"),
|
|
54
|
+
"S": frozenset("CG"),
|
|
55
|
+
"W": frozenset("AT"),
|
|
56
|
+
"K": frozenset("GT"),
|
|
57
|
+
"M": frozenset("AC"),
|
|
58
|
+
"B": frozenset("CGT"),
|
|
59
|
+
"D": frozenset("AGT"),
|
|
60
|
+
"H": frozenset("ACT"),
|
|
61
|
+
"V": frozenset("ACG"),
|
|
62
|
+
"N": frozenset("ACGT"),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _gap_counts(value: SequenceInput) -> tuple[int, int]:
|
|
67
|
+
sequence, _ = sequence_and_id(value)
|
|
68
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
69
|
+
return len(gaps), sum(gap.length is None for gap in gaps)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _raise_ambiguity(symbol: str, symbol_index: int, policy_name: str) -> None:
|
|
73
|
+
raise InvalidAlphabetError(
|
|
74
|
+
f"Ambiguous IUPAC symbol {symbol!r} is not allowed by this encoding policy.",
|
|
75
|
+
code="ENCODING_AMBIGUITY_NOT_ALLOWED",
|
|
76
|
+
context={"symbol": symbol, "symbol_index": symbol_index, "policy": policy_name},
|
|
77
|
+
hint="Choose an ambiguity policy that explicitly represents or masks IUPAC symbols.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _expand_gap(gap: Gap, gap_index: int) -> int:
|
|
82
|
+
if gap.length is None:
|
|
83
|
+
raise UnknownLengthError(
|
|
84
|
+
"An unknown-length Gap cannot be expanded into positional encoding rows.",
|
|
85
|
+
code="UNKNOWN_GAP_ENCODING_LENGTH",
|
|
86
|
+
context={"gap_index": gap_index},
|
|
87
|
+
hint="Use gap_policy='omit' or resolve the Gap length first.",
|
|
88
|
+
)
|
|
89
|
+
return gap.length
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _validate_max_output_length(value: int) -> None:
|
|
93
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
94
|
+
raise ConfigurationError(
|
|
95
|
+
"max_output_length must be a positive integer.",
|
|
96
|
+
code="INVALID_ENCODING_SIZE_LIMIT",
|
|
97
|
+
context={"max_output_length": value},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _planned_output_length(
|
|
102
|
+
value: SequenceInput,
|
|
103
|
+
*,
|
|
104
|
+
gap_policy: GapEncodingPolicy,
|
|
105
|
+
) -> int:
|
|
106
|
+
sequence, _ = sequence_and_id(value)
|
|
107
|
+
if gap_policy is not GapEncodingPolicy.EXPAND:
|
|
108
|
+
return sequence.symbol_length
|
|
109
|
+
length = sequence.symbol_length
|
|
110
|
+
gap_index = 0
|
|
111
|
+
for part in sequence.parts:
|
|
112
|
+
if isinstance(part, Gap):
|
|
113
|
+
length += _expand_gap(part, gap_index)
|
|
114
|
+
gap_index += 1
|
|
115
|
+
return length
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def integer_encode(
|
|
119
|
+
value: SequenceInput,
|
|
120
|
+
*,
|
|
121
|
+
ambiguity_policy: IntegerAmbiguityPolicy | str = IntegerAmbiguityPolicy.IUPAC,
|
|
122
|
+
gap_policy: GapEncodingPolicy | str = GapEncodingPolicy.ERROR,
|
|
123
|
+
max_output_length: int = DEFAULT_MAX_OUTPUT_LENGTH,
|
|
124
|
+
) -> IntegerEncodingResult:
|
|
125
|
+
"""Encode DNA symbols with a versioned integer codebook.
|
|
126
|
+
|
|
127
|
+
Canonical bases always map to ``A=0, C=1, G=2, T=3``. Under ``iupac``,
|
|
128
|
+
the remaining IUPAC symbols map to 4 through 14 in
|
|
129
|
+
:data:`IUPAC_INTEGER_ORDER`; under ``sentinel`` they all map to ``-2``.
|
|
130
|
+
``gap_policy='expand'`` emits ``-1`` once per known Gap coordinate.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
134
|
+
resolved_ambiguity = coerce_enum(
|
|
135
|
+
ambiguity_policy,
|
|
136
|
+
IntegerAmbiguityPolicy,
|
|
137
|
+
"integer ambiguity policy",
|
|
138
|
+
)
|
|
139
|
+
resolved_gap = coerce_enum(gap_policy, GapEncodingPolicy, "Gap encoding policy")
|
|
140
|
+
_validate_max_output_length(max_output_length)
|
|
141
|
+
planned_length = _planned_output_length(value, gap_policy=resolved_gap)
|
|
142
|
+
if planned_length > max_output_length:
|
|
143
|
+
raise ConfigurationError(
|
|
144
|
+
"Integer encoding output exceeds max_output_length.",
|
|
145
|
+
code="ENCODING_SIZE_LIMIT",
|
|
146
|
+
context={
|
|
147
|
+
"planned_output_length": planned_length,
|
|
148
|
+
"max_output_length": max_output_length,
|
|
149
|
+
},
|
|
150
|
+
hint=(
|
|
151
|
+
"Use gap_policy='omit', process a smaller sequence, or explicitly raise the limit."
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
symbols = (
|
|
155
|
+
IUPAC_INTEGER_ORDER
|
|
156
|
+
if resolved_ambiguity is IntegerAmbiguityPolicy.IUPAC
|
|
157
|
+
else CANONICAL_BASE_ORDER
|
|
158
|
+
)
|
|
159
|
+
codebook_values: dict[str, int] = {symbol: index for index, symbol in enumerate(symbols)}
|
|
160
|
+
if resolved_ambiguity is IntegerAmbiguityPolicy.SENTINEL:
|
|
161
|
+
codebook_values["<IUPAC>"] = INTEGER_AMBIGUITY_CODE
|
|
162
|
+
if resolved_gap is GapEncodingPolicy.EXPAND:
|
|
163
|
+
codebook_values["<GAP>"] = INTEGER_GAP_CODE
|
|
164
|
+
|
|
165
|
+
encoded: list[int] = []
|
|
166
|
+
symbol_index = 0
|
|
167
|
+
gap_index = 0
|
|
168
|
+
ambiguity_count = 0
|
|
169
|
+
expanded_gap_length = 0
|
|
170
|
+
omitted_gap_count = 0
|
|
171
|
+
for part in sequence.parts:
|
|
172
|
+
if isinstance(part, str):
|
|
173
|
+
for symbol in part:
|
|
174
|
+
if symbol in "ACGT":
|
|
175
|
+
encoded.append(codebook_values[symbol])
|
|
176
|
+
elif resolved_ambiguity is IntegerAmbiguityPolicy.ERROR:
|
|
177
|
+
_raise_ambiguity(symbol, symbol_index, resolved_ambiguity.value)
|
|
178
|
+
elif resolved_ambiguity is IntegerAmbiguityPolicy.IUPAC:
|
|
179
|
+
encoded.append(codebook_values[symbol])
|
|
180
|
+
ambiguity_count += 1
|
|
181
|
+
else:
|
|
182
|
+
encoded.append(INTEGER_AMBIGUITY_CODE)
|
|
183
|
+
ambiguity_count += 1
|
|
184
|
+
symbol_index += 1
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if resolved_gap is GapEncodingPolicy.ERROR:
|
|
188
|
+
raise UnsupportedGapOperationError(
|
|
189
|
+
"Integer encoding does not accept explicit Gaps under the error policy.",
|
|
190
|
+
code="ENCODING_GAP_NOT_ALLOWED",
|
|
191
|
+
context={"gap_index": gap_index},
|
|
192
|
+
hint="Choose gap_policy='omit' or 'expand'.",
|
|
193
|
+
)
|
|
194
|
+
if resolved_gap is GapEncodingPolicy.OMIT:
|
|
195
|
+
omitted_gap_count += 1
|
|
196
|
+
else:
|
|
197
|
+
length = _expand_gap(part, gap_index)
|
|
198
|
+
encoded.extend((INTEGER_GAP_CODE,) * length)
|
|
199
|
+
expanded_gap_length += length
|
|
200
|
+
gap_index += 1
|
|
201
|
+
|
|
202
|
+
gap_count, unknown_gap_count = _gap_counts(value)
|
|
203
|
+
return IntegerEncodingResult(
|
|
204
|
+
name="integer",
|
|
205
|
+
method="fixed_iupac_integer_encoding",
|
|
206
|
+
schema_version="dnakit.integer.v1",
|
|
207
|
+
sequence_id=sequence_id,
|
|
208
|
+
symbol_length=sequence.symbol_length,
|
|
209
|
+
gap_count=gap_count,
|
|
210
|
+
unknown_gap_count=unknown_gap_count,
|
|
211
|
+
values=tuple(encoded),
|
|
212
|
+
codebook=FrozenDict(codebook_values),
|
|
213
|
+
ambiguity_policy=resolved_ambiguity,
|
|
214
|
+
gap_policy=resolved_gap,
|
|
215
|
+
output_length=len(encoded),
|
|
216
|
+
encoded_ambiguity_count=ambiguity_count,
|
|
217
|
+
expanded_gap_length=expanded_gap_length,
|
|
218
|
+
omitted_gap_count=omitted_gap_count,
|
|
219
|
+
max_output_length=max_output_length,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _resolve_base_order(value: Sequence[str]) -> tuple[str, ...]:
|
|
224
|
+
if isinstance(value, (str, bytes)):
|
|
225
|
+
raise ConfigurationError("base_order must be a sequence containing A, C, G, and T.")
|
|
226
|
+
resolved = tuple(value)
|
|
227
|
+
if (
|
|
228
|
+
len(resolved) != 4
|
|
229
|
+
or set(resolved) != set("ACGT")
|
|
230
|
+
or any(not isinstance(base, str) for base in resolved)
|
|
231
|
+
):
|
|
232
|
+
raise ConfigurationError(
|
|
233
|
+
"base_order must contain each of A, C, G, and T exactly once.",
|
|
234
|
+
context={"base_order": resolved},
|
|
235
|
+
)
|
|
236
|
+
return resolved
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def one_hot_encode(
|
|
240
|
+
value: SequenceInput,
|
|
241
|
+
*,
|
|
242
|
+
ambiguity_policy: OneHotAmbiguityPolicy | str = OneHotAmbiguityPolicy.ERROR,
|
|
243
|
+
gap_policy: GapEncodingPolicy | str = GapEncodingPolicy.ERROR,
|
|
244
|
+
base_order: Sequence[str] = CANONICAL_BASE_ORDER,
|
|
245
|
+
max_output_length: int = DEFAULT_MAX_OUTPUT_LENGTH,
|
|
246
|
+
) -> OneHotEncodingResult:
|
|
247
|
+
"""Encode DNA as immutable A/C/G/T rows without requiring NumPy.
|
|
248
|
+
|
|
249
|
+
``fractional`` distributes mass uniformly over the canonical bases allowed
|
|
250
|
+
by an IUPAC symbol, while ``zero`` emits an all-zero row. Known-length Gaps
|
|
251
|
+
can likewise be expanded to zero rows; unknown-length Gaps cannot be
|
|
252
|
+
expanded because their output dimension is undefined.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
256
|
+
resolved_ambiguity = coerce_enum(
|
|
257
|
+
ambiguity_policy,
|
|
258
|
+
OneHotAmbiguityPolicy,
|
|
259
|
+
"one-hot ambiguity policy",
|
|
260
|
+
)
|
|
261
|
+
resolved_gap = coerce_enum(gap_policy, GapEncodingPolicy, "Gap encoding policy")
|
|
262
|
+
_validate_max_output_length(max_output_length)
|
|
263
|
+
planned_length = _planned_output_length(value, gap_policy=resolved_gap)
|
|
264
|
+
if planned_length > max_output_length:
|
|
265
|
+
raise ConfigurationError(
|
|
266
|
+
"One-hot encoding output exceeds max_output_length.",
|
|
267
|
+
code="ENCODING_SIZE_LIMIT",
|
|
268
|
+
context={
|
|
269
|
+
"planned_output_length": planned_length,
|
|
270
|
+
"max_output_length": max_output_length,
|
|
271
|
+
},
|
|
272
|
+
hint=(
|
|
273
|
+
"Use gap_policy='omit', process a smaller sequence, or explicitly raise the limit."
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
resolved_order = _resolve_base_order(base_order)
|
|
277
|
+
column_by_base = {base: index for index, base in enumerate(resolved_order)}
|
|
278
|
+
zero_row = (0.0, 0.0, 0.0, 0.0)
|
|
279
|
+
|
|
280
|
+
rows: list[tuple[float, ...]] = []
|
|
281
|
+
symbol_index = 0
|
|
282
|
+
gap_index = 0
|
|
283
|
+
ambiguity_count = 0
|
|
284
|
+
expanded_gap_length = 0
|
|
285
|
+
omitted_gap_count = 0
|
|
286
|
+
for part in sequence.parts:
|
|
287
|
+
if isinstance(part, str):
|
|
288
|
+
for symbol in part:
|
|
289
|
+
if symbol not in "ACGT" and resolved_ambiguity is OneHotAmbiguityPolicy.ERROR:
|
|
290
|
+
_raise_ambiguity(symbol, symbol_index, resolved_ambiguity.value)
|
|
291
|
+
if symbol not in "ACGT" and resolved_ambiguity is OneHotAmbiguityPolicy.ZERO:
|
|
292
|
+
rows.append(zero_row)
|
|
293
|
+
ambiguity_count += 1
|
|
294
|
+
symbol_index += 1
|
|
295
|
+
continue
|
|
296
|
+
allowed_bases = _IUPAC_BASES[symbol]
|
|
297
|
+
weight = 1.0 / len(allowed_bases)
|
|
298
|
+
row = [0.0, 0.0, 0.0, 0.0]
|
|
299
|
+
for base in allowed_bases:
|
|
300
|
+
row[column_by_base[base]] = weight
|
|
301
|
+
rows.append(tuple(row))
|
|
302
|
+
if symbol not in "ACGT":
|
|
303
|
+
ambiguity_count += 1
|
|
304
|
+
symbol_index += 1
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if resolved_gap is GapEncodingPolicy.ERROR:
|
|
308
|
+
raise UnsupportedGapOperationError(
|
|
309
|
+
"One-hot encoding does not accept explicit Gaps under the error policy.",
|
|
310
|
+
code="ENCODING_GAP_NOT_ALLOWED",
|
|
311
|
+
context={"gap_index": gap_index},
|
|
312
|
+
hint="Choose gap_policy='omit' or 'expand'.",
|
|
313
|
+
)
|
|
314
|
+
if resolved_gap is GapEncodingPolicy.OMIT:
|
|
315
|
+
omitted_gap_count += 1
|
|
316
|
+
else:
|
|
317
|
+
length = _expand_gap(part, gap_index)
|
|
318
|
+
rows.extend((zero_row,) * length)
|
|
319
|
+
expanded_gap_length += length
|
|
320
|
+
gap_index += 1
|
|
321
|
+
|
|
322
|
+
gap_count, unknown_gap_count = _gap_counts(value)
|
|
323
|
+
return OneHotEncodingResult(
|
|
324
|
+
name="one_hot",
|
|
325
|
+
method="iupac_acgt_one_hot_encoding",
|
|
326
|
+
schema_version="dnakit.one_hot.acgt.v1",
|
|
327
|
+
sequence_id=sequence_id,
|
|
328
|
+
symbol_length=sequence.symbol_length,
|
|
329
|
+
gap_count=gap_count,
|
|
330
|
+
unknown_gap_count=unknown_gap_count,
|
|
331
|
+
values=tuple(rows),
|
|
332
|
+
feature_names=resolved_order,
|
|
333
|
+
ambiguity_policy=resolved_ambiguity,
|
|
334
|
+
gap_policy=resolved_gap,
|
|
335
|
+
output_length=len(rows),
|
|
336
|
+
encoded_ambiguity_count=ambiguity_count,
|
|
337
|
+
expanded_gap_length=expanded_gap_length,
|
|
338
|
+
omitted_gap_count=omitted_gap_count,
|
|
339
|
+
max_output_length=max_output_length,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = [
|
|
344
|
+
"CANONICAL_BASE_ORDER",
|
|
345
|
+
"DEFAULT_MAX_OUTPUT_LENGTH",
|
|
346
|
+
"INTEGER_AMBIGUITY_CODE",
|
|
347
|
+
"INTEGER_GAP_CODE",
|
|
348
|
+
"IUPAC_INTEGER_ORDER",
|
|
349
|
+
"integer_encode",
|
|
350
|
+
"one_hot_encode",
|
|
351
|
+
]
|