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,520 @@
1
+ """Immutable, JSON-compatible summaries of dataset operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal, cast
8
+
9
+ from dnakit.core import DNASet
10
+ from dnakit.core._json import FrozenDict, JSONValue, to_json_compatible
11
+ from dnakit.exceptions import ConfigurationError
12
+
13
+ DedupAction = Literal["deduplicated", "kept_all", "dropped"]
14
+ Orientation = Literal["forward", "reverse_complement"]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class DedupGroup:
19
+ """One equivalence class and its chosen handling."""
20
+
21
+ group_index: int
22
+ representative_id: str
23
+ member_ids: tuple[str, ...]
24
+ orientations: tuple[Orientation, ...]
25
+ conflict: bool = False
26
+ conflict_values: tuple[JSONValue, ...] = ()
27
+ missing_conflict_value_count: int = 0
28
+ action: DedupAction = "deduplicated"
29
+ rotation_offsets: tuple[int | None, ...] = ()
30
+ rotation_offset_definition: str | None = None
31
+
32
+ def __post_init__(self) -> None:
33
+ if (
34
+ isinstance(self.group_index, bool)
35
+ or not isinstance(self.group_index, int)
36
+ or self.group_index < 0
37
+ ):
38
+ raise ConfigurationError("DedupGroup group_index must be non-negative.")
39
+ if not self.representative_id or not self.member_ids:
40
+ raise ConfigurationError("DedupGroup IDs must be non-empty.")
41
+ if len(self.member_ids) != len(self.orientations):
42
+ raise ConfigurationError("DedupGroup orientations must align with member_ids.")
43
+ if self.representative_id not in self.member_ids:
44
+ raise ConfigurationError("DedupGroup representative must be a member.")
45
+ if self.action not in {"deduplicated", "kept_all", "dropped"}:
46
+ raise ConfigurationError("DedupGroup action is invalid.")
47
+ if self.rotation_offsets and len(self.rotation_offsets) != len(self.member_ids):
48
+ raise ConfigurationError("DedupGroup rotation offsets must align with member_ids.")
49
+ if any(
50
+ value is not None
51
+ and (isinstance(value, bool) or not isinstance(value, int) or value < 0)
52
+ for value in self.rotation_offsets
53
+ ):
54
+ raise ConfigurationError("DedupGroup rotation offsets must be non-negative or None.")
55
+ if (
56
+ isinstance(self.missing_conflict_value_count, bool)
57
+ or not isinstance(self.missing_conflict_value_count, int)
58
+ or self.missing_conflict_value_count < 0
59
+ ):
60
+ raise ConfigurationError("Missing conflict value count must be non-negative.")
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class DeduplicationResult:
65
+ """Non-redundant records, equivalence groups, and conflict audit."""
66
+
67
+ records: DNASet
68
+ groups: tuple[DedupGroup, ...]
69
+ equivalence: str
70
+ representative_policy: str
71
+ conflict_field: str | None
72
+ conflict_policy: str
73
+ input_count: int
74
+ output_count: int
75
+ duplicate_count: int
76
+ conflicted_group_count: int
77
+ removed_count: int
78
+ merge_metadata: bool = False
79
+
80
+ def __post_init__(self) -> None:
81
+ if self.output_count != len(self.records):
82
+ raise ConfigurationError("Deduplication output_count does not match records.")
83
+ if self.input_count < 0 or self.output_count < 0 or self.output_count > self.input_count:
84
+ raise ConfigurationError("Deduplication counts are inconsistent.")
85
+ if self.removed_count != self.input_count - self.output_count:
86
+ raise ConfigurationError("Deduplication removed_count is inconsistent.")
87
+ if self.duplicate_count != self.input_count - len(self.groups):
88
+ raise ConfigurationError("Deduplication duplicate_count is inconsistent.")
89
+ if not isinstance(self.merge_metadata, bool):
90
+ raise ConfigurationError("Deduplication merge_metadata must be a boolean.")
91
+
92
+ def to_dict(self) -> dict[str, Any]:
93
+ """Return an audit payload without duplicating full sequences."""
94
+
95
+ payload = {
96
+ "equivalence": self.equivalence,
97
+ "representative_policy": self.representative_policy,
98
+ "conflict_field": self.conflict_field,
99
+ "conflict_policy": self.conflict_policy,
100
+ "merge_metadata": self.merge_metadata,
101
+ "input_count": self.input_count,
102
+ "output_count": self.output_count,
103
+ "duplicate_count": self.duplicate_count,
104
+ "conflicted_group_count": self.conflicted_group_count,
105
+ "removed_count": self.removed_count,
106
+ "output_ids": self.records.ids,
107
+ "groups": self.groups,
108
+ }
109
+ return cast(dict[str, Any], to_json_compatible(payload))
110
+
111
+
112
+ @dataclass(frozen=True, slots=True)
113
+ class SplitAssignment:
114
+ """Assignment of one original record position to one named split."""
115
+
116
+ input_index: int
117
+ record_id: str
118
+ split: str
119
+
120
+ def __post_init__(self) -> None:
121
+ if (
122
+ isinstance(self.input_index, bool)
123
+ or not isinstance(self.input_index, int)
124
+ or self.input_index < 0
125
+ ):
126
+ raise ConfigurationError("SplitAssignment input_index must be non-negative.")
127
+ if not self.record_id or not self.split:
128
+ raise ConfigurationError("SplitAssignment names must be non-empty.")
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class SplitSubset:
133
+ """One named materialized dataset subset."""
134
+
135
+ name: str
136
+ records: DNASet
137
+
138
+ def __post_init__(self) -> None:
139
+ if not isinstance(self.name, str) or not self.name.strip():
140
+ raise ConfigurationError("SplitSubset name must be non-empty.")
141
+
142
+
143
+ @dataclass(frozen=True, slots=True)
144
+ class SplitResult:
145
+ """Materialized subsets plus deterministic assignment metadata."""
146
+
147
+ subsets: tuple[SplitSubset, ...]
148
+ assignments: tuple[SplitAssignment, ...]
149
+ method: str
150
+ ratios: FrozenDict
151
+ counts: FrozenDict
152
+ seed: int
153
+ preserve_order: bool
154
+ metadata_key: str | None = None
155
+ component_count: int | None = None
156
+ pairwise_comparison_count: int = 0
157
+ similarity_method: str | None = None
158
+ similarity_threshold: float | None = None
159
+ shuffle: bool = True
160
+ similarity_k: int | None = None
161
+ max_pairwise_records: int | None = None
162
+ assignment_strategy: str | None = None
163
+
164
+ def __post_init__(self) -> None:
165
+ names = tuple(subset.name for subset in self.subsets)
166
+ if len(names) != len(set(names)) or set(names) != set(self.ratios):
167
+ raise ConfigurationError("SplitResult subset names must match ratio names.")
168
+ if set(self.counts) != set(names):
169
+ raise ConfigurationError("SplitResult count names must match subset names.")
170
+ if any(self.counts[name] != len(self.get(name)) for name in names):
171
+ raise ConfigurationError("SplitResult counts do not match materialized subsets.")
172
+ if sum(cast(int, self.counts[name]) for name in names) != len(self.assignments):
173
+ raise ConfigurationError("SplitResult assignments do not match subset counts.")
174
+ if not isinstance(self.shuffle, bool):
175
+ raise ConfigurationError("SplitResult shuffle must be a boolean.")
176
+ for field_name in ("similarity_k", "max_pairwise_records"):
177
+ value = getattr(self, field_name)
178
+ if value is not None and (
179
+ isinstance(value, bool) or not isinstance(value, int) or value <= 0
180
+ ):
181
+ raise ConfigurationError(f"SplitResult {field_name} must be positive or None.")
182
+ if self.assignment_strategy is not None and (
183
+ not isinstance(self.assignment_strategy, str) or not self.assignment_strategy.strip()
184
+ ):
185
+ raise ConfigurationError("SplitResult assignment_strategy must be non-empty or None.")
186
+
187
+ def get(self, name: str) -> DNASet:
188
+ """Return one subset by name."""
189
+
190
+ for subset in self.subsets:
191
+ if subset.name == name:
192
+ return subset.records
193
+ raise KeyError(name)
194
+
195
+ def to_dict(self) -> dict[str, Any]:
196
+ """Return an audit payload containing IDs rather than full sequences."""
197
+
198
+ payload = {
199
+ "method": self.method,
200
+ "ratios": self.ratios,
201
+ "counts": self.counts,
202
+ "seed": self.seed,
203
+ "shuffle": self.shuffle,
204
+ "preserve_order": self.preserve_order,
205
+ "metadata_key": self.metadata_key,
206
+ "component_count": self.component_count,
207
+ "pairwise_comparison_count": self.pairwise_comparison_count,
208
+ "similarity_method": self.similarity_method,
209
+ "similarity_threshold": self.similarity_threshold,
210
+ "similarity_k": self.similarity_k,
211
+ "max_pairwise_records": self.max_pairwise_records,
212
+ "assignment_strategy": self.assignment_strategy,
213
+ "subsets": {subset.name: subset.records.ids for subset in self.subsets},
214
+ "assignments": self.assignments,
215
+ }
216
+ return cast(dict[str, Any], to_json_compatible(payload))
217
+
218
+
219
+ @dataclass(frozen=True, slots=True)
220
+ class IUPACDuplicateGroup:
221
+ """Complete-link group whose member symbols are pairwise compatible."""
222
+
223
+ group_index: int
224
+ representative_id: str
225
+ member_ids: tuple[str, ...]
226
+ relation: Literal["singleton", "identical", "compatible"]
227
+
228
+
229
+ @dataclass(frozen=True, slots=True)
230
+ class IUPACPairRelation:
231
+ """Auditable relation for one exhaustively compared input pair."""
232
+
233
+ left_index: int
234
+ right_index: int
235
+ left_id: str
236
+ right_id: str
237
+ relation: Literal["identical", "compatible", "conflict"]
238
+
239
+
240
+ @dataclass(frozen=True, slots=True)
241
+ class IUPACDeduplicationResult:
242
+ records: DNASet
243
+ groups: tuple[IUPACDuplicateGroup, ...]
244
+ pair_relations: tuple[IUPACPairRelation, ...]
245
+ representative_policy: str
246
+ compatibility_rule: str
247
+ grouping_strategy: str
248
+ input_count: int
249
+ output_count: int
250
+ pairwise_comparison_count: int
251
+ identical_pair_count: int
252
+ compatible_pair_count: int
253
+ conflict_pair_count: int
254
+ max_records: int
255
+ max_pairwise_comparisons: int
256
+
257
+ def to_dict(self) -> dict[str, Any]:
258
+ return cast(dict[str, Any], to_json_compatible(self))
259
+
260
+
261
+ @dataclass(frozen=True, slots=True)
262
+ class Cluster:
263
+ cluster_index: int
264
+ member_indices: tuple[int, ...]
265
+ member_ids: tuple[str, ...]
266
+ representative_index: int
267
+ representative_id: str
268
+
269
+
270
+ @dataclass(frozen=True, slots=True)
271
+ class ClusteringResult:
272
+ clusters: tuple[Cluster, ...]
273
+ labels: tuple[int, ...]
274
+ representatives: DNASet
275
+ method: str
276
+ threshold: float
277
+ k: int
278
+ canonical: bool
279
+ representative_policy: str
280
+ clustering_strategy: str
281
+ seed: int
282
+ seed_used: bool
283
+ pairwise_comparison_count: int
284
+ max_records: int
285
+ max_pairwise_comparisons: int
286
+ max_alignment_cells: int
287
+
288
+ def to_dict(self) -> dict[str, Any]:
289
+ return cast(dict[str, Any], to_json_compatible(self))
290
+
291
+
292
+ @dataclass(frozen=True, slots=True)
293
+ class NeuralClusteringResult:
294
+ """K-means groups obtained from sequence-level DNA model representations."""
295
+
296
+ clusters: tuple[Cluster, ...]
297
+ labels: tuple[int, ...]
298
+ representatives: DNASet
299
+ centers: tuple[tuple[float, ...], ...]
300
+ model_name: str
301
+ checkpoint_path: str | None
302
+ pooling: str
303
+ embedding_dimension: int
304
+ clustering_dimension: int
305
+ n_clusters: int
306
+ inertia: float
307
+ silhouette_score: float | None
308
+ normalize: bool
309
+ pca_components: int | None
310
+ pca_explained_variance_ratio: tuple[float, ...]
311
+ seed: int
312
+ n_init: int
313
+ max_iter: int
314
+ iteration_count: int
315
+ input_count: int
316
+ clustering_strategy: str = "l2-normalize-optional-pca-kmeans++-lloyd"
317
+
318
+ def __post_init__(self) -> None:
319
+ if len(self.labels) != self.input_count:
320
+ raise ConfigurationError("Neural clustering labels do not match input_count.")
321
+ if len(self.clusters) != self.n_clusters or len(self.centers) != self.n_clusters:
322
+ raise ConfigurationError("Neural clustering cluster counts are inconsistent.")
323
+ if len(self.representatives) != self.n_clusters:
324
+ raise ConfigurationError("Neural clustering representatives are inconsistent.")
325
+ if any(len(center) != self.clustering_dimension for center in self.centers):
326
+ raise ConfigurationError("Neural clustering center dimensions are inconsistent.")
327
+ numeric = [self.inertia, *(value for center in self.centers for value in center)]
328
+ if self.silhouette_score is not None:
329
+ numeric.append(self.silhouette_score)
330
+ numeric.extend(self.pca_explained_variance_ratio)
331
+ if any(not math.isfinite(float(value)) for value in numeric):
332
+ raise ConfigurationError("Neural clustering metrics must be finite.")
333
+
334
+ def to_dict(self) -> dict[str, Any]:
335
+ """Return JSON-compatible labels, centers, scores, and provenance."""
336
+
337
+ return cast(dict[str, Any], to_json_compatible(self))
338
+
339
+
340
+ @dataclass(frozen=True, slots=True)
341
+ class LinkageStep:
342
+ step_index: int
343
+ left_node: int
344
+ right_node: int
345
+ new_node: int
346
+ distance: float
347
+ member_count: int
348
+
349
+
350
+ @dataclass(frozen=True, slots=True)
351
+ class HierarchicalClusteringResult:
352
+ record_ids: tuple[str, ...]
353
+ linkage: tuple[LinkageStep, ...]
354
+ method: str
355
+ linkage_method: str
356
+ distance_definition: str
357
+ k: int
358
+ canonical: bool
359
+ pairwise_comparison_count: int
360
+ linkage_distance_update_count: int
361
+ max_records: int
362
+ max_pairwise_comparisons: int
363
+ max_alignment_cells: int
364
+
365
+ def to_dict(self) -> dict[str, Any]:
366
+ return cast(dict[str, Any], to_json_compatible(self))
367
+
368
+
369
+ @dataclass(frozen=True, slots=True)
370
+ class RepresentativeSelectionResult:
371
+ representatives: DNASet
372
+ representative_ids: tuple[str, ...]
373
+ policy: str
374
+ cluster_count: int
375
+ medoid_metric: str | None
376
+ pairwise_comparison_count: int
377
+
378
+ def to_dict(self) -> dict[str, Any]:
379
+ return cast(dict[str, Any], to_json_compatible(self))
380
+
381
+
382
+ @dataclass(frozen=True, slots=True)
383
+ class TemporalSplitResult:
384
+ subsets: tuple[SplitSubset, ...]
385
+ assignments: tuple[SplitAssignment, ...]
386
+ counts: FrozenDict
387
+ ratios: FrozenDict
388
+ metadata_key: str
389
+ cutoffs: tuple[str, ...]
390
+ strategy: str
391
+ preserve_order: bool
392
+ timezone_policy: str
393
+ max_records: int
394
+
395
+ def get(self, name: str) -> DNASet:
396
+ for subset in self.subsets:
397
+ if subset.name == name:
398
+ return subset.records
399
+ raise KeyError(name)
400
+
401
+ def to_dict(self) -> dict[str, Any]:
402
+ return cast(dict[str, Any], to_json_compatible(self))
403
+
404
+
405
+ @dataclass(frozen=True, slots=True)
406
+ class JointSplitResult:
407
+ subsets: tuple[SplitSubset, ...]
408
+ assignments: tuple[SplitAssignment, ...]
409
+ counts: FrozenDict
410
+ target_counts: FrozenDict
411
+ achieved_ratios: FrozenDict
412
+ group_keys: tuple[str, ...]
413
+ label_key: str | None
414
+ similarity_threshold: float | None
415
+ similarity_k: int
416
+ similarity_canonical: bool
417
+ seed: int
418
+ strategy: str
419
+ feasible: bool
420
+ relaxed: bool
421
+ relaxations: tuple[str, ...]
422
+ max_ratio_deviation: float
423
+ ratio_tolerance: float
424
+ atomic_unit_count: int
425
+ pairwise_comparison_count: int
426
+ max_records: int
427
+ max_pairwise_comparisons: int
428
+ infeasible_policy: str
429
+
430
+ def get(self, name: str) -> DNASet:
431
+ for subset in self.subsets:
432
+ if subset.name == name:
433
+ return subset.records
434
+ raise KeyError(name)
435
+
436
+ def to_dict(self) -> dict[str, Any]:
437
+ return cast(dict[str, Any], to_json_compatible(self))
438
+
439
+
440
+ @dataclass(frozen=True, slots=True)
441
+ class LeakageEvent:
442
+ left_split: str
443
+ right_split: str
444
+ left_id: str
445
+ right_id: str
446
+ left_index: int
447
+ right_index: int
448
+ similarity: float
449
+ exact: bool
450
+
451
+
452
+ @dataclass(frozen=True, slots=True)
453
+ class LeakageReport:
454
+ events: tuple[LeakageEvent, ...]
455
+ method: str
456
+ threshold: float
457
+ strictness: str
458
+ exact_event_count: int
459
+ high_similarity_event_count: int
460
+ cross_pair_count: int
461
+ max_records: int
462
+ max_cross_pairs: int
463
+ max_events: int
464
+ truncated: bool
465
+ k: int
466
+ canonical: bool
467
+ max_alignment_cells: int
468
+
469
+ @property
470
+ def has_leakage(self) -> bool:
471
+ return bool(self.events)
472
+
473
+ def to_dict(self) -> dict[str, Any]:
474
+ return cast(dict[str, Any], to_json_compatible(self))
475
+
476
+
477
+ @dataclass(frozen=True, slots=True)
478
+ class SplitQualityResult:
479
+ total_count: int
480
+ counts: FrozenDict
481
+ target_ratios: FrozenDict
482
+ achieved_ratios: FrozenDict
483
+ ratio_deviations: FrozenDict
484
+ max_ratio_deviation: float
485
+ label_key: str | None
486
+ label_total_variation_by_split: FrozenDict
487
+ group_keys: tuple[str, ...]
488
+ group_leak_count: int
489
+ leakage_event_count: int | None
490
+ quality_score: float
491
+ score_definition: str
492
+ parameters: FrozenDict
493
+
494
+ def to_dict(self) -> dict[str, Any]:
495
+ return cast(dict[str, Any], to_json_compatible(self))
496
+
497
+
498
+ __all__ = [
499
+ "Cluster",
500
+ "ClusteringResult",
501
+ "DedupAction",
502
+ "DedupGroup",
503
+ "DeduplicationResult",
504
+ "HierarchicalClusteringResult",
505
+ "IUPACDeduplicationResult",
506
+ "IUPACDuplicateGroup",
507
+ "IUPACPairRelation",
508
+ "JointSplitResult",
509
+ "LeakageEvent",
510
+ "LeakageReport",
511
+ "LinkageStep",
512
+ "NeuralClusteringResult",
513
+ "Orientation",
514
+ "RepresentativeSelectionResult",
515
+ "SplitAssignment",
516
+ "SplitQualityResult",
517
+ "SplitResult",
518
+ "SplitSubset",
519
+ "TemporalSplitResult",
520
+ ]