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,249 @@
1
+ """Rule-based synthesis-risk screening with no experimental success claim."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+
8
+ from dnakit.core import IssueSeverity, Topology
9
+ from dnakit.core._json import FrozenDict, to_json_compatible
10
+ from dnakit.descriptors import gc_at_content, homopolymer_runs, window_descriptors
11
+ from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
12
+ from dnakit.patterns import find_inverted_repeats, find_tandem_repeats
13
+
14
+ from ._shared import EvaluationInput, aggregate_numeric, as_float, issue, materialize_input, report
15
+ from .config import SynthesisRiskConfig
16
+ from .results import EvaluationEntry, EvaluationReport
17
+
18
+ _DISCLAIMER = (
19
+ "DNAKit transparent sequence-rule screen only; this score is not a vendor acceptance "
20
+ "criterion, thermodynamic structure prediction, or experimental synthesis-success probability."
21
+ )
22
+
23
+
24
+ def _window_count(
25
+ sequence_length: int,
26
+ *,
27
+ window_size: int,
28
+ step: int,
29
+ include_partial: bool,
30
+ ) -> int:
31
+ """Return the exact number of rows produced by ``window_descriptors``."""
32
+
33
+ last_start = sequence_length if include_partial else sequence_length - window_size + 1
34
+ stop = max(0, last_start)
35
+ return (stop + step - 1) // step if stop else 0
36
+
37
+
38
+ def _risk_entry(item: object, index: int, config: SynthesisRiskConfig) -> EvaluationEntry:
39
+ from ._shared import InputItem
40
+
41
+ if not isinstance(item, InputItem):
42
+ raise TypeError("Internal synthesis input must be InputItem.")
43
+ sequence = item.sequence
44
+ if sequence.is_gapped:
45
+ raise UnsupportedGapOperationError(
46
+ "Synthesis-risk rules require gap-free input.",
47
+ code="SYNTHESIS_RISK_GAP_UNSUPPORTED",
48
+ context={"subject_id": item.subject_id},
49
+ )
50
+ if sequence.topology is Topology.CIRCULAR:
51
+ raise ConfigurationError(
52
+ "Synthesis-risk rules currently require linear topology.",
53
+ code="SYNTHESIS_RISK_LINEAR_REQUIRED",
54
+ context={"subject_id": item.subject_id},
55
+ )
56
+ canonical_symbols = sequence.symbols
57
+ canonical_sequence_sha256 = hashlib.sha256(canonical_symbols.encode("ascii")).hexdigest()
58
+ window_count = _window_count(
59
+ sequence.symbol_length,
60
+ window_size=config.window_size,
61
+ step=config.window_step,
62
+ include_partial=True,
63
+ )
64
+ if window_count > config.max_windows_per_sequence:
65
+ raise ConfigurationError(
66
+ "Synthesis-risk calculation exceeds max_windows_per_sequence.",
67
+ code="SYNTHESIS_RISK_WINDOW_LIMIT",
68
+ context={
69
+ "window_count": window_count,
70
+ "max_windows_per_sequence": config.max_windows_per_sequence,
71
+ "sequence_length": sequence.symbol_length,
72
+ "window_size": config.window_size,
73
+ "window_step": config.window_step,
74
+ "include_partial": True,
75
+ },
76
+ )
77
+ global_gc = gc_at_content(sequence, ambiguity_policy="ignore").gc_fraction
78
+ windows = window_descriptors(
79
+ sequence,
80
+ ("gc",),
81
+ window_size=config.window_size,
82
+ step=config.window_step,
83
+ include_partial=True,
84
+ ambiguity_policy="ignore",
85
+ )
86
+ risky_windows = tuple(
87
+ {
88
+ "start": window.symbol_start,
89
+ "end": window.symbol_end,
90
+ "gc_fraction": window.values["gc_fraction"],
91
+ }
92
+ for window in windows.windows
93
+ if isinstance(window.values["gc_fraction"], (int, float))
94
+ and not config.local_gc_min <= float(window.values["gc_fraction"]) <= config.local_gc_max
95
+ )
96
+ homopolymer = homopolymer_runs(
97
+ sequence,
98
+ min_run_length=config.homopolymer_threshold,
99
+ ambiguity_policy="ignore",
100
+ )
101
+ tandem = find_tandem_repeats(
102
+ sequence,
103
+ min_unit_length=config.tandem_min_unit,
104
+ max_unit_length=config.tandem_max_unit,
105
+ min_repeats=config.tandem_min_repeats,
106
+ max_comparisons=config.max_pattern_comparisons_per_sequence,
107
+ max_comparison_cells=config.max_pattern_comparisons_per_sequence,
108
+ max_matches=config.max_matches_per_sequence,
109
+ )
110
+ inverted = find_inverted_repeats(
111
+ sequence,
112
+ min_arm_length=config.inverted_min_arm,
113
+ max_arm_length=config.inverted_max_arm,
114
+ max_loop_length=config.inverted_max_loop,
115
+ max_comparisons=config.max_pattern_comparisons_per_sequence,
116
+ max_comparison_cells=config.max_pattern_comparisons_per_sequence,
117
+ max_matches=config.max_matches_per_sequence,
118
+ )
119
+ components = {
120
+ "global_gc": float(
121
+ global_gc is None or not config.global_gc_min <= global_gc <= config.global_gc_max
122
+ ),
123
+ "local_gc": min(1.0, len(risky_windows) / max(1, len(windows.windows))),
124
+ "homopolymer": min(
125
+ 1.0,
126
+ max(0, homopolymer.longest_length - config.homopolymer_threshold + 1)
127
+ / config.homopolymer_threshold,
128
+ ),
129
+ "tandem_repeat": min(1.0, len(tandem.hits) / 5.0),
130
+ "inverted_repeat_structure_proxy": min(1.0, len(inverted.hits) / 5.0),
131
+ }
132
+ score = math.fsum(components.values()) / len(components)
133
+ findings = []
134
+ if any(value > 0 for value in components.values()):
135
+ findings.append(
136
+ issue(
137
+ "EVAL_SYNTHESIS_RULE_FINDING",
138
+ IssueSeverity.WARNING,
139
+ "One or more transparent sequence-risk rules were triggered.",
140
+ triggered=tuple(name for name, value in components.items() if value > 0),
141
+ )
142
+ )
143
+ if sequence.ambiguity_count:
144
+ findings.append(
145
+ issue(
146
+ "EVAL_SYNTHESIS_AMBIGUITY_IGNORED",
147
+ IssueSeverity.WARNING,
148
+ "Ambiguous IUPAC symbols were excluded from composition-based calculations.",
149
+ ambiguity_count=sequence.ambiguity_count,
150
+ )
151
+ )
152
+ return EvaluationEntry(
153
+ item.subject_id,
154
+ index,
155
+ FrozenDict(
156
+ {
157
+ "score": score,
158
+ "risk_score": score,
159
+ "risk_level": "high" if score >= 0.5 else ("medium" if score >= 0.2 else "low"),
160
+ "canonical_sequence_sha256": canonical_sequence_sha256,
161
+ "canonical_sequence_length": sequence.symbol_length,
162
+ "components": components,
163
+ "global_gc_fraction": global_gc,
164
+ "risky_gc_windows": risky_windows,
165
+ "window_count": window_count,
166
+ "longest_homopolymer": homopolymer.longest_length,
167
+ "homopolymer_runs": to_json_compatible(homopolymer.runs),
168
+ "tandem_repeat_hits": to_json_compatible(tandem.hits),
169
+ "inverted_repeat_hits": to_json_compatible(inverted.hits),
170
+ "tandem_scan_truncated": tandem.truncated,
171
+ "inverted_repeat_scan_truncated": inverted.truncated,
172
+ "tandem_comparisons": tandem.parameters["comparisons"],
173
+ "inverted_repeat_comparisons": inverted.parameters["comparisons"],
174
+ "disclaimer": _DISCLAIMER,
175
+ }
176
+ ),
177
+ tuple(findings),
178
+ )
179
+
180
+
181
+ def evaluate_synthesis_risk(
182
+ value: EvaluationInput,
183
+ *,
184
+ config: SynthesisRiskConfig | None = None,
185
+ ) -> EvaluationReport:
186
+ """Screen transparent GC, run, repeat, and inverted-repeat proxy rules."""
187
+
188
+ resolved = SynthesisRiskConfig() if config is None else config
189
+ if not isinstance(resolved, SynthesisRiskConfig):
190
+ raise TypeError("config must be SynthesisRiskConfig or None.")
191
+ items = materialize_input(value, limits=resolved.limits)
192
+ entries = tuple(_risk_entry(item, index, resolved) for index, item in enumerate(items))
193
+ return report(
194
+ name="synthesis_risk",
195
+ method="equal-weight-transparent-sequence-rule-screen",
196
+ version="eval-synthesis-risk-v2",
197
+ parameters={
198
+ "global_gc_range": (resolved.global_gc_min, resolved.global_gc_max),
199
+ "local_gc_range": (resolved.local_gc_min, resolved.local_gc_max),
200
+ "window_size": resolved.window_size,
201
+ "window_step": resolved.window_step,
202
+ "partial_windows": True,
203
+ "homopolymer_threshold": resolved.homopolymer_threshold,
204
+ "tandem": {
205
+ "min_unit": resolved.tandem_min_unit,
206
+ "max_unit": resolved.tandem_max_unit,
207
+ "min_repeats": resolved.tandem_min_repeats,
208
+ },
209
+ "inverted_repeat": {
210
+ "min_arm": resolved.inverted_min_arm,
211
+ "max_arm": resolved.inverted_max_arm,
212
+ "max_loop": resolved.inverted_max_loop,
213
+ },
214
+ "aggregation": "arithmetic mean of five component risk values",
215
+ "structure_method": "inverted-repeat count proxy; no folding backend",
216
+ "sequence_binding": {
217
+ "digest": "SHA-256",
218
+ "payload": "DNASequence.symbols encoded as ASCII",
219
+ "length": "DNASequence.symbol_length",
220
+ },
221
+ "vendor_rule_set": None,
222
+ "disclaimer": _DISCLAIMER,
223
+ "resource_limits": {
224
+ "max_windows_per_sequence": resolved.max_windows_per_sequence,
225
+ "max_pattern_comparisons_per_sequence": (
226
+ resolved.max_pattern_comparisons_per_sequence
227
+ ),
228
+ "max_matches_per_sequence": resolved.max_matches_per_sequence,
229
+ "evaluation_limits": resolved.limits,
230
+ },
231
+ },
232
+ metrics={
233
+ "score": (
234
+ math.fsum(as_float(entry.metrics, "score") for entry in entries) / len(entries)
235
+ if entries
236
+ else None
237
+ ),
238
+ "record_count": len(entries),
239
+ "risk_score_summary": aggregate_numeric(entries, "risk_score"),
240
+ "high_risk_count": sum(entry.metrics["risk_level"] == "high" for entry in entries),
241
+ "medium_risk_count": sum(entry.metrics["risk_level"] == "medium" for entry in entries),
242
+ "low_risk_count": sum(entry.metrics["risk_level"] == "low" for entry in entries),
243
+ "disclaimer": _DISCLAIMER,
244
+ },
245
+ entries=entries,
246
+ )
247
+
248
+
249
+ __all__ = ["evaluate_synthesis_risk"]
dnakit/exceptions.py ADDED
@@ -0,0 +1,189 @@
1
+ """DNAKit exception hierarchy.
2
+
3
+ Exceptions are reserved for violated object invariants, invalid configuration,
4
+ and failed infrastructure. Recoverable sequence quality findings belong in an
5
+ ``Issue``-based report instead.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from types import MappingProxyType
12
+
13
+
14
+ class DNAKitError(Exception):
15
+ """Base class carrying a stable code and actionable diagnostic context."""
16
+
17
+ default_code = "DNAKIT_ERROR"
18
+
19
+ def __init__(
20
+ self,
21
+ message: str,
22
+ *,
23
+ code: str | None = None,
24
+ context: Mapping[str, object] | None = None,
25
+ hint: str | None = None,
26
+ ) -> None:
27
+ if not isinstance(message, str) or not message.strip():
28
+ raise ValueError("An exception message must be a non-empty string.")
29
+ resolved_code = self.default_code if code is None else code
30
+ if not isinstance(resolved_code, str) or not resolved_code.strip():
31
+ raise ValueError("An exception code must be a non-empty string.")
32
+ if hint is not None and (not isinstance(hint, str) or not hint.strip()):
33
+ raise ValueError("An exception hint must be None or a non-empty string.")
34
+
35
+ self.message = message
36
+ self.code = resolved_code
37
+ self.context = MappingProxyType(dict(context or {}))
38
+ self.hint = hint
39
+ super().__init__(message)
40
+
41
+ @property
42
+ def error_code(self) -> str:
43
+ """Alias retained for callers that prefer the explicit name."""
44
+
45
+ return self.code
46
+
47
+ def __str__(self) -> str:
48
+ text = f"[{self.code}] {self.message}"
49
+ if self.hint is not None:
50
+ text += f" Hint: {self.hint}"
51
+ return text
52
+
53
+
54
+ class SequenceError(DNAKitError):
55
+ """Base class for invalid sequence objects or unsupported sequence states."""
56
+
57
+ default_code = "SEQUENCE_ERROR"
58
+
59
+
60
+ class InvalidAlphabetError(SequenceError):
61
+ """Raised when symbols do not satisfy the declared DNA alphabet."""
62
+
63
+ default_code = "INVALID_ALPHABET"
64
+
65
+
66
+ class UnknownLengthError(SequenceError):
67
+ """Raised when an exact length is requested across an unknown-length gap."""
68
+
69
+ default_code = "UNKNOWN_LENGTH"
70
+
71
+
72
+ class UnsupportedGapOperationError(SequenceError):
73
+ """Raised when an operation cannot safely process sequence gaps."""
74
+
75
+ default_code = "UNSUPPORTED_GAP_OPERATION"
76
+
77
+
78
+ class CoordinateError(DNAKitError):
79
+ """Raised for invalid coordinates or impossible coordinate conversions."""
80
+
81
+ default_code = "COORDINATE_ERROR"
82
+
83
+
84
+ class FeatureError(DNAKitError):
85
+ """Raised when a feature violates its structural invariants."""
86
+
87
+ default_code = "FEATURE_ERROR"
88
+
89
+
90
+ class DuplicateIDError(DNAKitError):
91
+ """Raised when an operation requires unique record identifiers."""
92
+
93
+ default_code = "DUPLICATE_ID"
94
+
95
+
96
+ class InputFormatError(DNAKitError):
97
+ """Raised when serialized input does not match its declared format."""
98
+
99
+ default_code = "INPUT_FORMAT_ERROR"
100
+
101
+
102
+ class RecordSourceClosedError(DNAKitError):
103
+ """Raised when consuming a manually closed record source."""
104
+
105
+ default_code = "RECORD_SOURCE_CLOSED"
106
+
107
+
108
+ class ConfigurationError(DNAKitError):
109
+ """Raised for invalid or internally inconsistent configuration."""
110
+
111
+ default_code = "CONFIGURATION_ERROR"
112
+
113
+
114
+ class CacheError(DNAKitError):
115
+ """Raised for cache integrity or persistence failures."""
116
+
117
+ default_code = "CACHE_ERROR"
118
+
119
+
120
+ class BackendError(DNAKitError):
121
+ """Base class for optional backend failures."""
122
+
123
+ default_code = "BACKEND_ERROR"
124
+
125
+
126
+ class BackendUnavailableError(BackendError):
127
+ """Raised when a requested optional backend cannot be located."""
128
+
129
+ default_code = "BACKEND_UNAVAILABLE"
130
+
131
+
132
+ class BackendVersionError(BackendError):
133
+ """Raised when a backend version is incompatible or unparseable."""
134
+
135
+ default_code = "BACKEND_VERSION_ERROR"
136
+
137
+
138
+ class BackendTimeoutError(BackendError):
139
+ """Raised when a backend exceeds its configured timeout."""
140
+
141
+ default_code = "BACKEND_TIMEOUT"
142
+
143
+
144
+ class BackendExecutionError(BackendError):
145
+ """Raised when a backend process or library call fails."""
146
+
147
+ default_code = "BACKEND_EXECUTION_ERROR"
148
+
149
+
150
+ class DownloadError(BackendError):
151
+ """Raised when an external reference-data download fails."""
152
+
153
+ default_code = "DOWNLOAD_ERROR"
154
+
155
+
156
+ class QueryError(BackendError):
157
+ """Raised when a public biological-database query fails."""
158
+
159
+ default_code = "QUERY_ERROR"
160
+
161
+
162
+ class BatchExecutionError(DNAKitError):
163
+ """Raised when a batch cannot continue under its error policy."""
164
+
165
+ default_code = "BATCH_EXECUTION_ERROR"
166
+
167
+
168
+ __all__ = [
169
+ "BackendError",
170
+ "BackendExecutionError",
171
+ "BackendTimeoutError",
172
+ "BackendUnavailableError",
173
+ "BackendVersionError",
174
+ "BatchExecutionError",
175
+ "CacheError",
176
+ "ConfigurationError",
177
+ "CoordinateError",
178
+ "DNAKitError",
179
+ "DownloadError",
180
+ "DuplicateIDError",
181
+ "FeatureError",
182
+ "InputFormatError",
183
+ "InvalidAlphabetError",
184
+ "QueryError",
185
+ "RecordSourceClosedError",
186
+ "SequenceError",
187
+ "UnknownLengthError",
188
+ "UnsupportedGapOperationError",
189
+ ]
@@ -0,0 +1,109 @@
1
+ """Deterministic dependency-free encodings and fixed-schema fingerprints."""
2
+
3
+ from dnakit.fingerprints._shared import (
4
+ FingerprintAmbiguityPolicy,
5
+ FingerprintRepresentation,
6
+ GapEncodingPolicy,
7
+ IntegerAmbiguityPolicy,
8
+ KmerFingerprintMode,
9
+ OneHotAmbiguityPolicy,
10
+ SequenceInput,
11
+ )
12
+ from dnakit.fingerprints.advanced import (
13
+ AdvancedFingerprintResult,
14
+ FeaturePreprocessor,
15
+ ThermodynamicMissingStrategy,
16
+ ThermodynamicStructureAdapter,
17
+ coding_fingerprint,
18
+ fit_preprocessor,
19
+ gc_spatial_fingerprint,
20
+ hybrid_fingerprint,
21
+ motif_fingerprint,
22
+ multiscale_fingerprint,
23
+ repeat_fingerprint,
24
+ restriction_fingerprint,
25
+ thermodynamic_fingerprint,
26
+ )
27
+ from dnakit.fingerprints.bit import (
28
+ DEFAULT_HASHED_KMER_BITS,
29
+ DEFAULT_MAX_PANEL_SIZE,
30
+ MAX_BIT_FINGERPRINT_DIMENSION,
31
+ PanelMode,
32
+ hashed_kmer_fingerprint,
33
+ panel_fingerprint,
34
+ )
35
+ from dnakit.fingerprints.encoding import (
36
+ CANONICAL_BASE_ORDER,
37
+ DEFAULT_MAX_OUTPUT_LENGTH,
38
+ INTEGER_AMBIGUITY_CODE,
39
+ INTEGER_GAP_CODE,
40
+ IUPAC_INTEGER_ORDER,
41
+ integer_encode,
42
+ one_hot_encode,
43
+ )
44
+ from dnakit.fingerprints.kmer import DEFAULT_MAX_DIMENSION, kmer, kmer_fingerprint
45
+ from dnakit.fingerprints.results import (
46
+ BitFingerprintResult,
47
+ BitFingerprintValues,
48
+ FingerprintResult,
49
+ FingerprintValues,
50
+ IntegerEncodingResult,
51
+ KmerFingerprintResult,
52
+ Numeric,
53
+ OneHotEncodingResult,
54
+ RepresentationResult,
55
+ SketchResult,
56
+ )
57
+ from dnakit.fingerprints.sketch import DEFAULT_MAX_SKETCH_HASHES, fracminhash, minhash
58
+
59
+ __all__ = [
60
+ "CANONICAL_BASE_ORDER",
61
+ "DEFAULT_HASHED_KMER_BITS",
62
+ "DEFAULT_MAX_DIMENSION",
63
+ "DEFAULT_MAX_OUTPUT_LENGTH",
64
+ "DEFAULT_MAX_PANEL_SIZE",
65
+ "DEFAULT_MAX_SKETCH_HASHES",
66
+ "INTEGER_AMBIGUITY_CODE",
67
+ "INTEGER_GAP_CODE",
68
+ "IUPAC_INTEGER_ORDER",
69
+ "MAX_BIT_FINGERPRINT_DIMENSION",
70
+ "AdvancedFingerprintResult",
71
+ "BitFingerprintResult",
72
+ "BitFingerprintValues",
73
+ "FeaturePreprocessor",
74
+ "FingerprintAmbiguityPolicy",
75
+ "FingerprintRepresentation",
76
+ "FingerprintResult",
77
+ "FingerprintValues",
78
+ "GapEncodingPolicy",
79
+ "IntegerAmbiguityPolicy",
80
+ "IntegerEncodingResult",
81
+ "KmerFingerprintMode",
82
+ "KmerFingerprintResult",
83
+ "Numeric",
84
+ "OneHotAmbiguityPolicy",
85
+ "OneHotEncodingResult",
86
+ "PanelMode",
87
+ "RepresentationResult",
88
+ "SequenceInput",
89
+ "SketchResult",
90
+ "ThermodynamicMissingStrategy",
91
+ "ThermodynamicStructureAdapter",
92
+ "coding_fingerprint",
93
+ "fit_preprocessor",
94
+ "fracminhash",
95
+ "gc_spatial_fingerprint",
96
+ "hashed_kmer_fingerprint",
97
+ "hybrid_fingerprint",
98
+ "integer_encode",
99
+ "kmer",
100
+ "kmer_fingerprint",
101
+ "minhash",
102
+ "motif_fingerprint",
103
+ "multiscale_fingerprint",
104
+ "one_hot_encode",
105
+ "panel_fingerprint",
106
+ "repeat_fingerprint",
107
+ "restriction_fingerprint",
108
+ "thermodynamic_fingerprint",
109
+ ]
@@ -0,0 +1,116 @@
1
+ """Shared public policies and validation for native fingerprints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import TypeAlias, TypeVar
7
+
8
+ from dnakit.core.facade import DNA, resolve_single_dna
9
+ from dnakit.core.record import DNARecord
10
+ from dnakit.core.sequence import DNASequence
11
+ from dnakit.exceptions import ConfigurationError
12
+
13
+ SequenceInput: TypeAlias = DNA | DNASequence | DNARecord
14
+
15
+
16
+ class GapEncodingPolicy(str, Enum):
17
+ """Treatment of explicit :class:`~dnakit.core.Gap` parts."""
18
+
19
+ ERROR = "error"
20
+ OMIT = "omit"
21
+ EXPAND = "expand"
22
+
23
+
24
+ class IntegerAmbiguityPolicy(str, Enum):
25
+ """Treatment of non-canonical IUPAC symbols in integer encoding."""
26
+
27
+ ERROR = "error"
28
+ IUPAC = "iupac"
29
+ SENTINEL = "sentinel"
30
+
31
+
32
+ class OneHotAmbiguityPolicy(str, Enum):
33
+ """Treatment of non-canonical IUPAC symbols in one-hot encoding."""
34
+
35
+ ERROR = "error"
36
+ FRACTIONAL = "fractional"
37
+ ZERO = "zero"
38
+
39
+
40
+ class FingerprintAmbiguityPolicy(str, Enum):
41
+ """Treatment of IUPAC symbols in exact k-mer fingerprints."""
42
+
43
+ ERROR = "error"
44
+ IGNORE = "ignore"
45
+
46
+
47
+ class KmerFingerprintMode(str, Enum):
48
+ """Aggregation applied to exact k-mer observations."""
49
+
50
+ COUNT = "count"
51
+ FREQUENCY = "frequency"
52
+ BINARY = "binary"
53
+
54
+
55
+ class FingerprintRepresentation(str, Enum):
56
+ """Materialized fingerprint storage representation."""
57
+
58
+ DENSE = "dense"
59
+ SPARSE = "sparse"
60
+
61
+
62
+ EnumType = TypeVar("EnumType", bound=Enum)
63
+
64
+
65
+ def sequence_and_id(value: SequenceInput) -> tuple[DNASequence, str | None]:
66
+ """Return a sequence and the optional identifier of its source record."""
67
+
68
+ if isinstance(value, (DNA, DNARecord, DNASequence)):
69
+ sequence, record = resolve_single_dna(value)
70
+ return sequence, None if record is None else record.id
71
+ raise ConfigurationError(
72
+ "Fingerprint input must be a DNASequence or DNARecord.",
73
+ context={"input_type": type(value).__name__},
74
+ )
75
+
76
+
77
+ def coerce_enum(value: EnumType | str, enum_type: type[EnumType], name: str) -> EnumType:
78
+ """Coerce a string enum value with a stable configuration diagnostic."""
79
+
80
+ try:
81
+ return value if isinstance(value, enum_type) else enum_type(value)
82
+ except (TypeError, ValueError) as exc:
83
+ choices = ", ".join(repr(item.value) for item in enum_type)
84
+ raise ConfigurationError(
85
+ f"Unknown {name}.",
86
+ context={name: value},
87
+ hint=f"Choose one of: {choices}.",
88
+ ) from exc
89
+
90
+
91
+ def validate_bool(value: bool, name: str) -> None:
92
+ """Reject truthy integers where a real boolean is required."""
93
+
94
+ if not isinstance(value, bool):
95
+ raise ConfigurationError(f"{name} must be a boolean.", context={name: value})
96
+
97
+
98
+ def validate_positive_int(value: int, name: str) -> None:
99
+ """Validate a positive integer while rejecting booleans."""
100
+
101
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
102
+ raise ConfigurationError(
103
+ f"{name} must be a positive integer.",
104
+ context={name: value},
105
+ )
106
+
107
+
108
+ __all__ = [
109
+ "FingerprintAmbiguityPolicy",
110
+ "FingerprintRepresentation",
111
+ "GapEncodingPolicy",
112
+ "IntegerAmbiguityPolicy",
113
+ "KmerFingerprintMode",
114
+ "OneHotAmbiguityPolicy",
115
+ "SequenceInput",
116
+ ]