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,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"]