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