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,487 @@
|
|
|
1
|
+
"""Sequence-, record- and collection-level DNA validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import overload
|
|
9
|
+
|
|
10
|
+
from dnakit.core.collection import DNASet
|
|
11
|
+
from dnakit.core.enums import DNAAlphabet, IssueSeverity
|
|
12
|
+
from dnakit.core.facade import DNA
|
|
13
|
+
from dnakit.core.gap import Gap
|
|
14
|
+
from dnakit.core.issues import Issue
|
|
15
|
+
from dnakit.core.provenance import Provenance
|
|
16
|
+
from dnakit.core.record import DNARecord
|
|
17
|
+
from dnakit.core.sequence import DNASequence
|
|
18
|
+
from dnakit.exceptions import ConfigurationError
|
|
19
|
+
|
|
20
|
+
from ._shared import CANONICAL_BASES, IUPAC_BASES, ambiguity_report
|
|
21
|
+
from .config import DatasetValidationConfig, ValidationConfig
|
|
22
|
+
from .results import (
|
|
23
|
+
DatasetValidationReport,
|
|
24
|
+
DuplicateID,
|
|
25
|
+
QualitySummary,
|
|
26
|
+
ValidationReport,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def validate( # type: ignore[overload-overlap]
|
|
32
|
+
value: DNA,
|
|
33
|
+
*,
|
|
34
|
+
config: ValidationConfig | DatasetValidationConfig | None = None,
|
|
35
|
+
) -> ValidationReport | DatasetValidationReport: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@overload
|
|
39
|
+
def validate(
|
|
40
|
+
value: DNASequence | DNARecord,
|
|
41
|
+
*,
|
|
42
|
+
config: ValidationConfig | DatasetValidationConfig | None = None,
|
|
43
|
+
) -> ValidationReport: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def validate(
|
|
48
|
+
value: DNASet | Iterable[DNARecord],
|
|
49
|
+
*,
|
|
50
|
+
config: ValidationConfig | DatasetValidationConfig | None = None,
|
|
51
|
+
) -> DatasetValidationReport: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate(
|
|
55
|
+
value: DNA | DNASequence | DNARecord | DNASet | Iterable[DNARecord],
|
|
56
|
+
*,
|
|
57
|
+
config: ValidationConfig | DatasetValidationConfig | None = None,
|
|
58
|
+
) -> ValidationReport | DatasetValidationReport:
|
|
59
|
+
"""Validate one sequence/record or a record collection with one entry point.
|
|
60
|
+
|
|
61
|
+
The returned report type follows the input type. A collection is invalid
|
|
62
|
+
when any record violates its configured rules, when a required unique-ID
|
|
63
|
+
rule is violated, or when the collection is empty.
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(value, DNA):
|
|
66
|
+
if value.is_single:
|
|
67
|
+
return _validate_one(value.record, config=_resolve_record_config(config))
|
|
68
|
+
return _validate_collection(value.records, config=_resolve_dataset_config(config))
|
|
69
|
+
if isinstance(value, (DNASequence, DNARecord)):
|
|
70
|
+
return _validate_one(value, config=_resolve_record_config(config))
|
|
71
|
+
if isinstance(value, Iterable):
|
|
72
|
+
return _validate_collection(value, config=_resolve_dataset_config(config))
|
|
73
|
+
raise TypeError(
|
|
74
|
+
"validate expects a DNASequence, DNARecord, DNASet, or an iterable of DNARecord."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_record_config(
|
|
79
|
+
config: ValidationConfig | DatasetValidationConfig | None,
|
|
80
|
+
) -> ValidationConfig:
|
|
81
|
+
if config is None:
|
|
82
|
+
return ValidationConfig()
|
|
83
|
+
if isinstance(config, ValidationConfig):
|
|
84
|
+
return config
|
|
85
|
+
if isinstance(config, DatasetValidationConfig):
|
|
86
|
+
return config.record
|
|
87
|
+
raise ConfigurationError(
|
|
88
|
+
"config must be ValidationConfig, DatasetValidationConfig, or None.",
|
|
89
|
+
code="INVALID_VALIDATION_CONFIG",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _resolve_dataset_config(
|
|
94
|
+
config: ValidationConfig | DatasetValidationConfig | None,
|
|
95
|
+
) -> DatasetValidationConfig:
|
|
96
|
+
if config is None:
|
|
97
|
+
return DatasetValidationConfig()
|
|
98
|
+
if isinstance(config, DatasetValidationConfig):
|
|
99
|
+
return config
|
|
100
|
+
if isinstance(config, ValidationConfig):
|
|
101
|
+
return DatasetValidationConfig(record=config)
|
|
102
|
+
raise ConfigurationError(
|
|
103
|
+
"config must be ValidationConfig, DatasetValidationConfig, or None.",
|
|
104
|
+
code="INVALID_DATASET_VALIDATION_CONFIG",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_one(
|
|
109
|
+
sequence: DNASequence | DNARecord,
|
|
110
|
+
*,
|
|
111
|
+
config: ValidationConfig,
|
|
112
|
+
) -> ValidationReport:
|
|
113
|
+
resolved = config
|
|
114
|
+
if isinstance(sequence, DNARecord):
|
|
115
|
+
record = sequence
|
|
116
|
+
dna = record.sequence
|
|
117
|
+
record_id: str | None = record.id
|
|
118
|
+
elif isinstance(sequence, DNASequence):
|
|
119
|
+
record = None
|
|
120
|
+
dna = sequence
|
|
121
|
+
record_id = None
|
|
122
|
+
else:
|
|
123
|
+
raise TypeError("validate expects a DNASequence or DNARecord.")
|
|
124
|
+
|
|
125
|
+
issues: list[Issue] = []
|
|
126
|
+
alphabet = resolved.alphabet or dna.alphabet
|
|
127
|
+
allowed = CANONICAL_BASES if alphabet is DNAAlphabet.STRICT else IUPAC_BASES
|
|
128
|
+
invalid_positions: dict[str, list[int]] = defaultdict(list)
|
|
129
|
+
symbol_offset = 0
|
|
130
|
+
unknown_gap_count = 0
|
|
131
|
+
gap_count = 0
|
|
132
|
+
|
|
133
|
+
for part_index, part in enumerate(dna.parts):
|
|
134
|
+
if isinstance(part, Gap):
|
|
135
|
+
gap_count += 1
|
|
136
|
+
if not resolved.allow_gaps:
|
|
137
|
+
issues.append(
|
|
138
|
+
_issue(
|
|
139
|
+
"STD_GAP_NOT_ALLOWED",
|
|
140
|
+
IssueSeverity.ERROR,
|
|
141
|
+
"The sequence contains a Gap but gaps are disabled.",
|
|
142
|
+
{"part_index": part_index},
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
if part.length is None:
|
|
146
|
+
unknown_gap_count += 1
|
|
147
|
+
continue
|
|
148
|
+
for symbol in part:
|
|
149
|
+
if symbol not in allowed:
|
|
150
|
+
invalid_positions[symbol].append(symbol_offset)
|
|
151
|
+
symbol_offset += 1
|
|
152
|
+
|
|
153
|
+
for symbol, positions in sorted(invalid_positions.items()):
|
|
154
|
+
issues.append(
|
|
155
|
+
_issue(
|
|
156
|
+
"STD_INVALID_SYMBOL",
|
|
157
|
+
IssueSeverity.ERROR,
|
|
158
|
+
f"Character {symbol!r} is not valid for the {alphabet.value} DNA alphabet.",
|
|
159
|
+
{
|
|
160
|
+
"symbol": symbol,
|
|
161
|
+
"codepoint": f"U+{ord(symbol):04X}",
|
|
162
|
+
"positions": positions,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if unknown_gap_count and not resolved.allow_unknown_gap_length:
|
|
168
|
+
issues.append(
|
|
169
|
+
_issue(
|
|
170
|
+
"STD_UNKNOWN_GAP_LENGTH",
|
|
171
|
+
IssueSeverity.ERROR,
|
|
172
|
+
f"Found {unknown_gap_count} Gap object(s) with unknown length.",
|
|
173
|
+
{"count": unknown_gap_count},
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
length = dna.symbol_length
|
|
178
|
+
if length == 0 and not resolved.allow_empty:
|
|
179
|
+
issues.append(
|
|
180
|
+
_issue(
|
|
181
|
+
"STD_EMPTY_SEQUENCE",
|
|
182
|
+
IssueSeverity.ERROR,
|
|
183
|
+
"The sequence contains no nucleotide symbols.",
|
|
184
|
+
{},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
if resolved.sequence_length is not None and length != resolved.sequence_length:
|
|
188
|
+
issues.append(
|
|
189
|
+
_issue(
|
|
190
|
+
"STD_SEQUENCE_LENGTH_MISMATCH",
|
|
191
|
+
IssueSeverity.ERROR,
|
|
192
|
+
f"Sequence length {length} does not match the configured exact length "
|
|
193
|
+
f"{resolved.sequence_length}.",
|
|
194
|
+
{"length": length, "expected_length": resolved.sequence_length},
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
if (
|
|
198
|
+
resolved.min_length is not None
|
|
199
|
+
and length < resolved.min_length
|
|
200
|
+
and not (length == 0 and (resolved.allow_empty or resolved.min_length > 0))
|
|
201
|
+
):
|
|
202
|
+
issues.append(
|
|
203
|
+
_issue(
|
|
204
|
+
"STD_SEQUENCE_TOO_SHORT",
|
|
205
|
+
IssueSeverity.ERROR,
|
|
206
|
+
f"Sequence length {length} is below the configured minimum {resolved.min_length}.",
|
|
207
|
+
{"length": length, "minimum": resolved.min_length},
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
if resolved.max_length is not None and length > resolved.max_length:
|
|
211
|
+
issues.append(
|
|
212
|
+
_issue(
|
|
213
|
+
"STD_SEQUENCE_TOO_LONG",
|
|
214
|
+
IssueSeverity.ERROR,
|
|
215
|
+
f"Sequence length {length} exceeds the configured maximum {resolved.max_length}.",
|
|
216
|
+
{"length": length, "maximum": resolved.max_length},
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
ambiguity = ambiguity_report(
|
|
221
|
+
dna.parts,
|
|
222
|
+
denominator_includes_gap=resolved.ambiguity_denominator_includes_gap,
|
|
223
|
+
)
|
|
224
|
+
if resolved.max_ambiguity_fraction is not None:
|
|
225
|
+
if ambiguity.fraction is None and ambiguity.denominator is None:
|
|
226
|
+
issues.append(
|
|
227
|
+
_issue(
|
|
228
|
+
"STD_AMBIGUITY_DENOMINATOR_UNKNOWN",
|
|
229
|
+
IssueSeverity.WARNING,
|
|
230
|
+
"Ambiguity fraction cannot include an unknown-length Gap.",
|
|
231
|
+
{"gap_count": gap_count, "unknown_gap_count": unknown_gap_count},
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
elif (
|
|
235
|
+
ambiguity.fraction is not None and ambiguity.fraction > resolved.max_ambiguity_fraction
|
|
236
|
+
):
|
|
237
|
+
issues.append(
|
|
238
|
+
_issue(
|
|
239
|
+
"STD_AMBIGUITY_FRACTION_HIGH",
|
|
240
|
+
IssueSeverity.ERROR,
|
|
241
|
+
f"Ambiguity fraction {ambiguity.fraction:.6g} exceeds the "
|
|
242
|
+
f"configured maximum {resolved.max_ambiguity_fraction:.6g}.",
|
|
243
|
+
{
|
|
244
|
+
"fraction": ambiguity.fraction,
|
|
245
|
+
"maximum": resolved.max_ambiguity_fraction,
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
quality: QualitySummary | None = None
|
|
251
|
+
if record is not None:
|
|
252
|
+
_validate_metadata(record, resolved, issues)
|
|
253
|
+
quality = _validate_letter_annotations(record, resolved, issues)
|
|
254
|
+
|
|
255
|
+
issue_tuple = tuple(issues)
|
|
256
|
+
return ValidationReport(
|
|
257
|
+
record_id=record_id,
|
|
258
|
+
sequence=dna,
|
|
259
|
+
config=resolved,
|
|
260
|
+
algorithm_version="std-validate-v2",
|
|
261
|
+
provenance=Provenance(),
|
|
262
|
+
is_valid=not _has_error(issue_tuple),
|
|
263
|
+
symbol_length=length,
|
|
264
|
+
coordinate_span=dna.coordinate_span,
|
|
265
|
+
ambiguity=ambiguity,
|
|
266
|
+
quality=quality,
|
|
267
|
+
issues=issue_tuple,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _validate_collection(
|
|
272
|
+
records: DNASet | Iterable[DNARecord],
|
|
273
|
+
*,
|
|
274
|
+
config: DatasetValidationConfig,
|
|
275
|
+
) -> DatasetValidationReport:
|
|
276
|
+
"""Validate records once, including stable duplicate-ID detection."""
|
|
277
|
+
resolved = config
|
|
278
|
+
seen_indices: dict[str, list[int]] = defaultdict(list)
|
|
279
|
+
reports: list[ValidationReport] = []
|
|
280
|
+
invalid_indices: list[int] = []
|
|
281
|
+
count = 0
|
|
282
|
+
|
|
283
|
+
for index, record in enumerate(records):
|
|
284
|
+
if not isinstance(record, DNARecord):
|
|
285
|
+
raise TypeError(
|
|
286
|
+
f"records item {index} has type {type(record).__name__}; expected DNARecord."
|
|
287
|
+
)
|
|
288
|
+
seen_indices[record.id].append(index)
|
|
289
|
+
report = _validate_one(record, config=resolved.record)
|
|
290
|
+
if not report.is_valid:
|
|
291
|
+
invalid_indices.append(index)
|
|
292
|
+
if resolved.collect_record_reports:
|
|
293
|
+
reports.append(report)
|
|
294
|
+
count += 1
|
|
295
|
+
|
|
296
|
+
duplicates = tuple(
|
|
297
|
+
DuplicateID(id=record_id, indices=tuple(indices))
|
|
298
|
+
for record_id, indices in seen_indices.items()
|
|
299
|
+
if len(indices) > 1
|
|
300
|
+
)
|
|
301
|
+
issues: list[Issue] = []
|
|
302
|
+
if count == 0:
|
|
303
|
+
issues.append(
|
|
304
|
+
_issue(
|
|
305
|
+
"STD_EMPTY_DATASET",
|
|
306
|
+
IssueSeverity.ERROR,
|
|
307
|
+
"The dataset contains no records.",
|
|
308
|
+
{},
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
if duplicates:
|
|
312
|
+
severity = IssueSeverity.ERROR if resolved.require_unique_ids else IssueSeverity.WARNING
|
|
313
|
+
issues.append(
|
|
314
|
+
_issue(
|
|
315
|
+
"STD_DUPLICATE_RECORD_ID",
|
|
316
|
+
severity,
|
|
317
|
+
f"Found {len(duplicates)} duplicated record identifier(s).",
|
|
318
|
+
{
|
|
319
|
+
"duplicates": [
|
|
320
|
+
{"id": item.id, "indices": list(item.indices)} for item in duplicates
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
if invalid_indices:
|
|
326
|
+
issues.append(
|
|
327
|
+
_issue(
|
|
328
|
+
"STD_INVALID_RECORDS",
|
|
329
|
+
IssueSeverity.ERROR,
|
|
330
|
+
f"Found {len(invalid_indices)} record(s) that failed validation.",
|
|
331
|
+
{"indices": invalid_indices},
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
issue_tuple = tuple(issues)
|
|
336
|
+
return DatasetValidationReport(
|
|
337
|
+
record_count=count,
|
|
338
|
+
config=resolved,
|
|
339
|
+
algorithm_version="std-validate-set-v2",
|
|
340
|
+
provenance=Provenance(),
|
|
341
|
+
ids_unique=not duplicates,
|
|
342
|
+
duplicate_ids=duplicates,
|
|
343
|
+
is_valid=not invalid_indices and not _has_error(issue_tuple),
|
|
344
|
+
issues=issue_tuple,
|
|
345
|
+
record_reports=tuple(reports) if resolved.collect_record_reports else None,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def validate_set(
|
|
350
|
+
records: DNA | DNASet | Iterable[DNARecord],
|
|
351
|
+
*,
|
|
352
|
+
config: DatasetValidationConfig | None = None,
|
|
353
|
+
) -> DatasetValidationReport:
|
|
354
|
+
"""Backward-compatible alias for ``validate(records, config=config)``."""
|
|
355
|
+
return _validate_collection(records, config=_resolve_dataset_config(config))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _validate_metadata(
|
|
359
|
+
record: DNARecord,
|
|
360
|
+
config: ValidationConfig,
|
|
361
|
+
issues: list[Issue],
|
|
362
|
+
) -> None:
|
|
363
|
+
missing = tuple(
|
|
364
|
+
field
|
|
365
|
+
for field in config.required_metadata_fields
|
|
366
|
+
if field not in record.metadata or record.metadata[field] is None
|
|
367
|
+
)
|
|
368
|
+
if missing:
|
|
369
|
+
issues.append(
|
|
370
|
+
_issue(
|
|
371
|
+
"STD_REQUIRED_METADATA_MISSING",
|
|
372
|
+
IssueSeverity.ERROR,
|
|
373
|
+
f"Record {record.id!r} is missing required metadata.",
|
|
374
|
+
{"fields": list(missing)},
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _validate_letter_annotations(
|
|
380
|
+
record: DNARecord,
|
|
381
|
+
config: ValidationConfig,
|
|
382
|
+
issues: list[Issue],
|
|
383
|
+
) -> QualitySummary | None:
|
|
384
|
+
missing = tuple(
|
|
385
|
+
field
|
|
386
|
+
for field in config.required_letter_annotations
|
|
387
|
+
if field not in record.letter_annotations
|
|
388
|
+
)
|
|
389
|
+
if missing:
|
|
390
|
+
issues.append(
|
|
391
|
+
_issue(
|
|
392
|
+
"STD_REQUIRED_LETTER_ANNOTATION_MISSING",
|
|
393
|
+
IssueSeverity.ERROR,
|
|
394
|
+
f"Record {record.id!r} is missing required letter annotations.",
|
|
395
|
+
{"fields": list(missing)},
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if not config.check_phred_quality or "phred_quality" not in record.letter_annotations:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
raw_values = record.letter_annotations["phred_quality"]
|
|
403
|
+
if len(raw_values) != record.sequence.symbol_length:
|
|
404
|
+
issues.append(
|
|
405
|
+
_issue(
|
|
406
|
+
"STD_QUALITY_LENGTH_MISMATCH",
|
|
407
|
+
IssueSeverity.ERROR,
|
|
408
|
+
"PHRED quality count does not match sequence symbol length.",
|
|
409
|
+
{
|
|
410
|
+
"quality_count": len(raw_values),
|
|
411
|
+
"symbol_length": record.sequence.symbol_length,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
values: list[float] = []
|
|
417
|
+
invalid_indices: list[int] = []
|
|
418
|
+
out_of_range: list[int] = []
|
|
419
|
+
for index, value in enumerate(raw_values):
|
|
420
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
421
|
+
invalid_indices.append(index)
|
|
422
|
+
continue
|
|
423
|
+
numeric = float(value)
|
|
424
|
+
if not math.isfinite(numeric):
|
|
425
|
+
invalid_indices.append(index)
|
|
426
|
+
continue
|
|
427
|
+
values.append(numeric)
|
|
428
|
+
if not config.minimum_phred <= numeric <= config.maximum_phred:
|
|
429
|
+
out_of_range.append(index)
|
|
430
|
+
|
|
431
|
+
if invalid_indices:
|
|
432
|
+
issues.append(
|
|
433
|
+
_issue(
|
|
434
|
+
"STD_QUALITY_NON_NUMERIC",
|
|
435
|
+
IssueSeverity.ERROR,
|
|
436
|
+
"PHRED quality contains non-finite or non-numeric values.",
|
|
437
|
+
{"indices": invalid_indices},
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
if out_of_range:
|
|
441
|
+
issues.append(
|
|
442
|
+
_issue(
|
|
443
|
+
"STD_QUALITY_OUT_OF_RANGE",
|
|
444
|
+
IssueSeverity.ERROR,
|
|
445
|
+
"PHRED quality contains values outside the configured range.",
|
|
446
|
+
{
|
|
447
|
+
"indices": out_of_range,
|
|
448
|
+
"minimum": config.minimum_phred,
|
|
449
|
+
"maximum": config.maximum_phred,
|
|
450
|
+
},
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
mean = sum(values) / len(values) if values else None
|
|
455
|
+
if (
|
|
456
|
+
config.minimum_mean_phred is not None
|
|
457
|
+
and mean is not None
|
|
458
|
+
and mean < config.minimum_mean_phred
|
|
459
|
+
):
|
|
460
|
+
issues.append(
|
|
461
|
+
_issue(
|
|
462
|
+
"STD_QUALITY_MEAN_LOW",
|
|
463
|
+
IssueSeverity.ERROR,
|
|
464
|
+
f"Mean PHRED quality {mean:.6g} is below the configured minimum "
|
|
465
|
+
f"{config.minimum_mean_phred:.6g}.",
|
|
466
|
+
{"mean": mean, "minimum": config.minimum_mean_phred},
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
return QualitySummary(
|
|
470
|
+
count=len(raw_values),
|
|
471
|
+
minimum=min(values) if values else None,
|
|
472
|
+
maximum=max(values) if values else None,
|
|
473
|
+
mean=mean,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _has_error(issues: tuple[Issue, ...]) -> bool:
|
|
478
|
+
return any(issue.severity is IssueSeverity.ERROR for issue in issues)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _issue(
|
|
482
|
+
code: str,
|
|
483
|
+
severity: IssueSeverity,
|
|
484
|
+
message: str,
|
|
485
|
+
details: dict[str, object],
|
|
486
|
+
) -> Issue:
|
|
487
|
+
return Issue(code=code, severity=severity, message=message, details=details)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Explicit-coordinate DNA geometry and standard 3DNA/DSSR result parsing."""
|
|
2
|
+
|
|
3
|
+
from .geometry import StructureProgress, analyze_ensemble_flexibility, analyze_structure
|
|
4
|
+
from .pdb import load_pdb, load_pdb_ensemble
|
|
5
|
+
from .results import (
|
|
6
|
+
Atom3D,
|
|
7
|
+
BackboneTorsion,
|
|
8
|
+
BasePairStepParameters,
|
|
9
|
+
ChainGeometry,
|
|
10
|
+
Conditional3DFeature,
|
|
11
|
+
DNA3DStructure,
|
|
12
|
+
DSSRSummaryResult,
|
|
13
|
+
EnsembleFlexibilityResult,
|
|
14
|
+
HelixGeometry,
|
|
15
|
+
HydrogenBondGeometry,
|
|
16
|
+
Residue3D,
|
|
17
|
+
ResidueFlexibility,
|
|
18
|
+
ShapeGeometry,
|
|
19
|
+
Structure3DAnalysisResult,
|
|
20
|
+
ThreeDNAParameterResult,
|
|
21
|
+
)
|
|
22
|
+
from .threedna import read_3dna_bp_step, read_dssr_json
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Atom3D",
|
|
26
|
+
"BackboneTorsion",
|
|
27
|
+
"BasePairStepParameters",
|
|
28
|
+
"ChainGeometry",
|
|
29
|
+
"Conditional3DFeature",
|
|
30
|
+
"DNA3DStructure",
|
|
31
|
+
"DSSRSummaryResult",
|
|
32
|
+
"EnsembleFlexibilityResult",
|
|
33
|
+
"HelixGeometry",
|
|
34
|
+
"HydrogenBondGeometry",
|
|
35
|
+
"Residue3D",
|
|
36
|
+
"ResidueFlexibility",
|
|
37
|
+
"ShapeGeometry",
|
|
38
|
+
"Structure3DAnalysisResult",
|
|
39
|
+
"StructureProgress",
|
|
40
|
+
"ThreeDNAParameterResult",
|
|
41
|
+
"analyze_ensemble_flexibility",
|
|
42
|
+
"analyze_structure",
|
|
43
|
+
"load_pdb",
|
|
44
|
+
"load_pdb_ensemble",
|
|
45
|
+
"read_3dna_bp_step",
|
|
46
|
+
"read_dssr_json",
|
|
47
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared provenance for native coordinate analysis and 3DNA/DSSR adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dnakit.core import (
|
|
6
|
+
Citation,
|
|
7
|
+
ExecutionMode,
|
|
8
|
+
ImplementationInfo,
|
|
9
|
+
ImplementationLabel,
|
|
10
|
+
OriginClass,
|
|
11
|
+
Provenance,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def native_structure_provenance() -> Provenance:
|
|
16
|
+
return Provenance(
|
|
17
|
+
implementation=ImplementationInfo(
|
|
18
|
+
label=ImplementationLabel.NOVEL,
|
|
19
|
+
execution_mode=ExecutionMode.INTERNAL,
|
|
20
|
+
origin_class=OriginClass.DNAKIT,
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def threedna_provenance() -> Provenance:
|
|
26
|
+
return Provenance(
|
|
27
|
+
implementation=ImplementationInfo(
|
|
28
|
+
label=ImplementationLabel.ADAPTER,
|
|
29
|
+
execution_mode=ExecutionMode.HYBRID,
|
|
30
|
+
origin_class=OriginClass.INTEGRATION,
|
|
31
|
+
citations=(
|
|
32
|
+
Citation(
|
|
33
|
+
"3dna2003",
|
|
34
|
+
title="3DNA: analysis, rebuilding and visualization of 3D nucleic acids",
|
|
35
|
+
doi="10.1093/nar/gkg680",
|
|
36
|
+
),
|
|
37
|
+
Citation(
|
|
38
|
+
"dssr2015",
|
|
39
|
+
title="DSSR: an integrated tool for nucleic acid spatial structure",
|
|
40
|
+
doi="10.1093/nar/gkv716",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["native_structure_provenance", "threedna_provenance"]
|