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,591 @@
|
|
|
1
|
+
"""Validated configuration for transparent DNA evaluation methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Literal, TypeAlias
|
|
10
|
+
|
|
11
|
+
from dnakit.core._json import FrozenDict
|
|
12
|
+
from dnakit.exceptions import ConfigurationError
|
|
13
|
+
from dnakit.representations import RepresentationConfig
|
|
14
|
+
|
|
15
|
+
GapDenominatorPolicy: TypeAlias = Literal["exclude", "include_known", "error"]
|
|
16
|
+
PairSimilarityMethod: TypeAlias = Literal["exact", "identity", "edit", "kmer"]
|
|
17
|
+
UniquenessEquivalence: TypeAlias = Literal[
|
|
18
|
+
"exact",
|
|
19
|
+
"reverse_complement",
|
|
20
|
+
"circular",
|
|
21
|
+
"circular_reverse_complement",
|
|
22
|
+
"iupac",
|
|
23
|
+
"approximate",
|
|
24
|
+
]
|
|
25
|
+
ScoreDirection: TypeAlias = Literal["higher_is_better", "lower_is_better"]
|
|
26
|
+
MissingScorePolicy: TypeAlias = Literal["error", "omit", "zero"]
|
|
27
|
+
KmerAmbiguityPolicy: TypeAlias = Literal["error", "ignore"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _finite(value: object, name: str) -> float:
|
|
31
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
32
|
+
raise ConfigurationError(f"{name} must be a finite number.")
|
|
33
|
+
return float(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _probability(value: object, name: str) -> float:
|
|
37
|
+
resolved = _finite(value, name)
|
|
38
|
+
if not 0.0 <= resolved <= 1.0:
|
|
39
|
+
raise ConfigurationError(f"{name} must be within [0, 1].")
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _positive_int(value: object, name: str, maximum: int) -> int:
|
|
44
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 < value <= maximum:
|
|
45
|
+
raise ConfigurationError(f"{name} must be an integer within [1, {maximum}].")
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _non_negative_int(value: object, name: str, maximum: int) -> int:
|
|
50
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
|
|
51
|
+
raise ConfigurationError(f"{name} must be an integer within [0, {maximum}].")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _freeze_weights(
|
|
56
|
+
values: Mapping[str, float],
|
|
57
|
+
*,
|
|
58
|
+
name: str,
|
|
59
|
+
allowed: frozenset[str] | None = None,
|
|
60
|
+
) -> FrozenDict:
|
|
61
|
+
if not isinstance(values, Mapping) or not values:
|
|
62
|
+
raise ConfigurationError(f"{name} must be a non-empty mapping.")
|
|
63
|
+
resolved: dict[str, float] = {}
|
|
64
|
+
for key, value in values.items():
|
|
65
|
+
if not isinstance(key, str) or not key.strip():
|
|
66
|
+
raise ConfigurationError(f"{name} keys must be non-empty strings.")
|
|
67
|
+
if allowed is not None and key not in allowed:
|
|
68
|
+
raise ConfigurationError(f"{name} contains an unknown component: {key!r}.")
|
|
69
|
+
weight = _finite(value, f"{name}[{key!r}]")
|
|
70
|
+
if weight < 0:
|
|
71
|
+
raise ConfigurationError(f"{name} values must be non-negative.")
|
|
72
|
+
resolved[key] = weight
|
|
73
|
+
if math.fsum(resolved.values()) <= 0:
|
|
74
|
+
raise ConfigurationError(f"{name} must contain at least one positive weight.")
|
|
75
|
+
return FrozenDict(resolved)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class EvaluationLimits:
|
|
80
|
+
"""Shared caps for materialization and exhaustive calculations."""
|
|
81
|
+
|
|
82
|
+
max_records: int = 1_000
|
|
83
|
+
max_total_symbols: int = 10_000_000
|
|
84
|
+
max_pairwise_comparisons: int = 500_000
|
|
85
|
+
max_alignment_cells: int = 5_000_000
|
|
86
|
+
|
|
87
|
+
def __post_init__(self) -> None:
|
|
88
|
+
_positive_int(self.max_records, "max_records", 1_000_000)
|
|
89
|
+
_positive_int(self.max_total_symbols, "max_total_symbols", 1_000_000_000)
|
|
90
|
+
_positive_int(self.max_pairwise_comparisons, "max_pairwise_comparisons", 50_000_000)
|
|
91
|
+
_positive_int(self.max_alignment_cells, "max_alignment_cells", 100_000_000)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _ambiguity_weights() -> dict[str, float]:
|
|
95
|
+
return {
|
|
96
|
+
"R": 0.5,
|
|
97
|
+
"Y": 0.5,
|
|
98
|
+
"S": 0.5,
|
|
99
|
+
"W": 0.5,
|
|
100
|
+
"K": 0.5,
|
|
101
|
+
"M": 0.5,
|
|
102
|
+
"B": 0.75,
|
|
103
|
+
"D": 0.75,
|
|
104
|
+
"H": 0.75,
|
|
105
|
+
"V": 0.75,
|
|
106
|
+
"N": 1.0,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class AmbiguityEvaluationConfig:
|
|
112
|
+
max_fraction: float = 0.05
|
|
113
|
+
symbol_weights: Mapping[str, float] = field(default_factory=_ambiguity_weights)
|
|
114
|
+
gap_denominator_policy: GapDenominatorPolicy = "exclude"
|
|
115
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
object.__setattr__(self, "max_fraction", _probability(self.max_fraction, "max_fraction"))
|
|
119
|
+
if self.gap_denominator_policy not in {"exclude", "include_known", "error"}:
|
|
120
|
+
raise ConfigurationError("Unknown gap_denominator_policy.")
|
|
121
|
+
allowed = frozenset("RYSWKMBDHVN")
|
|
122
|
+
weights = _freeze_weights(self.symbol_weights, name="symbol_weights", allowed=allowed)
|
|
123
|
+
if set(weights) != allowed:
|
|
124
|
+
raise ConfigurationError("symbol_weights must define every ambiguous IUPAC symbol.")
|
|
125
|
+
if any(
|
|
126
|
+
isinstance(value, (int, float)) and not isinstance(value, bool) and value > 1.0
|
|
127
|
+
for value in weights.values()
|
|
128
|
+
):
|
|
129
|
+
raise ConfigurationError("Ambiguity symbol weights must be within [0, 1].")
|
|
130
|
+
object.__setattr__(self, "symbol_weights", weights)
|
|
131
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
132
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _complexity_weights() -> dict[str, float]:
|
|
136
|
+
return {
|
|
137
|
+
"entropy": 1.0,
|
|
138
|
+
"linguistic": 1.0,
|
|
139
|
+
"repeat_cleanliness": 1.0,
|
|
140
|
+
"homopolymer_cleanliness": 1.0,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True)
|
|
145
|
+
class ComplexityEvaluationConfig:
|
|
146
|
+
max_word_size: int = 6
|
|
147
|
+
max_repeat_unit: int = 20
|
|
148
|
+
min_repeat_count: int = 2
|
|
149
|
+
acceptable_homopolymer_length: int = 6
|
|
150
|
+
weights: Mapping[str, float] = field(default_factory=_complexity_weights)
|
|
151
|
+
max_observations_per_sequence: int = 10_000_000
|
|
152
|
+
max_comparisons_per_sequence: int = 5_000_000
|
|
153
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
_positive_int(self.max_word_size, "max_word_size", 16)
|
|
157
|
+
_positive_int(self.max_repeat_unit, "max_repeat_unit", 100)
|
|
158
|
+
if self.min_repeat_count < 2:
|
|
159
|
+
raise ConfigurationError("min_repeat_count must be at least 2.")
|
|
160
|
+
_positive_int(self.min_repeat_count, "min_repeat_count", 1_000_000)
|
|
161
|
+
_positive_int(
|
|
162
|
+
self.acceptable_homopolymer_length, "acceptable_homopolymer_length", 1_000_000
|
|
163
|
+
)
|
|
164
|
+
_positive_int(
|
|
165
|
+
self.max_observations_per_sequence, "max_observations_per_sequence", 100_000_000
|
|
166
|
+
)
|
|
167
|
+
_positive_int(
|
|
168
|
+
self.max_comparisons_per_sequence, "max_comparisons_per_sequence", 100_000_000
|
|
169
|
+
)
|
|
170
|
+
object.__setattr__(
|
|
171
|
+
self,
|
|
172
|
+
"weights",
|
|
173
|
+
_freeze_weights(
|
|
174
|
+
self.weights,
|
|
175
|
+
name="weights",
|
|
176
|
+
allowed=frozenset(_complexity_weights()),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
180
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _quality_weights() -> dict[str, float]:
|
|
184
|
+
return {
|
|
185
|
+
"validity": 2.0,
|
|
186
|
+
"ambiguity": 1.0,
|
|
187
|
+
"complexity": 1.0,
|
|
188
|
+
"length": 1.0,
|
|
189
|
+
"completeness": 1.0,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True, slots=True)
|
|
194
|
+
class QualityEvaluationConfig:
|
|
195
|
+
min_length: int = 1
|
|
196
|
+
max_length: int | None = None
|
|
197
|
+
ambiguity: AmbiguityEvaluationConfig = field(default_factory=AmbiguityEvaluationConfig)
|
|
198
|
+
complexity: ComplexityEvaluationConfig = field(default_factory=ComplexityEvaluationConfig)
|
|
199
|
+
weights: Mapping[str, float] = field(default_factory=_quality_weights)
|
|
200
|
+
warning_score: float = 0.75
|
|
201
|
+
pass_score: float = 0.9
|
|
202
|
+
|
|
203
|
+
def __post_init__(self) -> None:
|
|
204
|
+
_non_negative_int(self.min_length, "min_length", 1_000_000_000)
|
|
205
|
+
if self.max_length is not None:
|
|
206
|
+
_positive_int(self.max_length, "max_length", 1_000_000_000)
|
|
207
|
+
if self.max_length < self.min_length:
|
|
208
|
+
raise ConfigurationError("max_length cannot be smaller than min_length.")
|
|
209
|
+
if not isinstance(self.ambiguity, AmbiguityEvaluationConfig):
|
|
210
|
+
raise ConfigurationError("ambiguity must be AmbiguityEvaluationConfig.")
|
|
211
|
+
if not isinstance(self.complexity, ComplexityEvaluationConfig):
|
|
212
|
+
raise ConfigurationError("complexity must be ComplexityEvaluationConfig.")
|
|
213
|
+
if self.ambiguity.limits != self.complexity.limits:
|
|
214
|
+
raise ConfigurationError(
|
|
215
|
+
"Quality ambiguity and complexity configs must use identical limits.",
|
|
216
|
+
code="QUALITY_LIMIT_MISMATCH",
|
|
217
|
+
)
|
|
218
|
+
object.__setattr__(
|
|
219
|
+
self,
|
|
220
|
+
"weights",
|
|
221
|
+
_freeze_weights(self.weights, name="weights", allowed=frozenset(_quality_weights())),
|
|
222
|
+
)
|
|
223
|
+
warning = _probability(self.warning_score, "warning_score")
|
|
224
|
+
passing = _probability(self.pass_score, "pass_score")
|
|
225
|
+
if warning > passing:
|
|
226
|
+
raise ConfigurationError("warning_score cannot exceed pass_score.")
|
|
227
|
+
object.__setattr__(self, "warning_score", warning)
|
|
228
|
+
object.__setattr__(self, "pass_score", passing)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True, slots=True)
|
|
232
|
+
class UniquenessEvaluationConfig:
|
|
233
|
+
equivalence: UniquenessEquivalence = "exact"
|
|
234
|
+
approximate_method: PairSimilarityMethod = "kmer"
|
|
235
|
+
threshold: float = 0.9
|
|
236
|
+
k: int = 5
|
|
237
|
+
canonical: bool = True
|
|
238
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
239
|
+
|
|
240
|
+
def __post_init__(self) -> None:
|
|
241
|
+
if self.equivalence not in {
|
|
242
|
+
"exact",
|
|
243
|
+
"reverse_complement",
|
|
244
|
+
"circular",
|
|
245
|
+
"circular_reverse_complement",
|
|
246
|
+
"iupac",
|
|
247
|
+
"approximate",
|
|
248
|
+
}:
|
|
249
|
+
raise ConfigurationError("Unknown uniqueness equivalence.")
|
|
250
|
+
if self.approximate_method not in {"exact", "identity", "edit", "kmer"}:
|
|
251
|
+
raise ConfigurationError("Unknown approximate_method.")
|
|
252
|
+
object.__setattr__(self, "threshold", _probability(self.threshold, "threshold"))
|
|
253
|
+
_positive_int(self.k, "k", 32)
|
|
254
|
+
if not isinstance(self.canonical, bool):
|
|
255
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
256
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
257
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True, slots=True)
|
|
261
|
+
class DiversityEvaluationConfig:
|
|
262
|
+
method: PairSimilarityMethod = "kmer"
|
|
263
|
+
k: int = 5
|
|
264
|
+
canonical: bool = True
|
|
265
|
+
cluster_threshold: float = 0.9
|
|
266
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
267
|
+
|
|
268
|
+
def __post_init__(self) -> None:
|
|
269
|
+
if self.method not in {"exact", "identity", "edit", "kmer"}:
|
|
270
|
+
raise ConfigurationError("Unknown diversity similarity method.")
|
|
271
|
+
_positive_int(self.k, "k", 32)
|
|
272
|
+
if not isinstance(self.canonical, bool):
|
|
273
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
274
|
+
object.__setattr__(
|
|
275
|
+
self, "cluster_threshold", _probability(self.cluster_threshold, "cluster_threshold")
|
|
276
|
+
)
|
|
277
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
278
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True, slots=True)
|
|
282
|
+
class ReferenceSearchConfig:
|
|
283
|
+
method: PairSimilarityMethod = "kmer"
|
|
284
|
+
k: int = 7
|
|
285
|
+
canonical: bool = True
|
|
286
|
+
top_k: int = 1
|
|
287
|
+
min_similarity: float = 0.0
|
|
288
|
+
min_coverage: float = 0.0
|
|
289
|
+
copy_threshold: float = 0.9
|
|
290
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
291
|
+
|
|
292
|
+
def __post_init__(self) -> None:
|
|
293
|
+
if self.method not in {"exact", "identity", "edit", "kmer"}:
|
|
294
|
+
raise ConfigurationError("Unknown reference similarity method.")
|
|
295
|
+
_positive_int(self.k, "k", 32)
|
|
296
|
+
_positive_int(self.top_k, "top_k", 100_000)
|
|
297
|
+
if not isinstance(self.canonical, bool):
|
|
298
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
299
|
+
object.__setattr__(
|
|
300
|
+
self, "min_similarity", _probability(self.min_similarity, "min_similarity")
|
|
301
|
+
)
|
|
302
|
+
object.__setattr__(self, "min_coverage", _probability(self.min_coverage, "min_coverage"))
|
|
303
|
+
object.__setattr__(
|
|
304
|
+
self, "copy_threshold", _probability(self.copy_threshold, "copy_threshold")
|
|
305
|
+
)
|
|
306
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
307
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass(frozen=True, slots=True)
|
|
311
|
+
class DistributionEvaluationConfig:
|
|
312
|
+
features: tuple[str, ...] = ("length", "gc", "kmer", "motif", "repeat")
|
|
313
|
+
k: int = 3
|
|
314
|
+
canonical: bool = True
|
|
315
|
+
motifs: tuple[str, ...] = ("CG",)
|
|
316
|
+
max_kmer_observations: int = 10_000_000
|
|
317
|
+
max_repeat_comparisons_per_sequence: int = 1_000_000
|
|
318
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
319
|
+
|
|
320
|
+
def __post_init__(self) -> None:
|
|
321
|
+
allowed = {"length", "gc", "kmer", "motif", "repeat"}
|
|
322
|
+
features = tuple(self.features)
|
|
323
|
+
if not features or len(set(features)) != len(features) or set(features) - allowed:
|
|
324
|
+
raise ConfigurationError("features must be unique supported distribution features.")
|
|
325
|
+
object.__setattr__(self, "features", features)
|
|
326
|
+
_positive_int(self.k, "k", 12)
|
|
327
|
+
if not isinstance(self.canonical, bool):
|
|
328
|
+
raise ConfigurationError("canonical must be boolean.")
|
|
329
|
+
motifs = tuple(self.motifs)
|
|
330
|
+
if len(set(motifs)) != len(motifs) or any(
|
|
331
|
+
not isinstance(motif, str) or not motif or set(motif) - set("ACGT") for motif in motifs
|
|
332
|
+
):
|
|
333
|
+
raise ConfigurationError("motifs must contain unique, non-empty A/C/G/T strings.")
|
|
334
|
+
if "motif" in features and not motifs:
|
|
335
|
+
raise ConfigurationError("At least one motif is required for motif distribution.")
|
|
336
|
+
object.__setattr__(self, "motifs", motifs)
|
|
337
|
+
_positive_int(self.max_kmer_observations, "max_kmer_observations", 100_000_000)
|
|
338
|
+
_positive_int(
|
|
339
|
+
self.max_repeat_comparisons_per_sequence,
|
|
340
|
+
"max_repeat_comparisons_per_sequence",
|
|
341
|
+
100_000_000,
|
|
342
|
+
)
|
|
343
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
344
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass(frozen=True, slots=True)
|
|
348
|
+
class FrechetDistanceConfig:
|
|
349
|
+
"""Configure a Fréchet distance over DNA foundation-model representations."""
|
|
350
|
+
|
|
351
|
+
representation: RepresentationConfig = field(default_factory=RepresentationConfig)
|
|
352
|
+
normalize: bool = True
|
|
353
|
+
max_cross_gram_elements: int = 1_000_000
|
|
354
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
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_FRECHET_DISTANCE_CONFIG",
|
|
361
|
+
)
|
|
362
|
+
if not isinstance(self.normalize, bool):
|
|
363
|
+
raise ConfigurationError(
|
|
364
|
+
"normalize must be boolean.",
|
|
365
|
+
code="INVALID_FRECHET_DISTANCE_CONFIG",
|
|
366
|
+
)
|
|
367
|
+
_positive_int(
|
|
368
|
+
self.max_cross_gram_elements,
|
|
369
|
+
"max_cross_gram_elements",
|
|
370
|
+
100_000_000,
|
|
371
|
+
)
|
|
372
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
373
|
+
raise ConfigurationError(
|
|
374
|
+
"limits must be EvaluationLimits.",
|
|
375
|
+
code="INVALID_FRECHET_DISTANCE_CONFIG",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True, slots=True)
|
|
380
|
+
class FragmentSimilarityConfig:
|
|
381
|
+
"""Configure the DNA adaptation of the MOSES Frag metric."""
|
|
382
|
+
|
|
383
|
+
k: int = 3
|
|
384
|
+
canonical: bool = True
|
|
385
|
+
ambiguity_policy: KmerAmbiguityPolicy = "ignore"
|
|
386
|
+
max_kmer_observations: int = 10_000_000
|
|
387
|
+
show_progress: bool = True
|
|
388
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
389
|
+
|
|
390
|
+
def __post_init__(self) -> None:
|
|
391
|
+
_positive_int(self.k, "k", 32)
|
|
392
|
+
if not isinstance(self.canonical, bool):
|
|
393
|
+
raise ConfigurationError(
|
|
394
|
+
"canonical must be boolean.",
|
|
395
|
+
code="INVALID_FRAGMENT_SIMILARITY_CONFIG",
|
|
396
|
+
)
|
|
397
|
+
if self.ambiguity_policy not in {"error", "ignore"}:
|
|
398
|
+
raise ConfigurationError(
|
|
399
|
+
"ambiguity_policy must be 'error' or 'ignore'.",
|
|
400
|
+
code="INVALID_FRAGMENT_SIMILARITY_CONFIG",
|
|
401
|
+
)
|
|
402
|
+
_positive_int(
|
|
403
|
+
self.max_kmer_observations,
|
|
404
|
+
"max_kmer_observations",
|
|
405
|
+
100_000_000,
|
|
406
|
+
)
|
|
407
|
+
if not isinstance(self.show_progress, bool):
|
|
408
|
+
raise ConfigurationError(
|
|
409
|
+
"show_progress must be boolean.",
|
|
410
|
+
code="INVALID_FRAGMENT_SIMILARITY_CONFIG",
|
|
411
|
+
)
|
|
412
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
413
|
+
raise ConfigurationError(
|
|
414
|
+
"limits must be EvaluationLimits.",
|
|
415
|
+
code="INVALID_FRAGMENT_SIMILARITY_CONFIG",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@dataclass(frozen=True, slots=True)
|
|
420
|
+
class SNNConfig:
|
|
421
|
+
"""Configure similarity to nearest neighbor over DNA bit fingerprints."""
|
|
422
|
+
|
|
423
|
+
k: int = 7
|
|
424
|
+
n_bits: int = 1_024
|
|
425
|
+
canonical: bool = True
|
|
426
|
+
seed: int = 0
|
|
427
|
+
ambiguity_policy: KmerAmbiguityPolicy = "ignore"
|
|
428
|
+
max_fingerprint_elements: int = 10_000_000
|
|
429
|
+
show_progress: bool = True
|
|
430
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
431
|
+
|
|
432
|
+
def __post_init__(self) -> None:
|
|
433
|
+
_positive_int(self.k, "k", 32)
|
|
434
|
+
_positive_int(self.n_bits, "n_bits", 1_000_000)
|
|
435
|
+
if not isinstance(self.canonical, bool):
|
|
436
|
+
raise ConfigurationError(
|
|
437
|
+
"canonical must be boolean.",
|
|
438
|
+
code="INVALID_SNN_CONFIG",
|
|
439
|
+
)
|
|
440
|
+
if (
|
|
441
|
+
isinstance(self.seed, bool)
|
|
442
|
+
or not isinstance(self.seed, int)
|
|
443
|
+
or not 0 <= self.seed < 2**64
|
|
444
|
+
):
|
|
445
|
+
raise ConfigurationError(
|
|
446
|
+
"seed must be an integer in [0, 2**64).",
|
|
447
|
+
code="INVALID_SNN_CONFIG",
|
|
448
|
+
)
|
|
449
|
+
if self.ambiguity_policy not in {"error", "ignore"}:
|
|
450
|
+
raise ConfigurationError(
|
|
451
|
+
"ambiguity_policy must be 'error' or 'ignore'.",
|
|
452
|
+
code="INVALID_SNN_CONFIG",
|
|
453
|
+
)
|
|
454
|
+
_positive_int(
|
|
455
|
+
self.max_fingerprint_elements,
|
|
456
|
+
"max_fingerprint_elements",
|
|
457
|
+
100_000_000,
|
|
458
|
+
)
|
|
459
|
+
if not isinstance(self.show_progress, bool):
|
|
460
|
+
raise ConfigurationError(
|
|
461
|
+
"show_progress must be boolean.",
|
|
462
|
+
code="INVALID_SNN_CONFIG",
|
|
463
|
+
)
|
|
464
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
465
|
+
raise ConfigurationError(
|
|
466
|
+
"limits must be EvaluationLimits.",
|
|
467
|
+
code="INVALID_SNN_CONFIG",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@dataclass(frozen=True, slots=True)
|
|
472
|
+
class SynthesisRiskConfig:
|
|
473
|
+
global_gc_min: float = 0.30
|
|
474
|
+
global_gc_max: float = 0.70
|
|
475
|
+
local_gc_min: float = 0.20
|
|
476
|
+
local_gc_max: float = 0.80
|
|
477
|
+
window_size: int = 50
|
|
478
|
+
window_step: int = 10
|
|
479
|
+
homopolymer_threshold: int = 8
|
|
480
|
+
tandem_min_unit: int = 2
|
|
481
|
+
tandem_max_unit: int = 20
|
|
482
|
+
tandem_min_repeats: int = 4
|
|
483
|
+
inverted_min_arm: int = 10
|
|
484
|
+
inverted_max_arm: int = 30
|
|
485
|
+
inverted_max_loop: int = 80
|
|
486
|
+
max_windows_per_sequence: int = 1_000_000
|
|
487
|
+
max_pattern_comparisons_per_sequence: int = 2_000_000
|
|
488
|
+
max_matches_per_sequence: int = 100_000
|
|
489
|
+
limits: EvaluationLimits = field(default_factory=EvaluationLimits)
|
|
490
|
+
|
|
491
|
+
def __post_init__(self) -> None:
|
|
492
|
+
for name in ("global_gc_min", "global_gc_max", "local_gc_min", "local_gc_max"):
|
|
493
|
+
object.__setattr__(self, name, _probability(getattr(self, name), name))
|
|
494
|
+
if self.global_gc_min > self.global_gc_max or self.local_gc_min > self.local_gc_max:
|
|
495
|
+
raise ConfigurationError("Minimum GC thresholds cannot exceed maximum thresholds.")
|
|
496
|
+
for name, maximum in (
|
|
497
|
+
("window_size", 1_000_000),
|
|
498
|
+
("window_step", 1_000_000),
|
|
499
|
+
("homopolymer_threshold", 1_000_000),
|
|
500
|
+
("tandem_min_unit", 100),
|
|
501
|
+
("tandem_max_unit", 100),
|
|
502
|
+
("tandem_min_repeats", 1_000_000),
|
|
503
|
+
("inverted_min_arm", 1_000),
|
|
504
|
+
("inverted_max_arm", 1_000),
|
|
505
|
+
("max_windows_per_sequence", 10_000_000),
|
|
506
|
+
("max_pattern_comparisons_per_sequence", 100_000_000),
|
|
507
|
+
("max_matches_per_sequence", 1_000_000),
|
|
508
|
+
):
|
|
509
|
+
_positive_int(getattr(self, name), name, maximum)
|
|
510
|
+
_non_negative_int(self.inverted_max_loop, "inverted_max_loop", 1_000_000)
|
|
511
|
+
if self.tandem_min_unit > self.tandem_max_unit:
|
|
512
|
+
raise ConfigurationError("tandem_min_unit cannot exceed tandem_max_unit.")
|
|
513
|
+
if self.tandem_min_repeats < 2:
|
|
514
|
+
raise ConfigurationError("tandem_min_repeats must be at least 2.")
|
|
515
|
+
if self.inverted_min_arm > self.inverted_max_arm:
|
|
516
|
+
raise ConfigurationError("inverted_min_arm cannot exceed inverted_max_arm.")
|
|
517
|
+
if not isinstance(self.limits, EvaluationLimits):
|
|
518
|
+
raise ConfigurationError("limits must be EvaluationLimits.")
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@dataclass(frozen=True, slots=True)
|
|
522
|
+
class ScoreRule:
|
|
523
|
+
"""One explicit min-max normalization rule for a scorecard component."""
|
|
524
|
+
|
|
525
|
+
direction: ScoreDirection = "higher_is_better"
|
|
526
|
+
weight: float = 1.0
|
|
527
|
+
minimum: float = 0.0
|
|
528
|
+
maximum: float = 1.0
|
|
529
|
+
metric: str | None = None
|
|
530
|
+
|
|
531
|
+
def __post_init__(self) -> None:
|
|
532
|
+
if self.direction not in {"higher_is_better", "lower_is_better"}:
|
|
533
|
+
raise ConfigurationError("Unknown score direction.")
|
|
534
|
+
weight = _finite(self.weight, "weight")
|
|
535
|
+
minimum = _finite(self.minimum, "minimum")
|
|
536
|
+
maximum = _finite(self.maximum, "maximum")
|
|
537
|
+
if weight <= 0 or minimum >= maximum:
|
|
538
|
+
raise ConfigurationError("ScoreRule requires weight > 0 and minimum < maximum.")
|
|
539
|
+
if self.metric is not None and (
|
|
540
|
+
not isinstance(self.metric, str) or not self.metric.strip()
|
|
541
|
+
):
|
|
542
|
+
raise ConfigurationError("ScoreRule metric must be non-empty or None.")
|
|
543
|
+
object.__setattr__(self, "weight", weight)
|
|
544
|
+
object.__setattr__(self, "minimum", minimum)
|
|
545
|
+
object.__setattr__(self, "maximum", maximum)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@dataclass(frozen=True, slots=True)
|
|
549
|
+
class ScorecardConfig:
|
|
550
|
+
rules: Mapping[str, ScoreRule]
|
|
551
|
+
missing_policy: MissingScorePolicy = "error"
|
|
552
|
+
warning_score: float = 0.5
|
|
553
|
+
pass_score: float = 0.8
|
|
554
|
+
|
|
555
|
+
def __post_init__(self) -> None:
|
|
556
|
+
if not isinstance(self.rules, Mapping) or not self.rules:
|
|
557
|
+
raise ConfigurationError("rules must be a non-empty mapping.")
|
|
558
|
+
rules: dict[str, ScoreRule] = {}
|
|
559
|
+
for name, rule in self.rules.items():
|
|
560
|
+
if not isinstance(name, str) or not name.strip() or not isinstance(rule, ScoreRule):
|
|
561
|
+
raise ConfigurationError(
|
|
562
|
+
"Scorecard rules require non-empty names and ScoreRule values."
|
|
563
|
+
)
|
|
564
|
+
rules[name] = rule
|
|
565
|
+
object.__setattr__(self, "rules", MappingProxyType(rules))
|
|
566
|
+
if self.missing_policy not in {"error", "omit", "zero"}:
|
|
567
|
+
raise ConfigurationError("Unknown missing_policy.")
|
|
568
|
+
warning = _probability(self.warning_score, "warning_score")
|
|
569
|
+
passing = _probability(self.pass_score, "pass_score")
|
|
570
|
+
if warning > passing:
|
|
571
|
+
raise ConfigurationError("warning_score cannot exceed pass_score.")
|
|
572
|
+
object.__setattr__(self, "warning_score", warning)
|
|
573
|
+
object.__setattr__(self, "pass_score", passing)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
__all__ = [
|
|
577
|
+
"AmbiguityEvaluationConfig",
|
|
578
|
+
"ComplexityEvaluationConfig",
|
|
579
|
+
"DistributionEvaluationConfig",
|
|
580
|
+
"DiversityEvaluationConfig",
|
|
581
|
+
"EvaluationLimits",
|
|
582
|
+
"FragmentSimilarityConfig",
|
|
583
|
+
"FrechetDistanceConfig",
|
|
584
|
+
"QualityEvaluationConfig",
|
|
585
|
+
"ReferenceSearchConfig",
|
|
586
|
+
"SNNConfig",
|
|
587
|
+
"ScoreRule",
|
|
588
|
+
"ScorecardConfig",
|
|
589
|
+
"SynthesisRiskConfig",
|
|
590
|
+
"UniquenessEvaluationConfig",
|
|
591
|
+
]
|