dnakit 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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"]