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.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. 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
+ ]