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,341 @@
|
|
|
1
|
+
"""CpG-island and Shannon low-complexity region detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from dnakit.core import DNASequence, Interval
|
|
11
|
+
from dnakit.core.gap import Gap
|
|
12
|
+
from dnakit.exceptions import ConfigurationError
|
|
13
|
+
from dnakit.patterns._shared import (
|
|
14
|
+
SequenceInput,
|
|
15
|
+
build_result,
|
|
16
|
+
frozen,
|
|
17
|
+
pattern_provenance,
|
|
18
|
+
require_linear,
|
|
19
|
+
resolve_sequence,
|
|
20
|
+
segment_location,
|
|
21
|
+
segments,
|
|
22
|
+
validate_positive_int,
|
|
23
|
+
)
|
|
24
|
+
from dnakit.patterns.results import LowComplexityResult, PatternResult, RegionHit
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class _CandidateRegion:
|
|
29
|
+
start: int
|
|
30
|
+
end: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _validate_fraction(value: float, name: str) -> float:
|
|
34
|
+
if (
|
|
35
|
+
isinstance(value, bool)
|
|
36
|
+
or not isinstance(value, (int, float))
|
|
37
|
+
or not math.isfinite(value)
|
|
38
|
+
or value < 0
|
|
39
|
+
or value > 1
|
|
40
|
+
):
|
|
41
|
+
raise ConfigurationError(f"{name} must be finite and between 0 and 1.")
|
|
42
|
+
return float(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _merge_window(regions: list[_CandidateRegion], start: int, end: int) -> None:
|
|
46
|
+
if regions and start <= regions[-1].end:
|
|
47
|
+
previous = regions[-1]
|
|
48
|
+
regions[-1] = _CandidateRegion(previous.start, max(previous.end, end))
|
|
49
|
+
else:
|
|
50
|
+
regions.append(_CandidateRegion(start, end))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cpg_metrics(text: str) -> tuple[float, float, int, int, int]:
|
|
54
|
+
c_count = text.count("C")
|
|
55
|
+
g_count = text.count("G")
|
|
56
|
+
cpg_count = sum(text[index : index + 2] == "CG" for index in range(len(text) - 1))
|
|
57
|
+
gc_fraction = (c_count + g_count) / len(text) if text else 0.0
|
|
58
|
+
observed_expected = (
|
|
59
|
+
(cpg_count * len(text)) / (c_count * g_count) if c_count and g_count else 0.0
|
|
60
|
+
)
|
|
61
|
+
return gc_fraction, observed_expected, cpg_count, c_count, g_count
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_cpg_islands(
|
|
65
|
+
value: SequenceInput,
|
|
66
|
+
*,
|
|
67
|
+
window_size: int = 200,
|
|
68
|
+
step: int = 1,
|
|
69
|
+
min_gc: float = 0.5,
|
|
70
|
+
min_observed_expected: float = 0.6,
|
|
71
|
+
min_region_length: int = 200,
|
|
72
|
+
max_windows: int = 2_000_000,
|
|
73
|
+
max_matches: int = 100_000,
|
|
74
|
+
) -> PatternResult[RegionHit]:
|
|
75
|
+
"""Find merged qualifying windows using a configurable classic CpG rule.
|
|
76
|
+
|
|
77
|
+
A window qualifies when it is unambiguous, has ``GC >= min_gc``, and has
|
|
78
|
+
``observed CpG / expected CpG >= min_observed_expected`` where
|
|
79
|
+
``O/E = CpG * L / (C * G)``. Overlapping qualifying windows are merged.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
83
|
+
require_linear(sequence, "CpG-island detection")
|
|
84
|
+
validate_positive_int(window_size, "window_size")
|
|
85
|
+
validate_positive_int(step, "step")
|
|
86
|
+
validate_positive_int(min_region_length, "min_region_length")
|
|
87
|
+
validate_positive_int(max_windows, "max_windows")
|
|
88
|
+
validate_positive_int(max_matches, "max_matches")
|
|
89
|
+
resolved_gc = _validate_fraction(min_gc, "min_gc")
|
|
90
|
+
if (
|
|
91
|
+
isinstance(min_observed_expected, bool)
|
|
92
|
+
or not isinstance(min_observed_expected, (int, float))
|
|
93
|
+
or not math.isfinite(min_observed_expected)
|
|
94
|
+
or min_observed_expected < 0
|
|
95
|
+
):
|
|
96
|
+
raise ConfigurationError("min_observed_expected must be finite and non-negative.")
|
|
97
|
+
|
|
98
|
+
hits: list[RegionHit] = []
|
|
99
|
+
windows_checked = 0
|
|
100
|
+
truncated = False
|
|
101
|
+
for item in segments(sequence):
|
|
102
|
+
candidates: list[_CandidateRegion] = []
|
|
103
|
+
for start in range(0, len(item.text) - window_size + 1, step):
|
|
104
|
+
windows_checked += 1
|
|
105
|
+
if windows_checked > max_windows:
|
|
106
|
+
raise ConfigurationError("CpG-island scan exceeded max_windows.")
|
|
107
|
+
window = item.text[start : start + window_size]
|
|
108
|
+
if set(window) - set("ACGT"):
|
|
109
|
+
continue
|
|
110
|
+
gc_fraction, observed_expected, _, _, _ = _cpg_metrics(window)
|
|
111
|
+
if gc_fraction >= resolved_gc and observed_expected >= min_observed_expected:
|
|
112
|
+
_merge_window(candidates, start, start + window_size)
|
|
113
|
+
for candidate in candidates:
|
|
114
|
+
length = candidate.end - candidate.start
|
|
115
|
+
if length < min_region_length:
|
|
116
|
+
continue
|
|
117
|
+
if len(hits) >= max_matches:
|
|
118
|
+
truncated = True
|
|
119
|
+
break
|
|
120
|
+
region = item.text[candidate.start : candidate.end]
|
|
121
|
+
gc_fraction, observed_expected, cpg_count, c_count, g_count = _cpg_metrics(region)
|
|
122
|
+
symbol_location, coordinate_location = segment_location(
|
|
123
|
+
item, candidate.start, candidate.end
|
|
124
|
+
)
|
|
125
|
+
hits.append(
|
|
126
|
+
RegionHit(
|
|
127
|
+
kind="cpg_island_candidate",
|
|
128
|
+
symbol_location=symbol_location,
|
|
129
|
+
coordinate_location=coordinate_location,
|
|
130
|
+
length=length,
|
|
131
|
+
score=observed_expected,
|
|
132
|
+
attributes=frozen(
|
|
133
|
+
{
|
|
134
|
+
"gc_fraction": gc_fraction,
|
|
135
|
+
"observed_expected": observed_expected,
|
|
136
|
+
"cpg_count": cpg_count,
|
|
137
|
+
"c_count": c_count,
|
|
138
|
+
"g_count": g_count,
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
if truncated:
|
|
144
|
+
break
|
|
145
|
+
result = build_result(
|
|
146
|
+
sequence,
|
|
147
|
+
sequence_id,
|
|
148
|
+
name="cpg_island_scan",
|
|
149
|
+
method="merged_qualifying_windows",
|
|
150
|
+
algorithm_version="1.0",
|
|
151
|
+
parameters={
|
|
152
|
+
"window_size": window_size,
|
|
153
|
+
"step": step,
|
|
154
|
+
"min_gc": resolved_gc,
|
|
155
|
+
"min_observed_expected": float(min_observed_expected),
|
|
156
|
+
"min_region_length": min_region_length,
|
|
157
|
+
"observed_expected_formula": "CpG * L / (C * G)",
|
|
158
|
+
"threshold_comparison": "greater-than-or-equal",
|
|
159
|
+
"ambiguous_window_policy": "skip",
|
|
160
|
+
"merge_policy": "overlapping-qualifying-windows",
|
|
161
|
+
"windows_checked": windows_checked,
|
|
162
|
+
"max_windows": max_windows,
|
|
163
|
+
},
|
|
164
|
+
hits=hits,
|
|
165
|
+
max_matches=max_matches,
|
|
166
|
+
truncated=truncated,
|
|
167
|
+
provenance=pattern_provenance(
|
|
168
|
+
reimplementation=True,
|
|
169
|
+
reference_name="Gardiner-Garden and Frommer configurable CpG boundary",
|
|
170
|
+
reference_version="DNAKit-explicit-thresholds-v1",
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
return cast(PatternResult[RegionHit], result)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _shannon_entropy(text: str) -> float:
|
|
177
|
+
counts = Counter(text)
|
|
178
|
+
total = len(text)
|
|
179
|
+
return -math.fsum(
|
|
180
|
+
(count / total) * math.log2(count / total) for count in counts.values() if count
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _mask_sequence(
|
|
185
|
+
sequence: DNASequence, intervals: tuple[Interval, ...], symbol: str
|
|
186
|
+
) -> DNASequence:
|
|
187
|
+
parts: list[str | Gap] = []
|
|
188
|
+
symbol_offset = 0
|
|
189
|
+
for part in sequence.parts:
|
|
190
|
+
if isinstance(part, Gap):
|
|
191
|
+
parts.append(part)
|
|
192
|
+
continue
|
|
193
|
+
masked = list(part)
|
|
194
|
+
part_end = symbol_offset + len(part)
|
|
195
|
+
for interval in intervals:
|
|
196
|
+
overlap_start = max(interval.start, symbol_offset)
|
|
197
|
+
overlap_end = min(interval.end, part_end)
|
|
198
|
+
for position in range(overlap_start, overlap_end):
|
|
199
|
+
masked[position - symbol_offset] = symbol
|
|
200
|
+
parts.append("".join(masked))
|
|
201
|
+
symbol_offset = part_end
|
|
202
|
+
output_alphabet = sequence.alphabet if symbol in "ACGT" else "iupac"
|
|
203
|
+
return DNASequence(
|
|
204
|
+
parts,
|
|
205
|
+
alphabet=output_alphabet,
|
|
206
|
+
topology=sequence.topology,
|
|
207
|
+
strandedness=sequence.strandedness,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def find_low_complexity_regions(
|
|
212
|
+
value: SequenceInput,
|
|
213
|
+
*,
|
|
214
|
+
window_size: int = 32,
|
|
215
|
+
step: int = 1,
|
|
216
|
+
max_entropy: float = 1.2,
|
|
217
|
+
min_canonical_fraction: float = 1.0,
|
|
218
|
+
min_region_length: int = 32,
|
|
219
|
+
mask_symbol: str = "N",
|
|
220
|
+
max_windows: int = 2_000_000,
|
|
221
|
+
max_matches: int = 100_000,
|
|
222
|
+
) -> LowComplexityResult:
|
|
223
|
+
"""Mark merged low-Shannon-entropy windows and return a masked sequence.
|
|
224
|
+
|
|
225
|
+
This implementation is explicitly an entropy-window method, not DUST or
|
|
226
|
+
RepeatMasker. Entropy is calculated in bits over canonical bases after the
|
|
227
|
+
canonical-fraction eligibility check.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
231
|
+
require_linear(sequence, "low-complexity detection")
|
|
232
|
+
validate_positive_int(window_size, "window_size")
|
|
233
|
+
validate_positive_int(step, "step")
|
|
234
|
+
validate_positive_int(min_region_length, "min_region_length")
|
|
235
|
+
validate_positive_int(max_windows, "max_windows")
|
|
236
|
+
validate_positive_int(max_matches, "max_matches")
|
|
237
|
+
if (
|
|
238
|
+
isinstance(max_entropy, bool)
|
|
239
|
+
or not isinstance(max_entropy, (int, float))
|
|
240
|
+
or not math.isfinite(max_entropy)
|
|
241
|
+
or max_entropy < 0
|
|
242
|
+
or max_entropy > 2
|
|
243
|
+
):
|
|
244
|
+
raise ConfigurationError("max_entropy must be finite and between 0 and 2 bits.")
|
|
245
|
+
resolved_canonical_fraction = _validate_fraction(
|
|
246
|
+
min_canonical_fraction, "min_canonical_fraction"
|
|
247
|
+
)
|
|
248
|
+
if (
|
|
249
|
+
not isinstance(mask_symbol, str)
|
|
250
|
+
or len(mask_symbol) != 1
|
|
251
|
+
or mask_symbol not in "ACGTRYSWKMBDHVN"
|
|
252
|
+
):
|
|
253
|
+
raise ConfigurationError("mask_symbol must be one uppercase DNA IUPAC symbol.")
|
|
254
|
+
|
|
255
|
+
hits: list[RegionHit] = []
|
|
256
|
+
windows_checked = 0
|
|
257
|
+
truncated = False
|
|
258
|
+
for item in segments(sequence):
|
|
259
|
+
candidates: list[_CandidateRegion] = []
|
|
260
|
+
for start in range(0, len(item.text) - window_size + 1, step):
|
|
261
|
+
windows_checked += 1
|
|
262
|
+
if windows_checked > max_windows:
|
|
263
|
+
raise ConfigurationError("Low-complexity scan exceeded max_windows.")
|
|
264
|
+
window = item.text[start : start + window_size]
|
|
265
|
+
canonical = "".join(base for base in window if base in "ACGT")
|
|
266
|
+
canonical_fraction = len(canonical) / len(window)
|
|
267
|
+
if not canonical or canonical_fraction < resolved_canonical_fraction:
|
|
268
|
+
continue
|
|
269
|
+
entropy = _shannon_entropy(canonical)
|
|
270
|
+
if entropy <= max_entropy:
|
|
271
|
+
_merge_window(candidates, start, start + window_size)
|
|
272
|
+
for candidate in candidates:
|
|
273
|
+
length = candidate.end - candidate.start
|
|
274
|
+
if length < min_region_length:
|
|
275
|
+
continue
|
|
276
|
+
if len(hits) >= max_matches:
|
|
277
|
+
truncated = True
|
|
278
|
+
break
|
|
279
|
+
region = item.text[candidate.start : candidate.end]
|
|
280
|
+
canonical = "".join(base for base in region if base in "ACGT")
|
|
281
|
+
entropy = _shannon_entropy(canonical)
|
|
282
|
+
symbol_location, coordinate_location = segment_location(
|
|
283
|
+
item, candidate.start, candidate.end
|
|
284
|
+
)
|
|
285
|
+
hits.append(
|
|
286
|
+
RegionHit(
|
|
287
|
+
kind="low_complexity",
|
|
288
|
+
symbol_location=symbol_location,
|
|
289
|
+
coordinate_location=coordinate_location,
|
|
290
|
+
length=length,
|
|
291
|
+
score=entropy,
|
|
292
|
+
attributes=frozen(
|
|
293
|
+
{
|
|
294
|
+
"entropy_bits": entropy,
|
|
295
|
+
"canonical_fraction": len(canonical) / len(region),
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
if truncated:
|
|
301
|
+
break
|
|
302
|
+
analysis = build_result(
|
|
303
|
+
sequence,
|
|
304
|
+
sequence_id,
|
|
305
|
+
name="low_complexity_scan",
|
|
306
|
+
method="shannon_entropy_windows",
|
|
307
|
+
algorithm_version="1.0",
|
|
308
|
+
parameters={
|
|
309
|
+
"window_size": window_size,
|
|
310
|
+
"step": step,
|
|
311
|
+
"max_entropy": float(max_entropy),
|
|
312
|
+
"entropy_unit": "bits",
|
|
313
|
+
"min_canonical_fraction": resolved_canonical_fraction,
|
|
314
|
+
"min_region_length": min_region_length,
|
|
315
|
+
"mask_symbol": mask_symbol,
|
|
316
|
+
"merge_policy": "overlapping-qualifying-windows",
|
|
317
|
+
"windows_checked": windows_checked,
|
|
318
|
+
"max_windows": max_windows,
|
|
319
|
+
"is_dust": False,
|
|
320
|
+
"is_repeatmasker": False,
|
|
321
|
+
},
|
|
322
|
+
hits=hits,
|
|
323
|
+
max_matches=max_matches,
|
|
324
|
+
truncated=truncated,
|
|
325
|
+
provenance=pattern_provenance(
|
|
326
|
+
reimplementation=True,
|
|
327
|
+
reference_name="Shannon entropy low-complexity boundary",
|
|
328
|
+
reference_version="DNAKit-window-v1",
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
intervals = tuple(
|
|
332
|
+
hit.symbol_location for hit in hits if isinstance(hit.symbol_location, Interval)
|
|
333
|
+
)
|
|
334
|
+
return LowComplexityResult(
|
|
335
|
+
analysis=cast(PatternResult[RegionHit], analysis),
|
|
336
|
+
masked_sequence=_mask_sequence(sequence, intervals, mask_symbol),
|
|
337
|
+
mask_symbol=mask_symbol,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
__all__ = ["find_cpg_islands", "find_low_complexity_regions"]
|