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,531 @@
1
+ """Immutable structured results returned by DNAKit fingerprint functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Any, TypeAlias, cast
8
+
9
+ from dnakit.core._json import FrozenDict, to_json_compatible
10
+ from dnakit.exceptions import ConfigurationError
11
+ from dnakit.fingerprints._shared import (
12
+ FingerprintAmbiguityPolicy,
13
+ FingerprintRepresentation,
14
+ GapEncodingPolicy,
15
+ IntegerAmbiguityPolicy,
16
+ KmerFingerprintMode,
17
+ OneHotAmbiguityPolicy,
18
+ )
19
+
20
+ Numeric: TypeAlias = int | float
21
+ FingerprintValues: TypeAlias = tuple[Numeric, ...] | FrozenDict
22
+ BitFingerprintValues: TypeAlias = tuple[int, ...] | FrozenDict
23
+
24
+
25
+ def _validate_common(result: RepresentationResult) -> None:
26
+ for name in ("name", "method", "schema_version"):
27
+ value = getattr(result, name)
28
+ if not isinstance(value, str) or not value.strip():
29
+ raise ConfigurationError(f"Fingerprint result {name} must be non-empty.")
30
+ if result.sequence_id is not None and (
31
+ not isinstance(result.sequence_id, str) or not result.sequence_id.strip()
32
+ ):
33
+ raise ConfigurationError("Fingerprint result sequence_id must be non-empty or None.")
34
+ for name in ("symbol_length", "gap_count", "unknown_gap_count"):
35
+ value = getattr(result, name)
36
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
37
+ raise ConfigurationError(f"Fingerprint result {name} must be non-negative.")
38
+ if result.unknown_gap_count > result.gap_count:
39
+ raise ConfigurationError("unknown_gap_count cannot exceed gap_count.")
40
+
41
+
42
+ def _is_non_negative_number(value: object) -> bool:
43
+ return (
44
+ not isinstance(value, bool)
45
+ and isinstance(value, (int, float))
46
+ and math.isfinite(value)
47
+ and value >= 0
48
+ )
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class RepresentationResult:
53
+ """Common auditable context for encodings and fingerprints."""
54
+
55
+ name: str
56
+ method: str
57
+ schema_version: str
58
+ sequence_id: str | None
59
+ symbol_length: int
60
+ gap_count: int
61
+ unknown_gap_count: int
62
+
63
+ def __post_init__(self) -> None:
64
+ _validate_common(self)
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ """Return a plain JSON-compatible representation."""
68
+
69
+ return cast(dict[str, Any], to_json_compatible(self))
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class IntegerEncodingResult(RepresentationResult):
74
+ """Position-preserving integer encoding with an explicit codebook."""
75
+
76
+ values: tuple[int, ...]
77
+ codebook: FrozenDict
78
+ ambiguity_policy: IntegerAmbiguityPolicy
79
+ gap_policy: GapEncodingPolicy
80
+ output_length: int
81
+ encoded_ambiguity_count: int
82
+ expanded_gap_length: int
83
+ omitted_gap_count: int
84
+ max_output_length: int
85
+
86
+ def __post_init__(self) -> None:
87
+ super().__post_init__()
88
+ if not isinstance(self.codebook, FrozenDict):
89
+ raise ConfigurationError("Integer encoding codebook must be a FrozenDict.")
90
+ if not isinstance(self.ambiguity_policy, IntegerAmbiguityPolicy) or not isinstance(
91
+ self.gap_policy, GapEncodingPolicy
92
+ ):
93
+ raise ConfigurationError("Integer encoding policies must use their public enums.")
94
+ code_values = tuple(self.codebook.values())
95
+ if any(isinstance(value, bool) or not isinstance(value, int) for value in code_values):
96
+ raise ConfigurationError("Integer encoding codebook values must all be integers.")
97
+ if len(set(cast(tuple[int, ...], code_values))) != len(code_values):
98
+ raise ConfigurationError("Integer encoding codebook values must be unique.")
99
+ if any(isinstance(value, bool) or not isinstance(value, int) for value in self.values):
100
+ raise ConfigurationError("Integer encoding values must all be integers.")
101
+ if isinstance(self.output_length, bool) or not isinstance(self.output_length, int):
102
+ raise ConfigurationError("Integer encoding output_length must be an integer.")
103
+ if self.output_length != len(self.values):
104
+ raise ConfigurationError("Integer encoding output_length does not match its values.")
105
+ for name in ("encoded_ambiguity_count", "expanded_gap_length", "omitted_gap_count"):
106
+ value = getattr(self, name)
107
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
108
+ raise ConfigurationError(f"Integer encoding {name} must be non-negative.")
109
+ if (
110
+ isinstance(self.max_output_length, bool)
111
+ or not isinstance(self.max_output_length, int)
112
+ or self.max_output_length <= 0
113
+ or self.output_length > self.max_output_length
114
+ ):
115
+ raise ConfigurationError("Integer encoding max_output_length is inconsistent.")
116
+ if self.output_length != self.symbol_length + self.expanded_gap_length:
117
+ raise ConfigurationError(
118
+ "Integer encoding output length must equal symbols plus expanded Gap positions."
119
+ )
120
+ if self.encoded_ambiguity_count > self.symbol_length:
121
+ raise ConfigurationError("encoded_ambiguity_count cannot exceed symbol_length.")
122
+ if self.omitted_gap_count > self.gap_count:
123
+ raise ConfigurationError("omitted_gap_count cannot exceed gap_count.")
124
+
125
+ @property
126
+ def dimension(self) -> int:
127
+ """Number of distinct codes in the active immutable codebook."""
128
+
129
+ return len(self.codebook)
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class OneHotEncodingResult(RepresentationResult):
134
+ """A/C/G/T one-hot rows with explicit ambiguity and Gap policies."""
135
+
136
+ values: tuple[tuple[float, ...], ...]
137
+ feature_names: tuple[str, ...]
138
+ ambiguity_policy: OneHotAmbiguityPolicy
139
+ gap_policy: GapEncodingPolicy
140
+ output_length: int
141
+ encoded_ambiguity_count: int
142
+ expanded_gap_length: int
143
+ omitted_gap_count: int
144
+ max_output_length: int
145
+
146
+ def __post_init__(self) -> None:
147
+ super().__post_init__()
148
+ if not isinstance(self.ambiguity_policy, OneHotAmbiguityPolicy) or not isinstance(
149
+ self.gap_policy, GapEncodingPolicy
150
+ ):
151
+ raise ConfigurationError("One-hot encoding policies must use their public enums.")
152
+ if isinstance(self.output_length, bool) or not isinstance(self.output_length, int):
153
+ raise ConfigurationError("One-hot output_length must be an integer.")
154
+ if self.output_length != len(self.values):
155
+ raise ConfigurationError("One-hot output_length does not match its rows.")
156
+ if len(self.feature_names) != 4 or set(self.feature_names) != set("ACGT"):
157
+ raise ConfigurationError("One-hot feature_names must be a permutation of A/C/G/T.")
158
+ for row in self.values:
159
+ if len(row) != self.dimension or any(
160
+ not _is_non_negative_number(value) or value > 1 for value in row
161
+ ):
162
+ raise ConfigurationError("One-hot rows must contain four finite values in [0, 1].")
163
+ if not math.isclose(sum(row), 0.0) and not math.isclose(sum(row), 1.0):
164
+ raise ConfigurationError("Each one-hot row must sum to zero or one.")
165
+ for name in ("encoded_ambiguity_count", "expanded_gap_length", "omitted_gap_count"):
166
+ value = getattr(self, name)
167
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
168
+ raise ConfigurationError(f"One-hot encoding {name} must be non-negative.")
169
+ if (
170
+ isinstance(self.max_output_length, bool)
171
+ or not isinstance(self.max_output_length, int)
172
+ or self.max_output_length <= 0
173
+ or self.output_length > self.max_output_length
174
+ ):
175
+ raise ConfigurationError("One-hot encoding max_output_length is inconsistent.")
176
+ if self.output_length != self.symbol_length + self.expanded_gap_length:
177
+ raise ConfigurationError(
178
+ "One-hot output length must equal symbols plus expanded Gap positions."
179
+ )
180
+ if self.encoded_ambiguity_count > self.symbol_length:
181
+ raise ConfigurationError("encoded_ambiguity_count cannot exceed symbol_length.")
182
+ if self.omitted_gap_count > self.gap_count:
183
+ raise ConfigurationError("omitted_gap_count cannot exceed gap_count.")
184
+
185
+ @property
186
+ def dimension(self) -> int:
187
+ """Number of columns in each encoded row."""
188
+
189
+ return len(self.feature_names)
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class FingerprintResult(RepresentationResult):
194
+ """Fixed-schema exact k-mer fingerprint in dense or sparse form."""
195
+
196
+ k: int
197
+ canonical: bool
198
+ mode: KmerFingerprintMode
199
+ representation: FingerprintRepresentation
200
+ ambiguity_policy: FingerprintAmbiguityPolicy
201
+ overlapping: bool
202
+ cross_gaps: bool
203
+ feature_names: tuple[str, ...]
204
+ values: FingerprintValues
205
+ observation_count: int
206
+ ignored_ambiguity_count: int
207
+ max_dimension: int
208
+
209
+ def __post_init__(self) -> None:
210
+ super().__post_init__()
211
+ if not isinstance(self.mode, KmerFingerprintMode):
212
+ raise ConfigurationError("Fingerprint mode must be KmerFingerprintMode.")
213
+ if not isinstance(self.representation, FingerprintRepresentation):
214
+ raise ConfigurationError(
215
+ "Fingerprint representation must be FingerprintRepresentation."
216
+ )
217
+ if not isinstance(self.ambiguity_policy, FingerprintAmbiguityPolicy):
218
+ raise ConfigurationError(
219
+ "Fingerprint ambiguity_policy must be FingerprintAmbiguityPolicy."
220
+ )
221
+ if isinstance(self.k, bool) or not isinstance(self.k, int) or self.k <= 0:
222
+ raise ConfigurationError("Fingerprint k must be a positive integer.")
223
+ if (
224
+ isinstance(self.max_dimension, bool)
225
+ or not isinstance(self.max_dimension, int)
226
+ or self.max_dimension <= 0
227
+ ):
228
+ raise ConfigurationError("Fingerprint max_dimension must be a positive integer.")
229
+ if not isinstance(self.canonical, bool):
230
+ raise ConfigurationError("Fingerprint canonical must be a boolean.")
231
+ if not isinstance(self.overlapping, bool) or not isinstance(self.cross_gaps, bool):
232
+ raise ConfigurationError("Fingerprint traversal flags must be booleans.")
233
+ if len(set(self.feature_names)) != len(self.feature_names) or any(
234
+ not isinstance(name, str) or not name for name in self.feature_names
235
+ ):
236
+ raise ConfigurationError("Fingerprint feature_names must be unique non-empty strings.")
237
+ if self.dimension > self.max_dimension:
238
+ raise ConfigurationError("Fingerprint dimension exceeds max_dimension.")
239
+ if isinstance(self.values, tuple):
240
+ if self.representation is not FingerprintRepresentation.DENSE:
241
+ raise ConfigurationError("Tuple fingerprint values require dense representation.")
242
+ if len(self.values) != self.dimension:
243
+ raise ConfigurationError("Dense fingerprint length does not match its schema.")
244
+ numeric_values: tuple[object, ...] = self.values
245
+ elif isinstance(self.values, FrozenDict):
246
+ if self.representation is not FingerprintRepresentation.SPARSE:
247
+ raise ConfigurationError(
248
+ "Mapping fingerprint values require sparse representation."
249
+ )
250
+ unknown_features = set(self.values) - set(self.feature_names)
251
+ if unknown_features:
252
+ raise ConfigurationError(
253
+ "Sparse fingerprint contains features outside its schema.",
254
+ context={"features": sorted(unknown_features)},
255
+ )
256
+ numeric_values = tuple(self.values.values())
257
+ else:
258
+ raise ConfigurationError("Fingerprint values must be a tuple or FrozenDict.")
259
+ if any(not _is_non_negative_number(value) for value in numeric_values):
260
+ raise ConfigurationError("Fingerprint values must be finite non-negative numbers.")
261
+ if isinstance(self.values, FrozenDict) and any(value == 0 for value in numeric_values):
262
+ raise ConfigurationError("Sparse fingerprint values must omit zero entries.")
263
+ for name in ("observation_count", "ignored_ambiguity_count"):
264
+ value = getattr(self, name)
265
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
266
+ raise ConfigurationError(f"Fingerprint {name} must be non-negative.")
267
+ if self.mode is KmerFingerprintMode.COUNT and any(
268
+ not isinstance(value, int) or isinstance(value, bool) for value in numeric_values
269
+ ):
270
+ raise ConfigurationError("Count fingerprint values must be integers.")
271
+ if (
272
+ self.mode is KmerFingerprintMode.COUNT
273
+ and sum(cast(int, value) for value in numeric_values) != self.observation_count
274
+ ):
275
+ raise ConfigurationError("Count fingerprint values must sum to observation_count.")
276
+ if self.mode is KmerFingerprintMode.BINARY and any(
277
+ not isinstance(value, int) or isinstance(value, bool) or value not in (0, 1)
278
+ for value in numeric_values
279
+ ):
280
+ raise ConfigurationError("Binary fingerprint values must be zero or one.")
281
+ if self.mode is KmerFingerprintMode.FREQUENCY and any(
282
+ not isinstance(value, (int, float)) or value > 1 for value in numeric_values
283
+ ):
284
+ raise ConfigurationError("Frequency fingerprint values must be in [0, 1].")
285
+ if self.mode is KmerFingerprintMode.FREQUENCY:
286
+ expected_total = 1.0 if self.observation_count else 0.0
287
+ actual_total = sum(float(cast(Numeric, value)) for value in numeric_values)
288
+ if not math.isclose(actual_total, expected_total):
289
+ raise ConfigurationError(
290
+ "Frequency fingerprint values must sum to one, or zero with no observations."
291
+ )
292
+
293
+ @property
294
+ def dimension(self) -> int:
295
+ """Number of features in the immutable schema."""
296
+
297
+ return len(self.feature_names)
298
+
299
+ def dense_values(self) -> tuple[Numeric, ...]:
300
+ """Return values ordered exactly like :attr:`feature_names`."""
301
+
302
+ if isinstance(self.values, tuple):
303
+ return self.values
304
+ return tuple(cast(Numeric, self.values.get(name, 0)) for name in self.feature_names)
305
+
306
+ def sparse_values(self) -> FrozenDict:
307
+ """Return only non-zero values keyed by schema feature name."""
308
+
309
+ if isinstance(self.values, FrozenDict):
310
+ return self.values
311
+ return FrozenDict(
312
+ {
313
+ name: value
314
+ for name, value in zip(self.feature_names, self.values, strict=True)
315
+ if value != 0
316
+ }
317
+ )
318
+
319
+
320
+ KmerFingerprintResult = FingerprintResult
321
+
322
+
323
+ @dataclass(frozen=True)
324
+ class BitFingerprintResult(RepresentationResult):
325
+ """Fixed-schema binary fingerprint in dense or sparse form."""
326
+
327
+ representation: FingerprintRepresentation
328
+ feature_names: tuple[str, ...]
329
+ values: BitFingerprintValues
330
+ parameters: FrozenDict
331
+ observation_count: int
332
+ ignored_ambiguity_count: int
333
+ max_dimension: int
334
+
335
+ def __post_init__(self) -> None:
336
+ super().__post_init__()
337
+ if not isinstance(self.representation, FingerprintRepresentation):
338
+ raise ConfigurationError(
339
+ "Bit fingerprint representation must be FingerprintRepresentation."
340
+ )
341
+ if not self.feature_names or len(set(self.feature_names)) != len(self.feature_names):
342
+ raise ConfigurationError("Bit fingerprint feature_names must be unique and non-empty.")
343
+ if any(not isinstance(name, str) or not name for name in self.feature_names):
344
+ raise ConfigurationError(
345
+ "Bit fingerprint feature_names must be unique non-empty strings."
346
+ )
347
+ if (
348
+ isinstance(self.max_dimension, bool)
349
+ or not isinstance(self.max_dimension, int)
350
+ or self.max_dimension <= 0
351
+ or self.dimension > self.max_dimension
352
+ ):
353
+ raise ConfigurationError("Bit fingerprint max_dimension is inconsistent.")
354
+ if not isinstance(self.parameters, FrozenDict):
355
+ raise ConfigurationError("Bit fingerprint parameters must be a FrozenDict.")
356
+ for name in ("observation_count", "ignored_ambiguity_count"):
357
+ value = getattr(self, name)
358
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
359
+ raise ConfigurationError(f"Bit fingerprint {name} must be non-negative.")
360
+ if isinstance(self.values, tuple):
361
+ if self.representation is not FingerprintRepresentation.DENSE:
362
+ raise ConfigurationError(
363
+ "Tuple bit fingerprint values require dense representation."
364
+ )
365
+ if len(self.values) != self.dimension:
366
+ raise ConfigurationError("Dense bit fingerprint length does not match its schema.")
367
+ numeric_values: tuple[object, ...] = self.values
368
+ elif isinstance(self.values, FrozenDict):
369
+ if self.representation is not FingerprintRepresentation.SPARSE:
370
+ raise ConfigurationError(
371
+ "Mapping bit fingerprint values require sparse representation."
372
+ )
373
+ unknown_features = set(self.values) - set(self.feature_names)
374
+ if unknown_features:
375
+ raise ConfigurationError(
376
+ "Sparse bit fingerprint contains features outside its schema.",
377
+ context={"features": sorted(unknown_features)},
378
+ )
379
+ numeric_values = tuple(self.values.values())
380
+ else:
381
+ raise ConfigurationError("Bit fingerprint values must be a tuple or FrozenDict.")
382
+ if any(
383
+ not isinstance(value, int) or isinstance(value, bool) or value not in (0, 1)
384
+ for value in numeric_values
385
+ ):
386
+ raise ConfigurationError("Bit fingerprint values must be zero or one.")
387
+ if isinstance(self.values, FrozenDict) and any(value == 0 for value in numeric_values):
388
+ raise ConfigurationError("Sparse bit fingerprint values must omit zero entries.")
389
+
390
+ @property
391
+ def dimension(self) -> int:
392
+ """Number of bits in the immutable schema."""
393
+
394
+ return len(self.feature_names)
395
+
396
+ @property
397
+ def set_bit_count(self) -> int:
398
+ """Number of bits set to one."""
399
+
400
+ if isinstance(self.values, FrozenDict):
401
+ return len(self.values)
402
+ return sum(self.values)
403
+
404
+ def dense_values(self) -> tuple[int, ...]:
405
+ """Return bits ordered exactly like :attr:`feature_names`."""
406
+
407
+ if isinstance(self.values, tuple):
408
+ return self.values
409
+ return tuple(cast(int, self.values.get(name, 0)) for name in self.feature_names)
410
+
411
+ def sparse_values(self) -> FrozenDict:
412
+ """Return only set bits keyed by schema feature name."""
413
+
414
+ if isinstance(self.values, FrozenDict):
415
+ return self.values
416
+ return FrozenDict(
417
+ {
418
+ name: value
419
+ for name, value in zip(self.feature_names, self.values, strict=True)
420
+ if value
421
+ }
422
+ )
423
+
424
+
425
+ @dataclass(frozen=True)
426
+ class SketchResult:
427
+ """Versioned set of deterministic 64-bit k-mer hashes."""
428
+
429
+ name: str
430
+ method: str
431
+ schema_version: str
432
+ sequence_id: str | None
433
+ symbol_length: int
434
+ k: int
435
+ canonical: bool
436
+ seed: int
437
+ selection: str
438
+ hashes: tuple[int, ...]
439
+ num_hashes: int | None
440
+ scaled: int | None
441
+ threshold: int | None
442
+ observation_count: int
443
+ unique_hash_count: int
444
+ max_hashes: int
445
+ parameters: FrozenDict
446
+
447
+ def __post_init__(self) -> None:
448
+ for name in ("name", "method", "schema_version"):
449
+ value = getattr(self, name)
450
+ if not isinstance(value, str) or not value.strip():
451
+ raise ConfigurationError(f"Sketch {name} must be non-empty.")
452
+ if self.sequence_id is not None and (
453
+ not isinstance(self.sequence_id, str) or not self.sequence_id.strip()
454
+ ):
455
+ raise ConfigurationError("Sketch sequence_id must be non-empty or None.")
456
+ for name in ("symbol_length", "observation_count", "unique_hash_count"):
457
+ value = getattr(self, name)
458
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
459
+ raise ConfigurationError(f"Sketch {name} must be a non-negative integer.")
460
+ for name in ("k", "max_hashes"):
461
+ value = getattr(self, name)
462
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
463
+ raise ConfigurationError(f"Sketch {name} must be a positive integer.")
464
+ if not isinstance(self.canonical, bool):
465
+ raise ConfigurationError("Sketch canonical must be boolean.")
466
+ if (
467
+ isinstance(self.seed, bool)
468
+ or not isinstance(self.seed, int)
469
+ or not 0 <= self.seed < 2**64
470
+ ):
471
+ raise ConfigurationError("Sketch seed must be an unsigned 64-bit integer.")
472
+ if self.selection not in {"bottom_k", "scaled"}:
473
+ raise ConfigurationError("Sketch selection must be bottom_k or scaled.")
474
+ if len(self.hashes) > self.max_hashes or len(set(self.hashes)) != len(self.hashes):
475
+ raise ConfigurationError("Sketch hashes violate uniqueness or size limits.")
476
+ if tuple(sorted(self.hashes)) != self.hashes or any(
477
+ isinstance(item, bool) or not isinstance(item, int) or not 0 <= item < 2**64
478
+ for item in self.hashes
479
+ ):
480
+ raise ConfigurationError("Sketch hashes must be sorted unsigned 64-bit integers.")
481
+ if self.selection == "bottom_k":
482
+ if self.num_hashes is None or self.scaled is not None or self.threshold is not None:
483
+ raise ConfigurationError("Bottom-k sketch selection fields are inconsistent.")
484
+ if (
485
+ isinstance(self.num_hashes, bool)
486
+ or not isinstance(self.num_hashes, int)
487
+ or not 0 < self.num_hashes <= self.max_hashes
488
+ or len(self.hashes) > self.num_hashes
489
+ ):
490
+ raise ConfigurationError("Bottom-k num_hashes is invalid or exceeded.")
491
+ elif self.scaled is None or self.threshold is None or self.num_hashes is not None:
492
+ raise ConfigurationError("Scaled sketch selection fields are inconsistent.")
493
+ else:
494
+ if (
495
+ isinstance(self.scaled, bool)
496
+ or not isinstance(self.scaled, int)
497
+ or not 0 < self.scaled <= 2**64
498
+ or isinstance(self.threshold, bool)
499
+ or not isinstance(self.threshold, int)
500
+ or self.threshold != 2**64 // self.scaled
501
+ or any(item >= self.threshold for item in self.hashes)
502
+ ):
503
+ raise ConfigurationError("Scaled sketch threshold fields are invalid.")
504
+ if self.unique_hash_count < len(self.hashes):
505
+ raise ConfigurationError("unique_hash_count cannot be smaller than retained hashes.")
506
+ if self.unique_hash_count > self.observation_count:
507
+ raise ConfigurationError("unique_hash_count cannot exceed observation_count.")
508
+ expected_observations = max(0, self.symbol_length - self.k + 1)
509
+ if self.observation_count != expected_observations:
510
+ raise ConfigurationError(
511
+ "Sketch observation_count does not match sequence length and k."
512
+ )
513
+ if not isinstance(self.parameters, FrozenDict):
514
+ raise ConfigurationError("Sketch parameters must be FrozenDict.")
515
+
516
+ def to_dict(self) -> dict[str, Any]:
517
+ return cast(dict[str, Any], to_json_compatible(self))
518
+
519
+
520
+ __all__ = [
521
+ "BitFingerprintResult",
522
+ "BitFingerprintValues",
523
+ "FingerprintResult",
524
+ "FingerprintValues",
525
+ "IntegerEncodingResult",
526
+ "KmerFingerprintResult",
527
+ "Numeric",
528
+ "OneHotEncodingResult",
529
+ "RepresentationResult",
530
+ "SketchResult",
531
+ ]