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,480 @@
|
|
|
1
|
+
"""Bounded palindrome, inverted-repeat, tandem-repeat, and microsatellite scans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from dnakit.exceptions import ConfigurationError
|
|
9
|
+
from dnakit.patterns._shared import (
|
|
10
|
+
SequenceInput,
|
|
11
|
+
build_result,
|
|
12
|
+
iupac_compatible,
|
|
13
|
+
pattern_provenance,
|
|
14
|
+
require_linear,
|
|
15
|
+
resolve_sequence,
|
|
16
|
+
segment_location,
|
|
17
|
+
segments,
|
|
18
|
+
validate_bool,
|
|
19
|
+
validate_positive_int,
|
|
20
|
+
)
|
|
21
|
+
from dnakit.patterns.results import (
|
|
22
|
+
InvertedRepeatHit,
|
|
23
|
+
PalindromeHit,
|
|
24
|
+
PatternResult,
|
|
25
|
+
TandemRepeatHit,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_COMPLEMENT = str.maketrans(
|
|
29
|
+
{
|
|
30
|
+
"A": "T",
|
|
31
|
+
"C": "G",
|
|
32
|
+
"G": "C",
|
|
33
|
+
"T": "A",
|
|
34
|
+
"R": "Y",
|
|
35
|
+
"Y": "R",
|
|
36
|
+
"S": "S",
|
|
37
|
+
"W": "W",
|
|
38
|
+
"K": "M",
|
|
39
|
+
"M": "K",
|
|
40
|
+
"B": "V",
|
|
41
|
+
"D": "H",
|
|
42
|
+
"H": "D",
|
|
43
|
+
"V": "B",
|
|
44
|
+
"N": "N",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
_MAX_TANDEM_UNIT_LENGTH = 100_000
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _reverse_complement(text: str) -> str:
|
|
51
|
+
return text.translate(_COMPLEMENT)[::-1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _compatible(left: str, right: str) -> bool:
|
|
55
|
+
return len(left) == len(right) and all(
|
|
56
|
+
iupac_compatible(left_base, right_base)
|
|
57
|
+
for left_base, right_base in zip(left, right, strict=True)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_reverse_complement_palindromes(
|
|
62
|
+
value: SequenceInput,
|
|
63
|
+
*,
|
|
64
|
+
min_length: int = 4,
|
|
65
|
+
max_length: int = 100,
|
|
66
|
+
maximal_per_start: bool = True,
|
|
67
|
+
max_comparisons: int = 5_000_000,
|
|
68
|
+
max_comparison_cells: int = 50_000_000,
|
|
69
|
+
max_matches: int = 100_000,
|
|
70
|
+
) -> PatternResult[PalindromeHit]:
|
|
71
|
+
"""Find IUPAC-compatible reverse-complement palindromes within fragments."""
|
|
72
|
+
|
|
73
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
74
|
+
require_linear(sequence, "reverse-complement palindrome detection")
|
|
75
|
+
validate_positive_int(min_length, "min_length")
|
|
76
|
+
validate_positive_int(max_length, "max_length")
|
|
77
|
+
validate_positive_int(max_comparisons, "max_comparisons")
|
|
78
|
+
validate_positive_int(max_comparison_cells, "max_comparison_cells")
|
|
79
|
+
validate_positive_int(max_matches, "max_matches")
|
|
80
|
+
validate_bool(maximal_per_start, "maximal_per_start")
|
|
81
|
+
if min_length > max_length:
|
|
82
|
+
raise ConfigurationError("min_length cannot exceed max_length.")
|
|
83
|
+
|
|
84
|
+
hits: list[PalindromeHit] = []
|
|
85
|
+
comparisons = 0
|
|
86
|
+
comparison_cells = 0
|
|
87
|
+
truncated = False
|
|
88
|
+
for item in segments(sequence):
|
|
89
|
+
for start in range(len(item.text)):
|
|
90
|
+
matches: list[tuple[int, str]] = []
|
|
91
|
+
upper = min(max_length, len(item.text) - start)
|
|
92
|
+
for length in range(min_length, upper + 1):
|
|
93
|
+
comparisons += 1
|
|
94
|
+
comparison_cells += length
|
|
95
|
+
if comparisons > max_comparisons:
|
|
96
|
+
raise ConfigurationError("Palindrome scan exceeded max_comparisons.")
|
|
97
|
+
if comparison_cells > max_comparison_cells:
|
|
98
|
+
raise ConfigurationError("Palindrome scan exceeded max_comparison_cells.")
|
|
99
|
+
candidate = item.text[start : start + length]
|
|
100
|
+
if _compatible(candidate, _reverse_complement(candidate)):
|
|
101
|
+
matches.append((length, candidate))
|
|
102
|
+
selected = matches[-1:] if maximal_per_start else matches
|
|
103
|
+
for length, candidate in selected:
|
|
104
|
+
if len(hits) >= max_matches:
|
|
105
|
+
truncated = True
|
|
106
|
+
break
|
|
107
|
+
symbol_location, coordinate_location = segment_location(item, start, start + length)
|
|
108
|
+
hits.append(
|
|
109
|
+
PalindromeHit(
|
|
110
|
+
sequence=candidate,
|
|
111
|
+
symbol_location=symbol_location,
|
|
112
|
+
coordinate_location=coordinate_location,
|
|
113
|
+
length=length,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if truncated:
|
|
117
|
+
break
|
|
118
|
+
if truncated:
|
|
119
|
+
break
|
|
120
|
+
result = build_result(
|
|
121
|
+
sequence,
|
|
122
|
+
sequence_id,
|
|
123
|
+
name="reverse_complement_palindrome_scan",
|
|
124
|
+
method="bounded_exhaustive_iupac_compatibility",
|
|
125
|
+
algorithm_version="1.0",
|
|
126
|
+
parameters={
|
|
127
|
+
"min_length": min_length,
|
|
128
|
+
"max_length": max_length,
|
|
129
|
+
"maximal_per_start": maximal_per_start,
|
|
130
|
+
"ambiguity_rule": "IUPAC-set-intersection",
|
|
131
|
+
"comparisons": comparisons,
|
|
132
|
+
"max_comparisons": max_comparisons,
|
|
133
|
+
"comparison_cells": comparison_cells,
|
|
134
|
+
"max_comparison_cells": max_comparison_cells,
|
|
135
|
+
},
|
|
136
|
+
hits=hits,
|
|
137
|
+
max_matches=max_matches,
|
|
138
|
+
truncated=truncated,
|
|
139
|
+
provenance=pattern_provenance(reimplementation=False),
|
|
140
|
+
)
|
|
141
|
+
return cast(PatternResult[PalindromeHit], result)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def find_inverted_repeats(
|
|
145
|
+
value: SequenceInput,
|
|
146
|
+
*,
|
|
147
|
+
min_arm_length: int = 4,
|
|
148
|
+
max_arm_length: int = 50,
|
|
149
|
+
min_loop_length: int = 0,
|
|
150
|
+
max_loop_length: int = 100,
|
|
151
|
+
max_comparisons: int = 5_000_000,
|
|
152
|
+
max_comparison_cells: int = 50_000_000,
|
|
153
|
+
max_matches: int = 100_000,
|
|
154
|
+
) -> PatternResult[InvertedRepeatHit]:
|
|
155
|
+
"""Find exact/IUPAC-compatible inverted-repeat arms with bounded loops."""
|
|
156
|
+
|
|
157
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
158
|
+
require_linear(sequence, "inverted-repeat detection")
|
|
159
|
+
validate_positive_int(min_arm_length, "min_arm_length")
|
|
160
|
+
validate_positive_int(max_arm_length, "max_arm_length")
|
|
161
|
+
if (
|
|
162
|
+
isinstance(min_loop_length, bool)
|
|
163
|
+
or not isinstance(min_loop_length, int)
|
|
164
|
+
or min_loop_length < 0
|
|
165
|
+
):
|
|
166
|
+
raise ConfigurationError("min_loop_length must be a non-negative integer.")
|
|
167
|
+
if (
|
|
168
|
+
isinstance(max_loop_length, bool)
|
|
169
|
+
or not isinstance(max_loop_length, int)
|
|
170
|
+
or max_loop_length < 0
|
|
171
|
+
):
|
|
172
|
+
raise ConfigurationError("max_loop_length must be a non-negative integer.")
|
|
173
|
+
validate_positive_int(max_comparisons, "max_comparisons")
|
|
174
|
+
validate_positive_int(max_comparison_cells, "max_comparison_cells")
|
|
175
|
+
validate_positive_int(max_matches, "max_matches")
|
|
176
|
+
if min_arm_length > max_arm_length or min_loop_length > max_loop_length:
|
|
177
|
+
raise ConfigurationError("Minimum repeat bounds cannot exceed maximum bounds.")
|
|
178
|
+
|
|
179
|
+
hits: list[InvertedRepeatHit] = []
|
|
180
|
+
comparisons = 0
|
|
181
|
+
comparison_cells = 0
|
|
182
|
+
truncated = False
|
|
183
|
+
for item in segments(sequence):
|
|
184
|
+
text = item.text
|
|
185
|
+
for left_start in range(len(text)):
|
|
186
|
+
for arm_length in range(min_arm_length, max_arm_length + 1):
|
|
187
|
+
left_end = left_start + arm_length
|
|
188
|
+
if left_end > len(text):
|
|
189
|
+
break
|
|
190
|
+
left = text[left_start:left_end]
|
|
191
|
+
expected_right = _reverse_complement(left)
|
|
192
|
+
for loop_length in range(min_loop_length, max_loop_length + 1):
|
|
193
|
+
right_start = left_end + loop_length
|
|
194
|
+
right_end = right_start + arm_length
|
|
195
|
+
if right_end > len(text):
|
|
196
|
+
break
|
|
197
|
+
comparisons += 1
|
|
198
|
+
comparison_cells += arm_length
|
|
199
|
+
if comparisons > max_comparisons:
|
|
200
|
+
raise ConfigurationError("Inverted-repeat scan exceeded max_comparisons.")
|
|
201
|
+
if comparison_cells > max_comparison_cells:
|
|
202
|
+
raise ConfigurationError(
|
|
203
|
+
"Inverted-repeat scan exceeded max_comparison_cells."
|
|
204
|
+
)
|
|
205
|
+
right = text[right_start:right_end]
|
|
206
|
+
if not _compatible(right, expected_right):
|
|
207
|
+
continue
|
|
208
|
+
if len(hits) >= max_matches:
|
|
209
|
+
truncated = True
|
|
210
|
+
break
|
|
211
|
+
symbol_location, coordinate_location = segment_location(
|
|
212
|
+
item, left_start, right_end
|
|
213
|
+
)
|
|
214
|
+
left_location, _ = segment_location(item, left_start, left_end)
|
|
215
|
+
loop_location, _ = segment_location(item, left_end, right_start)
|
|
216
|
+
right_location, _ = segment_location(item, right_start, right_end)
|
|
217
|
+
hits.append(
|
|
218
|
+
InvertedRepeatHit(
|
|
219
|
+
left_arm=left,
|
|
220
|
+
right_arm=right,
|
|
221
|
+
arm_length=arm_length,
|
|
222
|
+
loop_length=loop_length,
|
|
223
|
+
symbol_location=symbol_location,
|
|
224
|
+
coordinate_location=coordinate_location,
|
|
225
|
+
left_symbol_location=left_location,
|
|
226
|
+
loop_symbol_location=loop_location,
|
|
227
|
+
right_symbol_location=right_location,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
if truncated:
|
|
231
|
+
break
|
|
232
|
+
if truncated:
|
|
233
|
+
break
|
|
234
|
+
if truncated:
|
|
235
|
+
break
|
|
236
|
+
result = build_result(
|
|
237
|
+
sequence,
|
|
238
|
+
sequence_id,
|
|
239
|
+
name="inverted_repeat_scan",
|
|
240
|
+
method="bounded_exact_arm_loop_enumeration",
|
|
241
|
+
algorithm_version="1.0",
|
|
242
|
+
parameters={
|
|
243
|
+
"min_arm_length": min_arm_length,
|
|
244
|
+
"max_arm_length": max_arm_length,
|
|
245
|
+
"min_loop_length": min_loop_length,
|
|
246
|
+
"max_loop_length": max_loop_length,
|
|
247
|
+
"mismatches": 0,
|
|
248
|
+
"indels": 0,
|
|
249
|
+
"ambiguity_rule": "IUPAC-set-intersection",
|
|
250
|
+
"comparisons": comparisons,
|
|
251
|
+
"max_comparisons": max_comparisons,
|
|
252
|
+
"comparison_cells": comparison_cells,
|
|
253
|
+
"max_comparison_cells": max_comparison_cells,
|
|
254
|
+
},
|
|
255
|
+
hits=hits,
|
|
256
|
+
max_matches=max_matches,
|
|
257
|
+
truncated=truncated,
|
|
258
|
+
provenance=pattern_provenance(reimplementation=False),
|
|
259
|
+
)
|
|
260
|
+
return cast(PatternResult[InvertedRepeatHit], result)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _repeat_thresholds(
|
|
264
|
+
min_unit_length: int,
|
|
265
|
+
max_unit_length: int,
|
|
266
|
+
min_repeats: int,
|
|
267
|
+
values: Mapping[int, int] | None,
|
|
268
|
+
) -> dict[int, int]:
|
|
269
|
+
if values is None:
|
|
270
|
+
return {length: min_repeats for length in range(min_unit_length, max_unit_length + 1)}
|
|
271
|
+
resolved: dict[int, int] = {}
|
|
272
|
+
expected_count = max_unit_length - min_unit_length + 1
|
|
273
|
+
if len(values) != expected_count:
|
|
274
|
+
raise ConfigurationError(
|
|
275
|
+
"min_repeats_by_unit must define every accepted unit length.",
|
|
276
|
+
context={"expected_count": expected_count, "actual_count": len(values)},
|
|
277
|
+
)
|
|
278
|
+
for unit_length in range(min_unit_length, max_unit_length + 1):
|
|
279
|
+
try:
|
|
280
|
+
threshold = values[unit_length]
|
|
281
|
+
except KeyError as exc:
|
|
282
|
+
raise ConfigurationError(
|
|
283
|
+
"min_repeats_by_unit must define every accepted unit length.",
|
|
284
|
+
context={"missing_unit_length": unit_length},
|
|
285
|
+
) from exc
|
|
286
|
+
validate_positive_int(threshold, f"min_repeats_by_unit[{unit_length}]")
|
|
287
|
+
if threshold < 2:
|
|
288
|
+
raise ConfigurationError("Every tandem-repeat threshold must be at least 2.")
|
|
289
|
+
resolved[unit_length] = threshold
|
|
290
|
+
return resolved
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def find_tandem_repeats(
|
|
294
|
+
value: SequenceInput,
|
|
295
|
+
*,
|
|
296
|
+
min_unit_length: int = 1,
|
|
297
|
+
max_unit_length: int = 20,
|
|
298
|
+
min_repeats: int = 2,
|
|
299
|
+
min_repeats_by_unit: Mapping[int, int] | None = None,
|
|
300
|
+
overlapping: bool = False,
|
|
301
|
+
max_comparisons: int = 5_000_000,
|
|
302
|
+
max_comparison_cells: int = 50_000_000,
|
|
303
|
+
max_matches: int = 100_000,
|
|
304
|
+
) -> PatternResult[TandemRepeatHit]:
|
|
305
|
+
"""Find maximal exact repeats using the smallest accepted period at each start."""
|
|
306
|
+
|
|
307
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
308
|
+
require_linear(sequence, "tandem-repeat detection")
|
|
309
|
+
validate_positive_int(min_unit_length, "min_unit_length")
|
|
310
|
+
validate_positive_int(max_unit_length, "max_unit_length")
|
|
311
|
+
validate_positive_int(min_repeats, "min_repeats")
|
|
312
|
+
validate_positive_int(max_comparisons, "max_comparisons")
|
|
313
|
+
validate_positive_int(max_comparison_cells, "max_comparison_cells")
|
|
314
|
+
validate_positive_int(max_matches, "max_matches")
|
|
315
|
+
validate_bool(overlapping, "overlapping")
|
|
316
|
+
if min_repeats < 2:
|
|
317
|
+
raise ConfigurationError("min_repeats must be at least 2.")
|
|
318
|
+
if min_unit_length > max_unit_length:
|
|
319
|
+
raise ConfigurationError("min_unit_length cannot exceed max_unit_length.")
|
|
320
|
+
if max_unit_length > _MAX_TANDEM_UNIT_LENGTH:
|
|
321
|
+
raise ConfigurationError(
|
|
322
|
+
"max_unit_length exceeds the hard tandem-repeat unit limit.",
|
|
323
|
+
code="TANDEM_REPEAT_UNIT_LIMIT",
|
|
324
|
+
context={"maximum": _MAX_TANDEM_UNIT_LENGTH},
|
|
325
|
+
)
|
|
326
|
+
longest_fragment = max((len(item.text) for item in segments(sequence)), default=0)
|
|
327
|
+
useful_maximum = longest_fragment // min_repeats
|
|
328
|
+
if min_unit_length > useful_maximum:
|
|
329
|
+
effective_max_unit_length = min_unit_length - 1
|
|
330
|
+
else:
|
|
331
|
+
effective_max_unit_length = min(max_unit_length, useful_maximum)
|
|
332
|
+
thresholds = _repeat_thresholds(
|
|
333
|
+
min_unit_length, max_unit_length, min_repeats, min_repeats_by_unit
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
hits: list[TandemRepeatHit] = []
|
|
337
|
+
comparisons = 0
|
|
338
|
+
comparison_cells = 0
|
|
339
|
+
truncated = False
|
|
340
|
+
for item in segments(sequence):
|
|
341
|
+
start = 0
|
|
342
|
+
while start < len(item.text):
|
|
343
|
+
found: tuple[str, int, int] | None = None
|
|
344
|
+
for unit_length in range(min_unit_length, effective_max_unit_length + 1):
|
|
345
|
+
unit_end = start + unit_length
|
|
346
|
+
minimum_end = start + unit_length * thresholds[unit_length]
|
|
347
|
+
if minimum_end > len(item.text):
|
|
348
|
+
continue
|
|
349
|
+
unit = item.text[start:unit_end]
|
|
350
|
+
repeat_count = 1
|
|
351
|
+
cursor = unit_end
|
|
352
|
+
while cursor + unit_length <= len(item.text):
|
|
353
|
+
comparisons += 1
|
|
354
|
+
comparison_cells += unit_length
|
|
355
|
+
if comparisons > max_comparisons:
|
|
356
|
+
raise ConfigurationError("Tandem-repeat scan exceeded max_comparisons.")
|
|
357
|
+
if comparison_cells > max_comparison_cells:
|
|
358
|
+
raise ConfigurationError(
|
|
359
|
+
"Tandem-repeat scan exceeded max_comparison_cells."
|
|
360
|
+
)
|
|
361
|
+
if item.text[cursor : cursor + unit_length] != unit:
|
|
362
|
+
break
|
|
363
|
+
repeat_count += 1
|
|
364
|
+
cursor += unit_length
|
|
365
|
+
if repeat_count >= thresholds[unit_length]:
|
|
366
|
+
found = unit, repeat_count, cursor
|
|
367
|
+
break
|
|
368
|
+
if found is None:
|
|
369
|
+
start += 1
|
|
370
|
+
continue
|
|
371
|
+
unit, repeat_count, end = found
|
|
372
|
+
if len(hits) >= max_matches:
|
|
373
|
+
truncated = True
|
|
374
|
+
break
|
|
375
|
+
symbol_location, coordinate_location = segment_location(item, start, end)
|
|
376
|
+
hits.append(
|
|
377
|
+
TandemRepeatHit(
|
|
378
|
+
unit=unit,
|
|
379
|
+
unit_length=len(unit),
|
|
380
|
+
repeat_count=repeat_count,
|
|
381
|
+
sequence=item.text[start:end],
|
|
382
|
+
symbol_location=symbol_location,
|
|
383
|
+
coordinate_location=coordinate_location,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
start += 1 if overlapping else end - start
|
|
387
|
+
if truncated:
|
|
388
|
+
break
|
|
389
|
+
result = build_result(
|
|
390
|
+
sequence,
|
|
391
|
+
sequence_id,
|
|
392
|
+
name="tandem_repeat_scan",
|
|
393
|
+
method="smallest_period_exact_run",
|
|
394
|
+
algorithm_version="1.0",
|
|
395
|
+
parameters={
|
|
396
|
+
"min_unit_length": min_unit_length,
|
|
397
|
+
"max_unit_length": max_unit_length,
|
|
398
|
+
"effective_max_unit_length": effective_max_unit_length,
|
|
399
|
+
"min_repeats": min_repeats,
|
|
400
|
+
"min_repeats_by_unit": {
|
|
401
|
+
str(unit_length): threshold for unit_length, threshold in sorted(thresholds.items())
|
|
402
|
+
},
|
|
403
|
+
"overlapping": overlapping,
|
|
404
|
+
"mismatches": 0,
|
|
405
|
+
"indels": 0,
|
|
406
|
+
"ambiguity_rule": "literal-IUPAC-symbol-equality",
|
|
407
|
+
"period_policy": "smallest-accepted-at-each-start",
|
|
408
|
+
"comparisons": comparisons,
|
|
409
|
+
"max_comparisons": max_comparisons,
|
|
410
|
+
"comparison_cells": comparison_cells,
|
|
411
|
+
"max_comparison_cells": max_comparison_cells,
|
|
412
|
+
},
|
|
413
|
+
hits=hits,
|
|
414
|
+
max_matches=max_matches,
|
|
415
|
+
truncated=truncated,
|
|
416
|
+
provenance=pattern_provenance(reimplementation=False),
|
|
417
|
+
)
|
|
418
|
+
return cast(PatternResult[TandemRepeatHit], result)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def find_microsatellites(
|
|
422
|
+
value: SequenceInput,
|
|
423
|
+
*,
|
|
424
|
+
min_repeats_by_unit: Mapping[int, int] | None = None,
|
|
425
|
+
overlapping: bool = False,
|
|
426
|
+
max_comparisons: int = 5_000_000,
|
|
427
|
+
max_comparison_cells: int = 50_000_000,
|
|
428
|
+
max_matches: int = 100_000,
|
|
429
|
+
) -> PatternResult[TandemRepeatHit]:
|
|
430
|
+
"""Find exact 1--6 bp STRs with explicit per-unit repeat thresholds."""
|
|
431
|
+
|
|
432
|
+
thresholds = (
|
|
433
|
+
{1: 6, 2: 3, 3: 3, 4: 3, 5: 3, 6: 3}
|
|
434
|
+
if min_repeats_by_unit is None
|
|
435
|
+
else dict(min_repeats_by_unit)
|
|
436
|
+
)
|
|
437
|
+
result = find_tandem_repeats(
|
|
438
|
+
value,
|
|
439
|
+
min_unit_length=1,
|
|
440
|
+
max_unit_length=6,
|
|
441
|
+
min_repeats=2,
|
|
442
|
+
min_repeats_by_unit=thresholds,
|
|
443
|
+
overlapping=overlapping,
|
|
444
|
+
max_comparisons=max_comparisons,
|
|
445
|
+
max_comparison_cells=max_comparison_cells,
|
|
446
|
+
max_matches=max_matches,
|
|
447
|
+
)
|
|
448
|
+
parameters = dict(result.parameters)
|
|
449
|
+
parameters.update(
|
|
450
|
+
{
|
|
451
|
+
"definition": "exact-short-tandem-repeat-1-to-6-bp",
|
|
452
|
+
"interruptions_allowed": False,
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
return PatternResult(
|
|
456
|
+
name="microsatellite_scan",
|
|
457
|
+
method=result.method,
|
|
458
|
+
algorithm_version=result.algorithm_version,
|
|
459
|
+
sequence_id=result.sequence_id,
|
|
460
|
+
parameters=type(result.parameters)(parameters),
|
|
461
|
+
hits=result.hits,
|
|
462
|
+
inspected_symbol_count=result.inspected_symbol_count,
|
|
463
|
+
gap_count=result.gap_count,
|
|
464
|
+
unknown_gap_count=result.unknown_gap_count,
|
|
465
|
+
max_matches=result.max_matches,
|
|
466
|
+
truncated=result.truncated,
|
|
467
|
+
coordinate_system=result.coordinate_system,
|
|
468
|
+
gap_policy=result.gap_policy,
|
|
469
|
+
topology=result.topology,
|
|
470
|
+
provenance=result.provenance,
|
|
471
|
+
issues=result.issues,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
__all__ = [
|
|
476
|
+
"find_inverted_repeats",
|
|
477
|
+
"find_microsatellites",
|
|
478
|
+
"find_reverse_complement_palindromes",
|
|
479
|
+
"find_tandem_repeats",
|
|
480
|
+
]
|