dnakit 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Validated configuration for deterministic dataset operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Literal, TypeAlias
|
|
9
|
+
|
|
10
|
+
from dnakit.core._json import FrozenDict
|
|
11
|
+
from dnakit.exceptions import ConfigurationError
|
|
12
|
+
from dnakit.representations.models import RepresentationConfig
|
|
13
|
+
|
|
14
|
+
DedupEquivalence: TypeAlias = Literal[
|
|
15
|
+
"exact",
|
|
16
|
+
"reverse_complement",
|
|
17
|
+
"circular",
|
|
18
|
+
"circular_reverse_complement",
|
|
19
|
+
]
|
|
20
|
+
RepresentativePolicy: TypeAlias = Literal["first", "last", "best_quality"]
|
|
21
|
+
ConflictPolicy: TypeAlias = Literal["error", "drop_group", "keep_representative", "keep_all"]
|
|
22
|
+
SplitMethod: TypeAlias = Literal["random", "hash", "stratified", "group", "similarity"]
|
|
23
|
+
MissingMetadataPolicy: TypeAlias = Literal["error", "separate"]
|
|
24
|
+
SimilarityAmbiguityPolicy: TypeAlias = Literal["error", "ignore"]
|
|
25
|
+
SimilarityGapPolicy: TypeAlias = Literal["error", "split"]
|
|
26
|
+
ClusterMethod: TypeAlias = Literal["identity", "edit", "kmer", "fingerprint"]
|
|
27
|
+
LinkageMethod: TypeAlias = Literal["single", "complete", "average"]
|
|
28
|
+
AdvancedRepresentativePolicy: TypeAlias = Literal[
|
|
29
|
+
"first", "shortest", "longest", "best_quality", "medoid"
|
|
30
|
+
]
|
|
31
|
+
InfeasiblePolicy: TypeAlias = Literal["error", "relax"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class DeduplicationConfig:
|
|
36
|
+
"""Control representative selection and duplicate-label conflicts."""
|
|
37
|
+
|
|
38
|
+
representative_policy: RepresentativePolicy = "first"
|
|
39
|
+
conflict_field: str | None = None
|
|
40
|
+
conflict_policy: ConflictPolicy = "error"
|
|
41
|
+
merge_metadata: bool = False
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if self.representative_policy not in {"first", "last", "best_quality"}:
|
|
45
|
+
raise ConfigurationError(
|
|
46
|
+
"Unknown representative_policy.",
|
|
47
|
+
code="INVALID_REPRESENTATIVE_POLICY",
|
|
48
|
+
context={"representative_policy": self.representative_policy},
|
|
49
|
+
)
|
|
50
|
+
if self.conflict_policy not in {
|
|
51
|
+
"error",
|
|
52
|
+
"drop_group",
|
|
53
|
+
"keep_representative",
|
|
54
|
+
"keep_all",
|
|
55
|
+
}:
|
|
56
|
+
raise ConfigurationError(
|
|
57
|
+
"Unknown conflict_policy.",
|
|
58
|
+
code="INVALID_CONFLICT_POLICY",
|
|
59
|
+
context={"conflict_policy": self.conflict_policy},
|
|
60
|
+
)
|
|
61
|
+
if self.conflict_field is not None and (
|
|
62
|
+
not isinstance(self.conflict_field, str) or not self.conflict_field.strip()
|
|
63
|
+
):
|
|
64
|
+
raise ConfigurationError(
|
|
65
|
+
"conflict_field must be None or a non-empty metadata key.",
|
|
66
|
+
code="INVALID_CONFLICT_FIELD",
|
|
67
|
+
)
|
|
68
|
+
if not isinstance(self.merge_metadata, bool):
|
|
69
|
+
raise ConfigurationError(
|
|
70
|
+
"merge_metadata must be a boolean.",
|
|
71
|
+
code="INVALID_DEDUPLICATION_CONFIG",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _default_ratios() -> dict[str, float]:
|
|
76
|
+
return {"train": 0.8, "valid": 0.1, "test": 0.1}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class SplitConfig:
|
|
81
|
+
"""Control random, stable-hash, stratified, group-aware, or k-mer similarity splitting."""
|
|
82
|
+
|
|
83
|
+
method: SplitMethod = "random"
|
|
84
|
+
ratios: Mapping[str, float] = field(default_factory=_default_ratios)
|
|
85
|
+
seed: int = 0
|
|
86
|
+
shuffle: bool = True
|
|
87
|
+
preserve_order: bool = True
|
|
88
|
+
metadata_key: str | None = None
|
|
89
|
+
missing_metadata_policy: MissingMetadataPolicy = "error"
|
|
90
|
+
similarity_k: int = 3
|
|
91
|
+
similarity_threshold: float = 0.8
|
|
92
|
+
similarity_ambiguity_policy: SimilarityAmbiguityPolicy = "error"
|
|
93
|
+
similarity_gap_policy: SimilarityGapPolicy = "error"
|
|
94
|
+
max_pairwise_records: int = 5_000
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
if self.method not in {"random", "hash", "stratified", "group", "similarity"}:
|
|
98
|
+
raise ConfigurationError(
|
|
99
|
+
"Unknown dataset split method.",
|
|
100
|
+
code="INVALID_SPLIT_METHOD",
|
|
101
|
+
context={"method": self.method},
|
|
102
|
+
)
|
|
103
|
+
if not isinstance(self.ratios, Mapping):
|
|
104
|
+
raise ConfigurationError(
|
|
105
|
+
"ratios must be a mapping of split names to fractions.",
|
|
106
|
+
code="INVALID_SPLIT_RATIOS",
|
|
107
|
+
)
|
|
108
|
+
ratio_items = tuple(self.ratios.items())
|
|
109
|
+
if len(ratio_items) < 2:
|
|
110
|
+
raise ConfigurationError(
|
|
111
|
+
"At least two split ratios are required.",
|
|
112
|
+
code="INVALID_SPLIT_RATIOS",
|
|
113
|
+
)
|
|
114
|
+
normalized_ratios: dict[str, float] = {}
|
|
115
|
+
for name, value in ratio_items:
|
|
116
|
+
if not isinstance(name, str) or not name.strip():
|
|
117
|
+
raise ConfigurationError(
|
|
118
|
+
"Split names must be non-empty strings.",
|
|
119
|
+
code="INVALID_SPLIT_RATIOS",
|
|
120
|
+
)
|
|
121
|
+
if (
|
|
122
|
+
isinstance(value, bool)
|
|
123
|
+
or not isinstance(value, (int, float))
|
|
124
|
+
or not math.isfinite(value)
|
|
125
|
+
or value <= 0
|
|
126
|
+
):
|
|
127
|
+
raise ConfigurationError(
|
|
128
|
+
"Every split ratio must be a finite positive number.",
|
|
129
|
+
code="INVALID_SPLIT_RATIOS",
|
|
130
|
+
context={"split": name, "ratio": value},
|
|
131
|
+
)
|
|
132
|
+
normalized_ratios[name] = float(value)
|
|
133
|
+
ratio_sum = sum(normalized_ratios.values())
|
|
134
|
+
if not math.isclose(ratio_sum, 1.0, rel_tol=0.0, abs_tol=1e-9):
|
|
135
|
+
raise ConfigurationError(
|
|
136
|
+
"Split ratios must sum to 1.0.",
|
|
137
|
+
code="INVALID_SPLIT_RATIOS",
|
|
138
|
+
context={"sum": ratio_sum},
|
|
139
|
+
)
|
|
140
|
+
if isinstance(self.seed, bool) or not isinstance(self.seed, int):
|
|
141
|
+
raise ConfigurationError(
|
|
142
|
+
"seed must be an integer.",
|
|
143
|
+
code="INVALID_SPLIT_SEED",
|
|
144
|
+
)
|
|
145
|
+
for field_name in ("shuffle", "preserve_order"):
|
|
146
|
+
if not isinstance(getattr(self, field_name), bool):
|
|
147
|
+
raise ConfigurationError(
|
|
148
|
+
f"{field_name} must be a boolean.",
|
|
149
|
+
code="INVALID_SPLIT_CONFIG",
|
|
150
|
+
)
|
|
151
|
+
if self.method in {"stratified", "group"} and (
|
|
152
|
+
not isinstance(self.metadata_key, str) or not self.metadata_key.strip()
|
|
153
|
+
):
|
|
154
|
+
raise ConfigurationError(
|
|
155
|
+
f"metadata_key is required for method={self.method!r}.",
|
|
156
|
+
code="SPLIT_METADATA_KEY_REQUIRED",
|
|
157
|
+
)
|
|
158
|
+
if self.metadata_key is not None and (
|
|
159
|
+
not isinstance(self.metadata_key, str) or not self.metadata_key.strip()
|
|
160
|
+
):
|
|
161
|
+
raise ConfigurationError(
|
|
162
|
+
"metadata_key must be None or a non-empty string.",
|
|
163
|
+
code="INVALID_SPLIT_METADATA_KEY",
|
|
164
|
+
)
|
|
165
|
+
if self.missing_metadata_policy not in {"error", "separate"}:
|
|
166
|
+
raise ConfigurationError(
|
|
167
|
+
"missing_metadata_policy must be 'error' or 'separate'.",
|
|
168
|
+
code="INVALID_MISSING_METADATA_POLICY",
|
|
169
|
+
)
|
|
170
|
+
if (
|
|
171
|
+
isinstance(self.similarity_k, bool)
|
|
172
|
+
or not isinstance(self.similarity_k, int)
|
|
173
|
+
or self.similarity_k <= 0
|
|
174
|
+
):
|
|
175
|
+
raise ConfigurationError(
|
|
176
|
+
"similarity_k must be a positive integer.",
|
|
177
|
+
code="INVALID_SIMILARITY_SPLIT_CONFIG",
|
|
178
|
+
)
|
|
179
|
+
if (
|
|
180
|
+
isinstance(self.similarity_threshold, bool)
|
|
181
|
+
or not isinstance(self.similarity_threshold, (int, float))
|
|
182
|
+
or not math.isfinite(self.similarity_threshold)
|
|
183
|
+
or not 0 <= self.similarity_threshold <= 1
|
|
184
|
+
):
|
|
185
|
+
raise ConfigurationError(
|
|
186
|
+
"similarity_threshold must be finite and within [0, 1].",
|
|
187
|
+
code="INVALID_SIMILARITY_SPLIT_CONFIG",
|
|
188
|
+
)
|
|
189
|
+
if self.similarity_ambiguity_policy not in {"error", "ignore"}:
|
|
190
|
+
raise ConfigurationError(
|
|
191
|
+
"similarity_ambiguity_policy must be 'error' or 'ignore'.",
|
|
192
|
+
code="INVALID_SIMILARITY_SPLIT_CONFIG",
|
|
193
|
+
)
|
|
194
|
+
if self.similarity_gap_policy not in {"error", "split"}:
|
|
195
|
+
raise ConfigurationError(
|
|
196
|
+
"similarity_gap_policy must be 'error' or 'split'.",
|
|
197
|
+
code="INVALID_SIMILARITY_SPLIT_CONFIG",
|
|
198
|
+
)
|
|
199
|
+
if (
|
|
200
|
+
isinstance(self.max_pairwise_records, bool)
|
|
201
|
+
or not isinstance(self.max_pairwise_records, int)
|
|
202
|
+
or self.max_pairwise_records <= 0
|
|
203
|
+
):
|
|
204
|
+
raise ConfigurationError(
|
|
205
|
+
"max_pairwise_records must be a positive integer.",
|
|
206
|
+
code="INVALID_SIMILARITY_SPLIT_CONFIG",
|
|
207
|
+
)
|
|
208
|
+
object.__setattr__(self, "ratios", FrozenDict(normalized_ratios))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _validate_probability(value: object, name: str) -> float:
|
|
212
|
+
if (
|
|
213
|
+
isinstance(value, bool)
|
|
214
|
+
or not isinstance(value, (int, float))
|
|
215
|
+
or not math.isfinite(value)
|
|
216
|
+
or not 0.0 <= value <= 1.0
|
|
217
|
+
):
|
|
218
|
+
raise ConfigurationError(
|
|
219
|
+
f"{name} must be finite and within [0, 1].",
|
|
220
|
+
code="INVALID_ADVANCED_DATASET_CONFIG",
|
|
221
|
+
)
|
|
222
|
+
return float(value)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _validate_bounded_positive_int(value: object, name: str, maximum: int) -> int:
|
|
226
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0 or value > maximum:
|
|
227
|
+
raise ConfigurationError(
|
|
228
|
+
f"{name} must be an integer in [1, {maximum}].",
|
|
229
|
+
code="INVALID_ADVANCED_DATASET_LIMIT",
|
|
230
|
+
)
|
|
231
|
+
return value
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True, slots=True)
|
|
235
|
+
class IUPACDeduplicationConfig:
|
|
236
|
+
"""Bound pairwise work for all-pairs IUPAC compatibility grouping."""
|
|
237
|
+
|
|
238
|
+
representative_policy: AdvancedRepresentativePolicy = "first"
|
|
239
|
+
max_records: int = 2_000
|
|
240
|
+
max_pairwise_comparisons: int = 1_000_000
|
|
241
|
+
|
|
242
|
+
def __post_init__(self) -> None:
|
|
243
|
+
if self.representative_policy not in {
|
|
244
|
+
"first",
|
|
245
|
+
"shortest",
|
|
246
|
+
"longest",
|
|
247
|
+
"best_quality",
|
|
248
|
+
"medoid",
|
|
249
|
+
}:
|
|
250
|
+
raise ConfigurationError("Unknown advanced representative policy.")
|
|
251
|
+
if self.representative_policy == "medoid":
|
|
252
|
+
raise ConfigurationError(
|
|
253
|
+
"IUPAC compatibility groups do not define a numeric medoid distance.",
|
|
254
|
+
code="IUPAC_MEDOID_UNDEFINED",
|
|
255
|
+
)
|
|
256
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 10_000)
|
|
257
|
+
_validate_bounded_positive_int(
|
|
258
|
+
self.max_pairwise_comparisons,
|
|
259
|
+
"max_pairwise_comparisons",
|
|
260
|
+
50_000_000,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass(frozen=True, slots=True)
|
|
265
|
+
class ClusterConfig:
|
|
266
|
+
"""Configuration for exhaustive bounded threshold-graph clustering."""
|
|
267
|
+
|
|
268
|
+
method: ClusterMethod = "identity"
|
|
269
|
+
threshold: float = 0.9
|
|
270
|
+
k: int = 3
|
|
271
|
+
canonical: bool = False
|
|
272
|
+
representative_policy: AdvancedRepresentativePolicy = "first"
|
|
273
|
+
seed: int = 0
|
|
274
|
+
max_records: int = 1_000
|
|
275
|
+
max_pairwise_comparisons: int = 500_000
|
|
276
|
+
max_alignment_cells: int = 5_000_000
|
|
277
|
+
|
|
278
|
+
def __post_init__(self) -> None:
|
|
279
|
+
if self.method not in {"identity", "edit", "kmer", "fingerprint"}:
|
|
280
|
+
raise ConfigurationError("Unknown clustering method.", code="INVALID_CLUSTER_METHOD")
|
|
281
|
+
object.__setattr__(self, "threshold", _validate_probability(self.threshold, "threshold"))
|
|
282
|
+
_validate_bounded_positive_int(self.k, "k", 12)
|
|
283
|
+
if not isinstance(self.canonical, bool):
|
|
284
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
285
|
+
if self.representative_policy not in {
|
|
286
|
+
"first",
|
|
287
|
+
"shortest",
|
|
288
|
+
"longest",
|
|
289
|
+
"best_quality",
|
|
290
|
+
"medoid",
|
|
291
|
+
}:
|
|
292
|
+
raise ConfigurationError("Unknown advanced representative policy.")
|
|
293
|
+
if isinstance(self.seed, bool) or not isinstance(self.seed, int):
|
|
294
|
+
raise ConfigurationError("seed must be an integer.")
|
|
295
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 5_000)
|
|
296
|
+
_validate_bounded_positive_int(
|
|
297
|
+
self.max_pairwise_comparisons,
|
|
298
|
+
"max_pairwise_comparisons",
|
|
299
|
+
10_000_000,
|
|
300
|
+
)
|
|
301
|
+
_validate_bounded_positive_int(
|
|
302
|
+
self.max_alignment_cells,
|
|
303
|
+
"max_alignment_cells",
|
|
304
|
+
100_000_000,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True, slots=True)
|
|
309
|
+
class HierarchicalClusteringConfig:
|
|
310
|
+
"""Configuration for exact agglomerative clustering over a bounded matrix."""
|
|
311
|
+
|
|
312
|
+
method: ClusterMethod = "identity"
|
|
313
|
+
linkage: LinkageMethod = "average"
|
|
314
|
+
k: int = 3
|
|
315
|
+
canonical: bool = False
|
|
316
|
+
max_records: int = 500
|
|
317
|
+
max_pairwise_comparisons: int = 124_750
|
|
318
|
+
max_alignment_cells: int = 5_000_000
|
|
319
|
+
|
|
320
|
+
def __post_init__(self) -> None:
|
|
321
|
+
if self.method not in {"identity", "edit", "kmer", "fingerprint"}:
|
|
322
|
+
raise ConfigurationError("Unknown clustering method.", code="INVALID_CLUSTER_METHOD")
|
|
323
|
+
if self.linkage not in {"single", "complete", "average"}:
|
|
324
|
+
raise ConfigurationError("Unknown linkage method.", code="INVALID_LINKAGE_METHOD")
|
|
325
|
+
_validate_bounded_positive_int(self.k, "k", 12)
|
|
326
|
+
if not isinstance(self.canonical, bool):
|
|
327
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
328
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 1_000)
|
|
329
|
+
_validate_bounded_positive_int(
|
|
330
|
+
self.max_pairwise_comparisons,
|
|
331
|
+
"max_pairwise_comparisons",
|
|
332
|
+
499_500,
|
|
333
|
+
)
|
|
334
|
+
_validate_bounded_positive_int(
|
|
335
|
+
self.max_alignment_cells,
|
|
336
|
+
"max_alignment_cells",
|
|
337
|
+
100_000_000,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass(frozen=True, slots=True)
|
|
342
|
+
class NeuralClusteringConfig:
|
|
343
|
+
"""Configure DNA model representations followed by seeded k-means."""
|
|
344
|
+
|
|
345
|
+
representation: RepresentationConfig = field(default_factory=RepresentationConfig)
|
|
346
|
+
n_clusters: int = 2
|
|
347
|
+
n_init: int = 10
|
|
348
|
+
max_iter: int = 300
|
|
349
|
+
tolerance: float = 1e-4
|
|
350
|
+
normalize: bool = True
|
|
351
|
+
pca_components: int | None = None
|
|
352
|
+
silhouette_sample_size: int = 5_000
|
|
353
|
+
seed: int = 0
|
|
354
|
+
max_records: int = 10_000
|
|
355
|
+
|
|
356
|
+
def __post_init__(self) -> None:
|
|
357
|
+
if not isinstance(self.representation, RepresentationConfig):
|
|
358
|
+
raise ConfigurationError(
|
|
359
|
+
"representation must be RepresentationConfig.",
|
|
360
|
+
code="INVALID_NEURAL_CLUSTER_CONFIG",
|
|
361
|
+
)
|
|
362
|
+
_validate_bounded_positive_int(self.n_clusters, "n_clusters", 100_000)
|
|
363
|
+
_validate_bounded_positive_int(self.n_init, "n_init", 1_000)
|
|
364
|
+
_validate_bounded_positive_int(self.max_iter, "max_iter", 100_000)
|
|
365
|
+
if (
|
|
366
|
+
isinstance(self.tolerance, bool)
|
|
367
|
+
or not isinstance(self.tolerance, (int, float))
|
|
368
|
+
or not math.isfinite(float(self.tolerance))
|
|
369
|
+
or not 0 < float(self.tolerance) <= 1.0
|
|
370
|
+
):
|
|
371
|
+
raise ConfigurationError(
|
|
372
|
+
"tolerance must be finite and in (0, 1].",
|
|
373
|
+
code="INVALID_NEURAL_CLUSTER_CONFIG",
|
|
374
|
+
)
|
|
375
|
+
object.__setattr__(self, "tolerance", float(self.tolerance))
|
|
376
|
+
if not isinstance(self.normalize, bool):
|
|
377
|
+
raise ConfigurationError(
|
|
378
|
+
"normalize must be boolean.",
|
|
379
|
+
code="INVALID_NEURAL_CLUSTER_CONFIG",
|
|
380
|
+
)
|
|
381
|
+
if self.pca_components is not None:
|
|
382
|
+
_validate_bounded_positive_int(self.pca_components, "pca_components", 100_000)
|
|
383
|
+
_validate_bounded_positive_int(
|
|
384
|
+
self.silhouette_sample_size,
|
|
385
|
+
"silhouette_sample_size",
|
|
386
|
+
1_000_000,
|
|
387
|
+
)
|
|
388
|
+
if isinstance(self.seed, bool) or not isinstance(self.seed, int):
|
|
389
|
+
raise ConfigurationError(
|
|
390
|
+
"seed must be an integer.",
|
|
391
|
+
code="INVALID_NEURAL_CLUSTER_CONFIG",
|
|
392
|
+
)
|
|
393
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 1_000_000)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@dataclass(frozen=True, slots=True)
|
|
397
|
+
class TemporalSplitConfig:
|
|
398
|
+
"""Chronological split configuration using ISO-8601 metadata strings."""
|
|
399
|
+
|
|
400
|
+
metadata_key: str = "date"
|
|
401
|
+
ratios: Mapping[str, float] = field(default_factory=lambda: {"train": 0.8, "test": 0.2})
|
|
402
|
+
cutoffs: tuple[str, ...] | None = None
|
|
403
|
+
preserve_order: bool = True
|
|
404
|
+
max_records: int = 1_000_000
|
|
405
|
+
|
|
406
|
+
def __post_init__(self) -> None:
|
|
407
|
+
if not isinstance(self.metadata_key, str) or not self.metadata_key.strip():
|
|
408
|
+
raise ConfigurationError("metadata_key must be a non-empty string.")
|
|
409
|
+
validated = SplitConfig(ratios=self.ratios, preserve_order=self.preserve_order)
|
|
410
|
+
object.__setattr__(self, "ratios", validated.ratios)
|
|
411
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 10_000_000)
|
|
412
|
+
if self.cutoffs is not None:
|
|
413
|
+
cutoffs = tuple(self.cutoffs)
|
|
414
|
+
if len(cutoffs) != len(self.ratios) - 1 or any(
|
|
415
|
+
not isinstance(value, str) or not value.strip() for value in cutoffs
|
|
416
|
+
):
|
|
417
|
+
raise ConfigurationError(
|
|
418
|
+
"cutoffs must contain one non-empty ISO timestamp per split boundary.",
|
|
419
|
+
code="INVALID_TEMPORAL_CUTOFFS",
|
|
420
|
+
)
|
|
421
|
+
object.__setattr__(self, "cutoffs", cutoffs)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@dataclass(frozen=True, slots=True)
|
|
425
|
+
class JointSplitConfig:
|
|
426
|
+
"""Greedy multi-constraint split with explicit infeasibility handling."""
|
|
427
|
+
|
|
428
|
+
ratios: Mapping[str, float] = field(default_factory=_default_ratios)
|
|
429
|
+
group_keys: tuple[str, ...] = ()
|
|
430
|
+
label_key: str | None = None
|
|
431
|
+
similarity_threshold: float | None = None
|
|
432
|
+
similarity_k: int = 3
|
|
433
|
+
similarity_canonical: bool = True
|
|
434
|
+
seed: int = 0
|
|
435
|
+
ratio_tolerance: float = 0.05
|
|
436
|
+
infeasible_policy: InfeasiblePolicy = "error"
|
|
437
|
+
max_records: int = 2_000
|
|
438
|
+
max_pairwise_comparisons: int = 1_000_000
|
|
439
|
+
|
|
440
|
+
def __post_init__(self) -> None:
|
|
441
|
+
validated = SplitConfig(ratios=self.ratios, seed=self.seed)
|
|
442
|
+
object.__setattr__(self, "ratios", validated.ratios)
|
|
443
|
+
keys = tuple(self.group_keys)
|
|
444
|
+
if any(not isinstance(key, str) or not key.strip() for key in keys):
|
|
445
|
+
raise ConfigurationError("group_keys must contain non-empty strings.")
|
|
446
|
+
if len(keys) != len(set(keys)):
|
|
447
|
+
raise ConfigurationError("group_keys must be unique.")
|
|
448
|
+
object.__setattr__(self, "group_keys", keys)
|
|
449
|
+
if self.label_key is not None and (
|
|
450
|
+
not isinstance(self.label_key, str) or not self.label_key.strip()
|
|
451
|
+
):
|
|
452
|
+
raise ConfigurationError("label_key must be non-empty or None.")
|
|
453
|
+
if self.similarity_threshold is not None:
|
|
454
|
+
object.__setattr__(
|
|
455
|
+
self,
|
|
456
|
+
"similarity_threshold",
|
|
457
|
+
_validate_probability(self.similarity_threshold, "similarity_threshold"),
|
|
458
|
+
)
|
|
459
|
+
if not keys and self.similarity_threshold is None:
|
|
460
|
+
raise ConfigurationError(
|
|
461
|
+
"At least one group key or similarity constraint is required.",
|
|
462
|
+
code="JOINT_SPLIT_CONSTRAINT_REQUIRED",
|
|
463
|
+
)
|
|
464
|
+
_validate_bounded_positive_int(self.similarity_k, "similarity_k", 12)
|
|
465
|
+
if not isinstance(self.similarity_canonical, bool):
|
|
466
|
+
raise ConfigurationError("similarity_canonical must be boolean.")
|
|
467
|
+
object.__setattr__(
|
|
468
|
+
self, "ratio_tolerance", _validate_probability(self.ratio_tolerance, "ratio_tolerance")
|
|
469
|
+
)
|
|
470
|
+
if self.infeasible_policy not in {"error", "relax"}:
|
|
471
|
+
raise ConfigurationError("infeasible_policy must be 'error' or 'relax'.")
|
|
472
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 5_000)
|
|
473
|
+
_validate_bounded_positive_int(
|
|
474
|
+
self.max_pairwise_comparisons,
|
|
475
|
+
"max_pairwise_comparisons",
|
|
476
|
+
10_000_000,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@dataclass(frozen=True, slots=True)
|
|
481
|
+
class LeakageConfig:
|
|
482
|
+
"""Cross-split exhaustive comparison controls."""
|
|
483
|
+
|
|
484
|
+
method: ClusterMethod = "identity"
|
|
485
|
+
threshold: float = 0.9
|
|
486
|
+
k: int = 3
|
|
487
|
+
canonical: bool = False
|
|
488
|
+
max_records: int = 5_000
|
|
489
|
+
max_cross_pairs: int = 1_000_000
|
|
490
|
+
max_events: int = 100_000
|
|
491
|
+
max_alignment_cells: int = 5_000_000
|
|
492
|
+
|
|
493
|
+
def __post_init__(self) -> None:
|
|
494
|
+
if self.method not in {"identity", "edit", "kmer", "fingerprint"}:
|
|
495
|
+
raise ConfigurationError("Unknown leakage method.")
|
|
496
|
+
object.__setattr__(self, "threshold", _validate_probability(self.threshold, "threshold"))
|
|
497
|
+
_validate_bounded_positive_int(self.k, "k", 12)
|
|
498
|
+
if not isinstance(self.canonical, bool):
|
|
499
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
500
|
+
_validate_bounded_positive_int(self.max_records, "max_records", 20_000)
|
|
501
|
+
_validate_bounded_positive_int(self.max_cross_pairs, "max_cross_pairs", 50_000_000)
|
|
502
|
+
_validate_bounded_positive_int(self.max_events, "max_events", 5_000_000)
|
|
503
|
+
_validate_bounded_positive_int(
|
|
504
|
+
self.max_alignment_cells,
|
|
505
|
+
"max_alignment_cells",
|
|
506
|
+
100_000_000,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
__all__ = [
|
|
511
|
+
"AdvancedRepresentativePolicy",
|
|
512
|
+
"ClusterConfig",
|
|
513
|
+
"ClusterMethod",
|
|
514
|
+
"ConflictPolicy",
|
|
515
|
+
"DedupEquivalence",
|
|
516
|
+
"DeduplicationConfig",
|
|
517
|
+
"HierarchicalClusteringConfig",
|
|
518
|
+
"IUPACDeduplicationConfig",
|
|
519
|
+
"InfeasiblePolicy",
|
|
520
|
+
"JointSplitConfig",
|
|
521
|
+
"LeakageConfig",
|
|
522
|
+
"LinkageMethod",
|
|
523
|
+
"MissingMetadataPolicy",
|
|
524
|
+
"NeuralClusteringConfig",
|
|
525
|
+
"RepresentativePolicy",
|
|
526
|
+
"SimilarityAmbiguityPolicy",
|
|
527
|
+
"SimilarityGapPolicy",
|
|
528
|
+
"SplitConfig",
|
|
529
|
+
"SplitMethod",
|
|
530
|
+
"TemporalSplitConfig",
|
|
531
|
+
]
|