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,257 @@
|
|
|
1
|
+
"""Deterministic transcription and genetic-code translation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, TypeAlias
|
|
6
|
+
|
|
7
|
+
from dnakit.core import DNASequence
|
|
8
|
+
from dnakit.exceptions import ConfigurationError, InvalidAlphabetError, UnsupportedGapOperationError
|
|
9
|
+
from dnakit.ops._common import require_sequence
|
|
10
|
+
|
|
11
|
+
StrandPolicy: TypeAlias = Literal["forward", "reverse"]
|
|
12
|
+
StopPolicy: TypeAlias = Literal["include", "truncate", "error"]
|
|
13
|
+
UnknownCodonPolicy: TypeAlias = Literal["x", "error"]
|
|
14
|
+
IncompleteCodonPolicy: TypeAlias = Literal["ignore", "error"]
|
|
15
|
+
|
|
16
|
+
_IUPAC_DNA = frozenset("ACGTRYSWKMBDHVN")
|
|
17
|
+
_IUPAC_RNA = frozenset("ACGURYSWKMBDHVN")
|
|
18
|
+
_DNA_COMPLEMENT = str.maketrans(
|
|
19
|
+
{
|
|
20
|
+
"A": "T",
|
|
21
|
+
"C": "G",
|
|
22
|
+
"G": "C",
|
|
23
|
+
"T": "A",
|
|
24
|
+
"R": "Y",
|
|
25
|
+
"Y": "R",
|
|
26
|
+
"S": "S",
|
|
27
|
+
"W": "W",
|
|
28
|
+
"K": "M",
|
|
29
|
+
"M": "K",
|
|
30
|
+
"B": "V",
|
|
31
|
+
"D": "H",
|
|
32
|
+
"H": "D",
|
|
33
|
+
"V": "B",
|
|
34
|
+
"N": "N",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# NCBI genetic code table 1, represented with DNA codons.
|
|
39
|
+
_TABLE_1 = {
|
|
40
|
+
"TTT": "F",
|
|
41
|
+
"TTC": "F",
|
|
42
|
+
"TTA": "L",
|
|
43
|
+
"TTG": "L",
|
|
44
|
+
"TCT": "S",
|
|
45
|
+
"TCC": "S",
|
|
46
|
+
"TCA": "S",
|
|
47
|
+
"TCG": "S",
|
|
48
|
+
"TAT": "Y",
|
|
49
|
+
"TAC": "Y",
|
|
50
|
+
"TAA": "*",
|
|
51
|
+
"TAG": "*",
|
|
52
|
+
"TGT": "C",
|
|
53
|
+
"TGC": "C",
|
|
54
|
+
"TGA": "*",
|
|
55
|
+
"TGG": "W",
|
|
56
|
+
"CTT": "L",
|
|
57
|
+
"CTC": "L",
|
|
58
|
+
"CTA": "L",
|
|
59
|
+
"CTG": "L",
|
|
60
|
+
"CCT": "P",
|
|
61
|
+
"CCC": "P",
|
|
62
|
+
"CCA": "P",
|
|
63
|
+
"CCG": "P",
|
|
64
|
+
"CAT": "H",
|
|
65
|
+
"CAC": "H",
|
|
66
|
+
"CAA": "Q",
|
|
67
|
+
"CAG": "Q",
|
|
68
|
+
"CGT": "R",
|
|
69
|
+
"CGC": "R",
|
|
70
|
+
"CGA": "R",
|
|
71
|
+
"CGG": "R",
|
|
72
|
+
"ATT": "I",
|
|
73
|
+
"ATC": "I",
|
|
74
|
+
"ATA": "I",
|
|
75
|
+
"ATG": "M",
|
|
76
|
+
"ACT": "T",
|
|
77
|
+
"ACC": "T",
|
|
78
|
+
"ACA": "T",
|
|
79
|
+
"ACG": "T",
|
|
80
|
+
"AAT": "N",
|
|
81
|
+
"AAC": "N",
|
|
82
|
+
"AAA": "K",
|
|
83
|
+
"AAG": "K",
|
|
84
|
+
"AGT": "S",
|
|
85
|
+
"AGC": "S",
|
|
86
|
+
"AGA": "R",
|
|
87
|
+
"AGG": "R",
|
|
88
|
+
"GTT": "V",
|
|
89
|
+
"GTC": "V",
|
|
90
|
+
"GTA": "V",
|
|
91
|
+
"GTG": "V",
|
|
92
|
+
"GCT": "A",
|
|
93
|
+
"GCC": "A",
|
|
94
|
+
"GCA": "A",
|
|
95
|
+
"GCG": "A",
|
|
96
|
+
"GAT": "D",
|
|
97
|
+
"GAC": "D",
|
|
98
|
+
"GAA": "E",
|
|
99
|
+
"GAG": "E",
|
|
100
|
+
"GGT": "G",
|
|
101
|
+
"GGC": "G",
|
|
102
|
+
"GGA": "G",
|
|
103
|
+
"GGG": "G",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _coerce_strand(strand: StrandPolicy) -> StrandPolicy:
|
|
108
|
+
if strand not in ("forward", "reverse"):
|
|
109
|
+
raise ConfigurationError(
|
|
110
|
+
"strand must be 'forward' or 'reverse'.",
|
|
111
|
+
code="INVALID_STRAND_POLICY",
|
|
112
|
+
context={"strand": strand},
|
|
113
|
+
)
|
|
114
|
+
return strand
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ungapped_symbols(sequence: DNASequence, *, operation: str) -> str:
|
|
118
|
+
resolved = require_sequence(sequence)
|
|
119
|
+
if resolved.is_gapped:
|
|
120
|
+
raise UnsupportedGapOperationError(
|
|
121
|
+
f"{operation} does not silently omit Gap objects.",
|
|
122
|
+
code="UNSUPPORTED_GAPPED_OPERATION",
|
|
123
|
+
context={"operation": operation},
|
|
124
|
+
hint="Translate or transcribe each nucleotide fragment separately.",
|
|
125
|
+
)
|
|
126
|
+
return resolved.symbols
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def transcribe(sequence: DNASequence, *, strand: StrandPolicy = "forward") -> str:
|
|
130
|
+
"""Transcribe DNA to RNA on the selected strand.
|
|
131
|
+
|
|
132
|
+
The return value is uppercase RNA text. Explicit gaps are rejected rather
|
|
133
|
+
than silently removed. ``strand='reverse'`` transcribes the reverse
|
|
134
|
+
complement of the supplied DNA representation.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
symbols = _ungapped_symbols(sequence, operation="transcribe")
|
|
138
|
+
if _coerce_strand(strand) == "reverse":
|
|
139
|
+
symbols = symbols.translate(_DNA_COMPLEMENT)[::-1]
|
|
140
|
+
return symbols.replace("T", "U")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _coerce_nucleic_text(sequence: DNASequence | str) -> str:
|
|
144
|
+
if isinstance(sequence, DNASequence):
|
|
145
|
+
return _ungapped_symbols(sequence, operation="translate")
|
|
146
|
+
if not isinstance(sequence, str):
|
|
147
|
+
raise InvalidAlphabetError(
|
|
148
|
+
"Translation input must be DNASequence or uppercase DNA/RNA text.",
|
|
149
|
+
context={"type": type(sequence).__name__},
|
|
150
|
+
)
|
|
151
|
+
symbols = set(sequence)
|
|
152
|
+
if "T" in symbols and "U" in symbols:
|
|
153
|
+
raise InvalidAlphabetError(
|
|
154
|
+
"Translation input cannot mix thymine and uracil.",
|
|
155
|
+
code="MIXED_NUCLEIC_ACID_ALPHABET",
|
|
156
|
+
)
|
|
157
|
+
allowed = _IUPAC_RNA if "U" in symbols else _IUPAC_DNA
|
|
158
|
+
invalid = sorted(symbols - allowed)
|
|
159
|
+
if invalid:
|
|
160
|
+
raise InvalidAlphabetError(
|
|
161
|
+
"Translation input contains invalid or non-normalized symbols.",
|
|
162
|
+
context={"invalid_symbols": invalid},
|
|
163
|
+
hint="Uppercase and clean raw input before translation.",
|
|
164
|
+
)
|
|
165
|
+
return sequence.replace("U", "T")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def translate(
|
|
169
|
+
sequence: DNASequence | str,
|
|
170
|
+
*,
|
|
171
|
+
frame: int = 0,
|
|
172
|
+
table: int = 1,
|
|
173
|
+
strand: StrandPolicy = "forward",
|
|
174
|
+
stop_policy: StopPolicy = "include",
|
|
175
|
+
unknown_policy: UnknownCodonPolicy = "x",
|
|
176
|
+
incomplete_policy: IncompleteCodonPolicy = "ignore",
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Translate DNA or RNA using NCBI genetic code table 1.
|
|
179
|
+
|
|
180
|
+
Coordinates use a zero-based frame (0, 1, or 2). Ambiguous codons become
|
|
181
|
+
``X`` or raise according to ``unknown_policy``. Stop codons are included as
|
|
182
|
+
``*``, truncate translation, or raise according to ``stop_policy``. A
|
|
183
|
+
trailing partial codon is ignored or rejected according to
|
|
184
|
+
``incomplete_policy``.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if isinstance(frame, bool) or not isinstance(frame, int) or frame not in (0, 1, 2):
|
|
188
|
+
raise ConfigurationError(
|
|
189
|
+
"frame must be one of 0, 1, or 2.",
|
|
190
|
+
code="INVALID_READING_FRAME",
|
|
191
|
+
context={"frame": frame},
|
|
192
|
+
)
|
|
193
|
+
if isinstance(table, bool) or not isinstance(table, int) or table != 1:
|
|
194
|
+
raise ConfigurationError(
|
|
195
|
+
"Only NCBI genetic code table 1 is available in the MVP.",
|
|
196
|
+
code="UNSUPPORTED_GENETIC_CODE",
|
|
197
|
+
context={"table": table},
|
|
198
|
+
)
|
|
199
|
+
_coerce_strand(strand)
|
|
200
|
+
if stop_policy not in ("include", "truncate", "error"):
|
|
201
|
+
raise ConfigurationError(
|
|
202
|
+
"stop_policy must be 'include', 'truncate', or 'error'.",
|
|
203
|
+
code="INVALID_STOP_POLICY",
|
|
204
|
+
context={"stop_policy": stop_policy},
|
|
205
|
+
)
|
|
206
|
+
if unknown_policy not in ("x", "error"):
|
|
207
|
+
raise ConfigurationError(
|
|
208
|
+
"unknown_policy must be 'x' or 'error'.",
|
|
209
|
+
code="INVALID_UNKNOWN_CODON_POLICY",
|
|
210
|
+
context={"unknown_policy": unknown_policy},
|
|
211
|
+
)
|
|
212
|
+
if incomplete_policy not in ("ignore", "error"):
|
|
213
|
+
raise ConfigurationError(
|
|
214
|
+
"incomplete_policy must be 'ignore' or 'error'.",
|
|
215
|
+
code="INVALID_INCOMPLETE_CODON_POLICY",
|
|
216
|
+
context={"incomplete_policy": incomplete_policy},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
dna = _coerce_nucleic_text(sequence)
|
|
220
|
+
if strand == "reverse":
|
|
221
|
+
dna = dna.translate(_DNA_COMPLEMENT)[::-1]
|
|
222
|
+
coding = dna[frame:]
|
|
223
|
+
remainder = len(coding) % 3
|
|
224
|
+
if remainder and incomplete_policy == "error":
|
|
225
|
+
raise InvalidAlphabetError(
|
|
226
|
+
"Translation input has a trailing incomplete codon.",
|
|
227
|
+
code="INCOMPLETE_CODON",
|
|
228
|
+
context={"frame": frame, "trailing_symbols": remainder},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
protein: list[str] = []
|
|
232
|
+
coding_end = len(coding) - remainder
|
|
233
|
+
for offset in range(0, coding_end, 3):
|
|
234
|
+
codon = coding[offset : offset + 3]
|
|
235
|
+
amino_acid = _TABLE_1.get(codon)
|
|
236
|
+
if amino_acid is None:
|
|
237
|
+
if unknown_policy == "error":
|
|
238
|
+
raise InvalidAlphabetError(
|
|
239
|
+
"Ambiguous codon cannot be translated under unknown_policy='error'.",
|
|
240
|
+
code="AMBIGUOUS_CODON",
|
|
241
|
+
context={"codon": codon, "codon_index": offset // 3},
|
|
242
|
+
)
|
|
243
|
+
amino_acid = "X"
|
|
244
|
+
if amino_acid == "*":
|
|
245
|
+
if stop_policy == "truncate":
|
|
246
|
+
break
|
|
247
|
+
if stop_policy == "error":
|
|
248
|
+
raise InvalidAlphabetError(
|
|
249
|
+
"Stop codon encountered under stop_policy='error'.",
|
|
250
|
+
code="STOP_CODON",
|
|
251
|
+
context={"codon": codon, "codon_index": offset // 3},
|
|
252
|
+
)
|
|
253
|
+
protein.append(amino_acid)
|
|
254
|
+
return "".join(protein)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = ["transcribe", "translate"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Deterministic sequence-pattern annotation APIs."""
|
|
2
|
+
|
|
3
|
+
from dnakit.patterns.coding import scan_codon_sites, scan_orfs
|
|
4
|
+
from dnakit.patterns.crispr import (
|
|
5
|
+
BUILTIN_PAM_RULES,
|
|
6
|
+
PAM_CATALOG_VERSION,
|
|
7
|
+
PAMRule,
|
|
8
|
+
scan_pam_candidates,
|
|
9
|
+
)
|
|
10
|
+
from dnakit.patterns.motif import (
|
|
11
|
+
PROMOTER_CATALOG_VERSION,
|
|
12
|
+
PROMOTER_MOTIFS,
|
|
13
|
+
PWM,
|
|
14
|
+
scan_motif,
|
|
15
|
+
scan_promoter_motifs,
|
|
16
|
+
scan_pwm,
|
|
17
|
+
scan_tf_pwm,
|
|
18
|
+
)
|
|
19
|
+
from dnakit.patterns.regions import find_cpg_islands, find_low_complexity_regions
|
|
20
|
+
from dnakit.patterns.repeats import (
|
|
21
|
+
find_inverted_repeats,
|
|
22
|
+
find_microsatellites,
|
|
23
|
+
find_reverse_complement_palindromes,
|
|
24
|
+
find_tandem_repeats,
|
|
25
|
+
)
|
|
26
|
+
from dnakit.patterns.restriction import (
|
|
27
|
+
BUILTIN_RESTRICTION_ENZYMES,
|
|
28
|
+
RESTRICTION_CATALOG_VERSION,
|
|
29
|
+
RestrictionEnzyme,
|
|
30
|
+
scan_restriction_sites,
|
|
31
|
+
)
|
|
32
|
+
from dnakit.patterns.results import (
|
|
33
|
+
CodonSite,
|
|
34
|
+
GuideCandidate,
|
|
35
|
+
InvertedRepeatHit,
|
|
36
|
+
LowComplexityResult,
|
|
37
|
+
MotifHit,
|
|
38
|
+
ORFHit,
|
|
39
|
+
PalindromeHit,
|
|
40
|
+
PatternResult,
|
|
41
|
+
RegionHit,
|
|
42
|
+
RestrictionSiteHit,
|
|
43
|
+
TandemRepeatHit,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"BUILTIN_PAM_RULES",
|
|
48
|
+
"BUILTIN_RESTRICTION_ENZYMES",
|
|
49
|
+
"PAM_CATALOG_VERSION",
|
|
50
|
+
"PROMOTER_CATALOG_VERSION",
|
|
51
|
+
"PROMOTER_MOTIFS",
|
|
52
|
+
"PWM",
|
|
53
|
+
"RESTRICTION_CATALOG_VERSION",
|
|
54
|
+
"CodonSite",
|
|
55
|
+
"GuideCandidate",
|
|
56
|
+
"InvertedRepeatHit",
|
|
57
|
+
"LowComplexityResult",
|
|
58
|
+
"MotifHit",
|
|
59
|
+
"ORFHit",
|
|
60
|
+
"PAMRule",
|
|
61
|
+
"PalindromeHit",
|
|
62
|
+
"PatternResult",
|
|
63
|
+
"RegionHit",
|
|
64
|
+
"RestrictionEnzyme",
|
|
65
|
+
"RestrictionSiteHit",
|
|
66
|
+
"TandemRepeatHit",
|
|
67
|
+
"find_cpg_islands",
|
|
68
|
+
"find_inverted_repeats",
|
|
69
|
+
"find_low_complexity_regions",
|
|
70
|
+
"find_microsatellites",
|
|
71
|
+
"find_reverse_complement_palindromes",
|
|
72
|
+
"find_tandem_repeats",
|
|
73
|
+
"scan_codon_sites",
|
|
74
|
+
"scan_motif",
|
|
75
|
+
"scan_orfs",
|
|
76
|
+
"scan_pam_candidates",
|
|
77
|
+
"scan_promoter_motifs",
|
|
78
|
+
"scan_pwm",
|
|
79
|
+
"scan_restriction_sites",
|
|
80
|
+
"scan_tf_pwm",
|
|
81
|
+
]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Shared bounded traversal and coordinate helpers for pattern analyses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from dnakit.core import (
|
|
10
|
+
DNA,
|
|
11
|
+
CompoundLocation,
|
|
12
|
+
DNARecord,
|
|
13
|
+
DNASequence,
|
|
14
|
+
ExecutionMode,
|
|
15
|
+
ImplementationInfo,
|
|
16
|
+
ImplementationLabel,
|
|
17
|
+
Interval,
|
|
18
|
+
Issue,
|
|
19
|
+
IssueSeverity,
|
|
20
|
+
Location,
|
|
21
|
+
OriginClass,
|
|
22
|
+
Provenance,
|
|
23
|
+
ReferenceInfo,
|
|
24
|
+
Strand,
|
|
25
|
+
UnresolvedLocation,
|
|
26
|
+
)
|
|
27
|
+
from dnakit.core._json import FrozenDict, freeze_mapping
|
|
28
|
+
from dnakit.core.facade import resolve_single_dna
|
|
29
|
+
from dnakit.core.gap import Gap
|
|
30
|
+
from dnakit.exceptions import ConfigurationError, SequenceError
|
|
31
|
+
from dnakit.patterns.results import PatternResult
|
|
32
|
+
|
|
33
|
+
SequenceInput: TypeAlias = DNA | DNASequence | DNARecord
|
|
34
|
+
|
|
35
|
+
IUPAC_BASES: Mapping[str, frozenset[str]] = {
|
|
36
|
+
"A": frozenset("A"),
|
|
37
|
+
"C": frozenset("C"),
|
|
38
|
+
"G": frozenset("G"),
|
|
39
|
+
"T": frozenset("T"),
|
|
40
|
+
"R": frozenset("AG"),
|
|
41
|
+
"Y": frozenset("CT"),
|
|
42
|
+
"S": frozenset("CG"),
|
|
43
|
+
"W": frozenset("AT"),
|
|
44
|
+
"K": frozenset("GT"),
|
|
45
|
+
"M": frozenset("AC"),
|
|
46
|
+
"B": frozenset("CGT"),
|
|
47
|
+
"D": frozenset("AGT"),
|
|
48
|
+
"H": frozenset("ACT"),
|
|
49
|
+
"V": frozenset("ACG"),
|
|
50
|
+
"N": frozenset("ACGT"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Segment:
|
|
56
|
+
text: str
|
|
57
|
+
symbol_start: int
|
|
58
|
+
coordinate_start: int | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_sequence(value: SequenceInput) -> tuple[DNASequence, str | None]:
|
|
62
|
+
if isinstance(value, (DNA, DNARecord, DNASequence)):
|
|
63
|
+
sequence, record = resolve_single_dna(value)
|
|
64
|
+
return sequence, None if record is None else record.id
|
|
65
|
+
raise ConfigurationError(
|
|
66
|
+
"Pattern input must be a DNASequence or DNARecord.",
|
|
67
|
+
context={"input_type": type(value).__name__},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_positive_int(value: int, name: str) -> None:
|
|
72
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
73
|
+
raise ConfigurationError(f"{name} must be a positive integer.", context={name: value})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_non_negative_int(value: int, name: str) -> None:
|
|
77
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
78
|
+
raise ConfigurationError(f"{name} must be a non-negative integer.", context={name: value})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_bool(value: bool, name: str) -> None:
|
|
82
|
+
if not isinstance(value, bool):
|
|
83
|
+
raise ConfigurationError(f"{name} must be a boolean.", context={name: value})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def segments(sequence: DNASequence) -> tuple[Segment, ...]:
|
|
87
|
+
"""Split at every explicit Gap and retain all resolvable coordinates."""
|
|
88
|
+
|
|
89
|
+
resolved: list[Segment] = []
|
|
90
|
+
symbol_start = 0
|
|
91
|
+
coordinate: int | None = 0
|
|
92
|
+
for part in sequence.parts:
|
|
93
|
+
if isinstance(part, str):
|
|
94
|
+
if part:
|
|
95
|
+
resolved.append(Segment(part, symbol_start, coordinate))
|
|
96
|
+
symbol_start += len(part)
|
|
97
|
+
if coordinate is not None:
|
|
98
|
+
coordinate += len(part)
|
|
99
|
+
else:
|
|
100
|
+
if part.length is None:
|
|
101
|
+
coordinate = None
|
|
102
|
+
elif coordinate is not None:
|
|
103
|
+
coordinate += part.length
|
|
104
|
+
return tuple(resolved)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _split_location(start: int, length: int, total: int) -> Location:
|
|
108
|
+
if length < 0 or length > total:
|
|
109
|
+
raise ConfigurationError(
|
|
110
|
+
"A circular match length must be between zero and sequence length."
|
|
111
|
+
)
|
|
112
|
+
end = start + length
|
|
113
|
+
if end <= total:
|
|
114
|
+
return Interval(start, end)
|
|
115
|
+
return CompoundLocation((Interval(start, total), Interval(0, end - total)))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def segment_location(
|
|
119
|
+
segment: Segment,
|
|
120
|
+
local_start: int,
|
|
121
|
+
local_end: int,
|
|
122
|
+
) -> tuple[Location, Location]:
|
|
123
|
+
"""Map one non-wrapping segment-local interval to symbol and genomic coordinates."""
|
|
124
|
+
|
|
125
|
+
symbol_location = Interval(
|
|
126
|
+
segment.symbol_start + local_start,
|
|
127
|
+
segment.symbol_start + local_end,
|
|
128
|
+
)
|
|
129
|
+
if segment.coordinate_start is None:
|
|
130
|
+
coordinate_location: Location = UnresolvedLocation(
|
|
131
|
+
"unknown-length gap precedes this pattern hit",
|
|
132
|
+
(symbol_location,),
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
coordinate_location = Interval(
|
|
136
|
+
segment.coordinate_start + local_start,
|
|
137
|
+
segment.coordinate_start + local_end,
|
|
138
|
+
)
|
|
139
|
+
return symbol_location, coordinate_location
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def circular_location(start: int, length: int, total: int) -> tuple[Location, Location, bool]:
|
|
143
|
+
start %= total
|
|
144
|
+
location = _split_location(start, length, total)
|
|
145
|
+
return location, location, isinstance(location, CompoundLocation)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reverse_complement_text(text: str) -> str:
|
|
149
|
+
return DNASequence(text, alphabet="iupac").reverse_complement().symbols
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def iupac_compatible(left: str, right: str) -> bool:
|
|
153
|
+
return bool(IUPAC_BASES[left] & IUPAC_BASES[right])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def validate_iupac_text(value: str, name: str, *, allow_empty: bool = False) -> str:
|
|
157
|
+
if not isinstance(value, str):
|
|
158
|
+
raise ConfigurationError(f"{name} must be a string.")
|
|
159
|
+
resolved = value.upper()
|
|
160
|
+
if not resolved and not allow_empty:
|
|
161
|
+
raise ConfigurationError(f"{name} must be non-empty.")
|
|
162
|
+
invalid = sorted(set(resolved) - set(IUPAC_BASES))
|
|
163
|
+
if invalid:
|
|
164
|
+
raise ConfigurationError(
|
|
165
|
+
f"{name} contains symbols outside the DNA IUPAC alphabet.",
|
|
166
|
+
context={"invalid_symbols": invalid},
|
|
167
|
+
)
|
|
168
|
+
return resolved
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def require_ungapped_circular(sequence: DNASequence, operation: str) -> None:
|
|
172
|
+
if sequence.topology.value == "circular" and sequence.is_gapped:
|
|
173
|
+
raise SequenceError(
|
|
174
|
+
f"{operation} cannot resolve a circular origin across explicit gaps.",
|
|
175
|
+
code="GAPPED_CIRCULAR_PATTERN_UNSUPPORTED",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def require_linear(sequence: DNASequence, operation: str) -> None:
|
|
180
|
+
if sequence.topology.value == "circular":
|
|
181
|
+
raise SequenceError(
|
|
182
|
+
f"{operation} currently requires linear topology.",
|
|
183
|
+
code="CIRCULAR_PATTERN_UNSUPPORTED",
|
|
184
|
+
hint="Linearize at a documented origin before running this analysis.",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def pattern_provenance(
|
|
189
|
+
*,
|
|
190
|
+
reimplementation: bool,
|
|
191
|
+
reference_name: str | None = None,
|
|
192
|
+
reference_version: str | None = None,
|
|
193
|
+
) -> Provenance:
|
|
194
|
+
return Provenance(
|
|
195
|
+
implementation=ImplementationInfo(
|
|
196
|
+
label=(
|
|
197
|
+
ImplementationLabel.REIMPLEMENTATION
|
|
198
|
+
if reimplementation
|
|
199
|
+
else ImplementationLabel.NATIVE
|
|
200
|
+
),
|
|
201
|
+
execution_mode=ExecutionMode.INTERNAL,
|
|
202
|
+
origin_class=(
|
|
203
|
+
OriginClass.PUBLISHED_ALGORITHM if reimplementation else OriginClass.DNAKIT
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
reference=(
|
|
207
|
+
None
|
|
208
|
+
if reference_name is None
|
|
209
|
+
else ReferenceInfo(reference_name, version=reference_version)
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_result(
|
|
215
|
+
sequence: DNASequence,
|
|
216
|
+
sequence_id: str | None,
|
|
217
|
+
*,
|
|
218
|
+
name: str,
|
|
219
|
+
method: str,
|
|
220
|
+
algorithm_version: str,
|
|
221
|
+
parameters: Mapping[str, object],
|
|
222
|
+
hits: Iterable[object],
|
|
223
|
+
max_matches: int,
|
|
224
|
+
truncated: bool,
|
|
225
|
+
provenance: Provenance,
|
|
226
|
+
issues: Iterable[Issue] = (),
|
|
227
|
+
) -> PatternResult[object]:
|
|
228
|
+
gap_count = sum(isinstance(part, Gap) for part in sequence.parts)
|
|
229
|
+
unknown_gap_count = sum(
|
|
230
|
+
isinstance(part, Gap) and part.length is None for part in sequence.parts
|
|
231
|
+
)
|
|
232
|
+
resolved_issues = list(issues)
|
|
233
|
+
if truncated:
|
|
234
|
+
resolved_issues.append(
|
|
235
|
+
Issue(
|
|
236
|
+
"PATTERN_MATCH_LIMIT_REACHED",
|
|
237
|
+
IssueSeverity.WARNING,
|
|
238
|
+
"Pattern hits were truncated at max_matches.",
|
|
239
|
+
details={"max_matches": max_matches},
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
return PatternResult(
|
|
243
|
+
name=name,
|
|
244
|
+
method=method,
|
|
245
|
+
algorithm_version=algorithm_version,
|
|
246
|
+
sequence_id=sequence_id,
|
|
247
|
+
parameters=freeze_mapping(parameters),
|
|
248
|
+
hits=tuple(hits),
|
|
249
|
+
inspected_symbol_count=sequence.symbol_length,
|
|
250
|
+
gap_count=gap_count,
|
|
251
|
+
unknown_gap_count=unknown_gap_count,
|
|
252
|
+
max_matches=max_matches,
|
|
253
|
+
truncated=truncated,
|
|
254
|
+
coordinate_system="0-based-half-open",
|
|
255
|
+
gap_policy="split-no-crossing",
|
|
256
|
+
topology=sequence.topology.value,
|
|
257
|
+
provenance=provenance,
|
|
258
|
+
issues=tuple(resolved_issues),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def frozen(values: Mapping[str, object]) -> FrozenDict:
|
|
263
|
+
return freeze_mapping(values)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def coerce_strands(value: Strand | str) -> tuple[Strand, ...]:
|
|
267
|
+
try:
|
|
268
|
+
strand = value if isinstance(value, Strand) else Strand(value)
|
|
269
|
+
except (TypeError, ValueError) as exc:
|
|
270
|
+
raise ConfigurationError(
|
|
271
|
+
"Unknown strand selection.",
|
|
272
|
+
context={"strand": value},
|
|
273
|
+
hint="Choose 'forward', 'reverse', or 'both'.",
|
|
274
|
+
) from exc
|
|
275
|
+
if strand is Strand.BOTH:
|
|
276
|
+
return (Strand.FORWARD, Strand.REVERSE)
|
|
277
|
+
if strand not in (Strand.FORWARD, Strand.REVERSE):
|
|
278
|
+
raise ConfigurationError("Pattern strand must be forward, reverse, or both.")
|
|
279
|
+
return (strand,)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = ["IUPAC_BASES", "SequenceInput"]
|