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
dnakit/patterns/motif.py
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""Bounded exact, IUPAC, regular-expression, PWM, promoter, and TF motif scans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from itertools import islice
|
|
10
|
+
from typing import Literal, cast
|
|
11
|
+
|
|
12
|
+
from dnakit.core import Location, Strand
|
|
13
|
+
from dnakit.core._json import to_json_compatible
|
|
14
|
+
from dnakit.exceptions import ConfigurationError, SequenceError
|
|
15
|
+
from dnakit.patterns._shared import (
|
|
16
|
+
SequenceInput,
|
|
17
|
+
build_result,
|
|
18
|
+
circular_location,
|
|
19
|
+
coerce_strands,
|
|
20
|
+
iupac_compatible,
|
|
21
|
+
pattern_provenance,
|
|
22
|
+
require_ungapped_circular,
|
|
23
|
+
resolve_sequence,
|
|
24
|
+
reverse_complement_text,
|
|
25
|
+
segment_location,
|
|
26
|
+
segments,
|
|
27
|
+
validate_bool,
|
|
28
|
+
validate_iupac_text,
|
|
29
|
+
validate_positive_int,
|
|
30
|
+
)
|
|
31
|
+
from dnakit.patterns.results import MotifHit, PatternResult, validate_finite_score
|
|
32
|
+
|
|
33
|
+
MotifMode = Literal["exact", "iupac", "regex"]
|
|
34
|
+
|
|
35
|
+
PROMOTER_MOTIFS: Mapping[str, str] = {
|
|
36
|
+
"eukaryotic_TATA_box_consensus": "TATAWAWR",
|
|
37
|
+
"bacterial_minus_10_consensus": "TATAAT",
|
|
38
|
+
"bacterial_minus_35_consensus": "TTGACA",
|
|
39
|
+
}
|
|
40
|
+
PROMOTER_CATALOG_VERSION = "builtin-boundary-v1"
|
|
41
|
+
MAX_PWM_LENGTH = 100_000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, init=False)
|
|
45
|
+
class PWM:
|
|
46
|
+
"""A position-weight input matrix normalized independently by column."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
a: tuple[float, ...]
|
|
50
|
+
c: tuple[float, ...]
|
|
51
|
+
g: tuple[float, ...]
|
|
52
|
+
t: tuple[float, ...]
|
|
53
|
+
|
|
54
|
+
def __init__(self, name: str, matrix: Mapping[str, Iterable[float]]) -> None:
|
|
55
|
+
if not isinstance(name, str) or not name.strip():
|
|
56
|
+
raise ConfigurationError("PWM name must be a non-empty string.")
|
|
57
|
+
if not isinstance(matrix, Mapping) or set(matrix) != set("ACGT"):
|
|
58
|
+
raise ConfigurationError("PWM matrix must contain exactly A, C, G, and T rows.")
|
|
59
|
+
rows: dict[str, tuple[float, ...]] = {}
|
|
60
|
+
for base in "ACGT":
|
|
61
|
+
raw = matrix[base]
|
|
62
|
+
if isinstance(raw, (str, bytes)):
|
|
63
|
+
raise ConfigurationError("PWM rows must be numerical sequences.")
|
|
64
|
+
try:
|
|
65
|
+
raw_values = tuple(islice(iter(raw), MAX_PWM_LENGTH + 1))
|
|
66
|
+
except TypeError as exc:
|
|
67
|
+
raise ConfigurationError("PWM rows must be numerical sequences.") from exc
|
|
68
|
+
if len(raw_values) > MAX_PWM_LENGTH:
|
|
69
|
+
raise ConfigurationError(
|
|
70
|
+
"PWM row exceeds the supported length limit.",
|
|
71
|
+
code="PWM_LENGTH_LIMIT",
|
|
72
|
+
context={"max_pwm_length": MAX_PWM_LENGTH},
|
|
73
|
+
)
|
|
74
|
+
if any(
|
|
75
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
76
|
+
for value in raw_values
|
|
77
|
+
):
|
|
78
|
+
raise ConfigurationError("PWM rows must contain real numbers, not booleans.")
|
|
79
|
+
try:
|
|
80
|
+
values = tuple(float(value) for value in raw_values)
|
|
81
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
82
|
+
raise ConfigurationError("PWM rows must contain numbers.") from exc
|
|
83
|
+
if not values or any(not math.isfinite(value) or value < 0 for value in values):
|
|
84
|
+
raise ConfigurationError("PWM values must be finite non-negative numbers.")
|
|
85
|
+
rows[base] = values
|
|
86
|
+
lengths = {len(row) for row in rows.values()}
|
|
87
|
+
if len(lengths) != 1:
|
|
88
|
+
raise ConfigurationError("All PWM rows must have the same non-zero length.")
|
|
89
|
+
for column in range(len(rows["A"])):
|
|
90
|
+
if math.fsum(rows[base][column] for base in "ACGT") <= 0:
|
|
91
|
+
raise ConfigurationError("Every PWM column must have positive total weight.")
|
|
92
|
+
object.__setattr__(self, "name", name)
|
|
93
|
+
object.__setattr__(self, "a", rows["A"])
|
|
94
|
+
object.__setattr__(self, "c", rows["C"])
|
|
95
|
+
object.__setattr__(self, "g", rows["G"])
|
|
96
|
+
object.__setattr__(self, "t", rows["T"])
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def length(self) -> int:
|
|
100
|
+
return len(self.a)
|
|
101
|
+
|
|
102
|
+
def row(self, base: str) -> tuple[float, ...]:
|
|
103
|
+
if base == "A":
|
|
104
|
+
return self.a
|
|
105
|
+
if base == "C":
|
|
106
|
+
return self.c
|
|
107
|
+
if base == "G":
|
|
108
|
+
return self.g
|
|
109
|
+
if base == "T":
|
|
110
|
+
return self.t
|
|
111
|
+
raise ConfigurationError("PWM scores are defined only for canonical DNA bases.")
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict[str, object]:
|
|
114
|
+
return cast(dict[str, object], to_json_compatible(self))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fixed_matches(
|
|
118
|
+
text: str,
|
|
119
|
+
motif: str,
|
|
120
|
+
*,
|
|
121
|
+
mode: Literal["exact", "iupac"],
|
|
122
|
+
overlapping: bool,
|
|
123
|
+
circular: bool,
|
|
124
|
+
) -> Iterator[tuple[int, int, str]]:
|
|
125
|
+
length = len(motif)
|
|
126
|
+
if circular and length > len(text):
|
|
127
|
+
raise ConfigurationError("A circular motif cannot be longer than the sequence.")
|
|
128
|
+
searchable = text + text[: length - 1] if circular and length > 1 else text
|
|
129
|
+
step = 1 if overlapping else length
|
|
130
|
+
start = 0
|
|
131
|
+
limit = len(text) if circular else len(text) - length + 1
|
|
132
|
+
while start < max(0, limit):
|
|
133
|
+
candidate = searchable[start : start + length]
|
|
134
|
+
matched = candidate == motif
|
|
135
|
+
if mode == "iupac":
|
|
136
|
+
matched = len(candidate) == length and all(
|
|
137
|
+
iupac_compatible(left, right) for left, right in zip(candidate, motif, strict=True)
|
|
138
|
+
)
|
|
139
|
+
if matched:
|
|
140
|
+
yield start, start + length, candidate
|
|
141
|
+
start += step
|
|
142
|
+
else:
|
|
143
|
+
start += 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _regex_matches(
|
|
147
|
+
text: str,
|
|
148
|
+
pattern: str,
|
|
149
|
+
*,
|
|
150
|
+
overlapping: bool,
|
|
151
|
+
) -> Iterator[tuple[int, int, str]]:
|
|
152
|
+
try:
|
|
153
|
+
compiled = re.compile(pattern)
|
|
154
|
+
except (re.error, OverflowError) as exc:
|
|
155
|
+
raise ConfigurationError(
|
|
156
|
+
"Invalid regular-expression motif.", context={"regex_error": str(exc)}
|
|
157
|
+
) from exc
|
|
158
|
+
position = 0
|
|
159
|
+
while position <= len(text):
|
|
160
|
+
match = compiled.search(text, position)
|
|
161
|
+
if match is None:
|
|
162
|
+
return
|
|
163
|
+
if match.end() == match.start():
|
|
164
|
+
raise ConfigurationError("Regular-expression motifs must not produce empty matches.")
|
|
165
|
+
yield match.start(), match.end(), match.group(0)
|
|
166
|
+
position = match.start() + 1 if overlapping else match.end()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _validate_safe_regex(pattern: str, scan_length: int) -> int:
|
|
170
|
+
"""Reject constructs with unbounded backtracking or non-DNA semantics."""
|
|
171
|
+
|
|
172
|
+
allowed = frozenset("ACGTRYSWKMBDHVNacgtryswkmbdhvn.[]^-?*+{},0123456789$")
|
|
173
|
+
invalid = sorted(set(pattern) - allowed)
|
|
174
|
+
if invalid or any(token in pattern for token in ("(", ")", "|", "\\")):
|
|
175
|
+
raise ConfigurationError(
|
|
176
|
+
"Regex motif is outside DNAKit's safe DNA-regex subset.",
|
|
177
|
+
context={"invalid_characters": invalid},
|
|
178
|
+
hint="Use literals, dot, character classes, anchors, and simple quantifiers only.",
|
|
179
|
+
)
|
|
180
|
+
if re.search(r"(?:[*+?]|\{\d+(?:,\d*)?\})\s*(?:[*+?]|\{)", pattern):
|
|
181
|
+
raise ConfigurationError("Nested or adjacent regex quantifiers are not allowed.")
|
|
182
|
+
quantifier_text = re.sub(r"\[[^\]]*\]", "X", pattern)
|
|
183
|
+
if "{," in quantifier_text:
|
|
184
|
+
raise ConfigurationError("Regex repetition bounds must include an explicit lower bound.")
|
|
185
|
+
unbounded = re.findall(r"[*+]|\{\d+,\}", quantifier_text)
|
|
186
|
+
if len(unbounded) > 1:
|
|
187
|
+
raise ConfigurationError(
|
|
188
|
+
"The safe DNA-regex subset allows at most one unbounded quantifier."
|
|
189
|
+
)
|
|
190
|
+
quantifiers = re.findall(r"[*+?]|\{\d+(?:,\d*)?\}", quantifier_text)
|
|
191
|
+
quantifier_count = len(quantifiers)
|
|
192
|
+
if quantifier_count > 32:
|
|
193
|
+
raise ConfigurationError("Regex motif contains too many quantifiers for bounded scanning.")
|
|
194
|
+
multiplier = 1
|
|
195
|
+
for quantifier in quantifiers:
|
|
196
|
+
if quantifier in ("*", "+") or quantifier.endswith(",}"):
|
|
197
|
+
factor = max(1, scan_length)
|
|
198
|
+
elif quantifier == "?":
|
|
199
|
+
factor = 2
|
|
200
|
+
else:
|
|
201
|
+
bounds = quantifier[1:-1].split(",")
|
|
202
|
+
lower = int(bounds[0])
|
|
203
|
+
upper = lower if len(bounds) == 1 else int(bounds[1])
|
|
204
|
+
if upper < lower:
|
|
205
|
+
raise ConfigurationError(
|
|
206
|
+
"Regex repetition upper bound cannot be below its lower bound."
|
|
207
|
+
)
|
|
208
|
+
factor = max(1, upper, upper - lower + 1)
|
|
209
|
+
multiplier *= factor
|
|
210
|
+
if multiplier > 50_000_000:
|
|
211
|
+
raise ConfigurationError("Regex quantifier expansion exceeds the safety limit.")
|
|
212
|
+
return multiplier
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _mapped_location(
|
|
216
|
+
*,
|
|
217
|
+
local_start: int,
|
|
218
|
+
local_end: int,
|
|
219
|
+
segment_text_length: int,
|
|
220
|
+
segment: object,
|
|
221
|
+
strand: Strand,
|
|
222
|
+
circular: bool,
|
|
223
|
+
) -> tuple[Location, Location, bool]:
|
|
224
|
+
from dnakit.patterns._shared import Segment
|
|
225
|
+
|
|
226
|
+
if not isinstance(segment, Segment):
|
|
227
|
+
raise AssertionError("Internal segment type mismatch.")
|
|
228
|
+
length = local_end - local_start
|
|
229
|
+
original_start = local_start if strand is Strand.FORWARD else segment_text_length - local_end
|
|
230
|
+
if circular:
|
|
231
|
+
return circular_location(original_start, length, segment_text_length)
|
|
232
|
+
symbol_location, coordinate_location = segment_location(
|
|
233
|
+
segment, original_start, original_start + length
|
|
234
|
+
)
|
|
235
|
+
return symbol_location, coordinate_location, False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def scan_motif(
|
|
239
|
+
value: SequenceInput,
|
|
240
|
+
motif: str,
|
|
241
|
+
*,
|
|
242
|
+
mode: MotifMode = "exact",
|
|
243
|
+
name: str = "motif",
|
|
244
|
+
strand: Strand | str = Strand.BOTH,
|
|
245
|
+
overlapping: bool = True,
|
|
246
|
+
merge_strands: bool = False,
|
|
247
|
+
max_matches: int = 100_000,
|
|
248
|
+
max_scan_length: int = 1_000_000,
|
|
249
|
+
max_pattern_length: int = 1_000,
|
|
250
|
+
max_scan_cells: int = 50_000_000,
|
|
251
|
+
) -> PatternResult[MotifHit]:
|
|
252
|
+
"""Scan literal, IUPAC-compatible, or Python-regex motifs on selected strands.
|
|
253
|
+
|
|
254
|
+
Regex scanning is bounded by ``max_scan_length`` and ``max_pattern_length``;
|
|
255
|
+
circular origin wrapping is supported only for fixed-length exact/IUPAC motifs.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
259
|
+
validate_positive_int(max_matches, "max_matches")
|
|
260
|
+
validate_positive_int(max_scan_length, "max_scan_length")
|
|
261
|
+
validate_positive_int(max_pattern_length, "max_pattern_length")
|
|
262
|
+
validate_positive_int(max_scan_cells, "max_scan_cells")
|
|
263
|
+
validate_bool(overlapping, "overlapping")
|
|
264
|
+
validate_bool(merge_strands, "merge_strands")
|
|
265
|
+
if sequence.symbol_length > max_scan_length:
|
|
266
|
+
raise ConfigurationError(
|
|
267
|
+
"Sequence exceeds max_scan_length.",
|
|
268
|
+
context={"symbol_length": sequence.symbol_length, "max_scan_length": max_scan_length},
|
|
269
|
+
)
|
|
270
|
+
if not isinstance(name, str) or not name.strip():
|
|
271
|
+
raise ConfigurationError("Motif name must be non-empty.")
|
|
272
|
+
raw_mode: object = mode
|
|
273
|
+
if not isinstance(raw_mode, str) or raw_mode not in ("exact", "iupac", "regex"):
|
|
274
|
+
raise ConfigurationError("Motif mode must be exact, iupac, or regex.")
|
|
275
|
+
resolved_mode = cast(MotifMode, raw_mode)
|
|
276
|
+
if not isinstance(motif, str) or not motif:
|
|
277
|
+
raise ConfigurationError("motif must be a non-empty string.")
|
|
278
|
+
if len(motif) > max_pattern_length:
|
|
279
|
+
raise ConfigurationError("Motif exceeds max_pattern_length.")
|
|
280
|
+
resolved_motif = motif if resolved_mode == "regex" else validate_iupac_text(motif, "motif")
|
|
281
|
+
regex_work_multiplier = (
|
|
282
|
+
_validate_safe_regex(resolved_motif, sequence.symbol_length)
|
|
283
|
+
if resolved_mode == "regex"
|
|
284
|
+
else 1
|
|
285
|
+
)
|
|
286
|
+
circular = sequence.topology.value == "circular"
|
|
287
|
+
require_ungapped_circular(sequence, "motif scanning")
|
|
288
|
+
if circular and resolved_mode == "regex":
|
|
289
|
+
raise SequenceError(
|
|
290
|
+
"Regex motifs do not have a fixed circular wrap length.",
|
|
291
|
+
code="CIRCULAR_REGEX_UNSUPPORTED",
|
|
292
|
+
hint="Use exact/IUPAC mode or linearize the sequence at a documented origin.",
|
|
293
|
+
)
|
|
294
|
+
selected_strands = coerce_strands(strand)
|
|
295
|
+
estimated_cells = (
|
|
296
|
+
sequence.symbol_length * len(resolved_motif) * len(selected_strands) * regex_work_multiplier
|
|
297
|
+
)
|
|
298
|
+
if estimated_cells > max_scan_cells:
|
|
299
|
+
raise ConfigurationError(
|
|
300
|
+
"Motif scan exceeds max_scan_cells.",
|
|
301
|
+
context={"estimated_cells": estimated_cells, "max_scan_cells": max_scan_cells},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
hits: list[MotifHit] = []
|
|
305
|
+
truncated = False
|
|
306
|
+
seen: dict[tuple[object, object, str], int] = {}
|
|
307
|
+
for item in segments(sequence):
|
|
308
|
+
for resolved_strand in selected_strands:
|
|
309
|
+
oriented = (
|
|
310
|
+
item.text
|
|
311
|
+
if resolved_strand is Strand.FORWARD
|
|
312
|
+
else reverse_complement_text(item.text)
|
|
313
|
+
)
|
|
314
|
+
iterator: Iterator[tuple[int, int, str]]
|
|
315
|
+
if resolved_mode == "regex":
|
|
316
|
+
iterator = _regex_matches(oriented, resolved_motif, overlapping=overlapping)
|
|
317
|
+
else:
|
|
318
|
+
iterator = _fixed_matches(
|
|
319
|
+
oriented,
|
|
320
|
+
resolved_motif,
|
|
321
|
+
mode=resolved_mode,
|
|
322
|
+
overlapping=overlapping,
|
|
323
|
+
circular=circular,
|
|
324
|
+
)
|
|
325
|
+
for start, end, matched in iterator:
|
|
326
|
+
symbol_location, coordinate_location, wrapped = _mapped_location(
|
|
327
|
+
local_start=start,
|
|
328
|
+
local_end=end,
|
|
329
|
+
segment_text_length=len(item.text),
|
|
330
|
+
segment=item,
|
|
331
|
+
strand=resolved_strand,
|
|
332
|
+
circular=circular,
|
|
333
|
+
)
|
|
334
|
+
key = (symbol_location, coordinate_location, matched)
|
|
335
|
+
if merge_strands and key in seen:
|
|
336
|
+
old_index = seen[key]
|
|
337
|
+
previous = hits[old_index]
|
|
338
|
+
hits[old_index] = MotifHit(
|
|
339
|
+
motif_name=previous.motif_name,
|
|
340
|
+
matched_sequence=previous.matched_sequence,
|
|
341
|
+
strand=Strand.BOTH,
|
|
342
|
+
symbol_location=previous.symbol_location,
|
|
343
|
+
coordinate_location=previous.coordinate_location,
|
|
344
|
+
score=None,
|
|
345
|
+
threshold=None,
|
|
346
|
+
wraps_origin=previous.wraps_origin,
|
|
347
|
+
)
|
|
348
|
+
continue
|
|
349
|
+
if len(hits) >= max_matches:
|
|
350
|
+
truncated = True
|
|
351
|
+
break
|
|
352
|
+
seen[key] = len(hits)
|
|
353
|
+
hits.append(
|
|
354
|
+
MotifHit(
|
|
355
|
+
motif_name=name,
|
|
356
|
+
matched_sequence=matched,
|
|
357
|
+
strand=resolved_strand,
|
|
358
|
+
symbol_location=symbol_location,
|
|
359
|
+
coordinate_location=coordinate_location,
|
|
360
|
+
score=None,
|
|
361
|
+
threshold=None,
|
|
362
|
+
wraps_origin=wrapped,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
if truncated:
|
|
366
|
+
break
|
|
367
|
+
if truncated:
|
|
368
|
+
break
|
|
369
|
+
result = build_result(
|
|
370
|
+
sequence,
|
|
371
|
+
sequence_id,
|
|
372
|
+
name="motif_scan",
|
|
373
|
+
method=resolved_mode,
|
|
374
|
+
algorithm_version="1.0",
|
|
375
|
+
parameters={
|
|
376
|
+
"motif": resolved_motif,
|
|
377
|
+
"motif_name": name,
|
|
378
|
+
"strand": str(strand),
|
|
379
|
+
"overlapping": overlapping,
|
|
380
|
+
"merge_strands": merge_strands,
|
|
381
|
+
"max_scan_length": max_scan_length,
|
|
382
|
+
"max_pattern_length": max_pattern_length,
|
|
383
|
+
"estimated_scan_cells": estimated_cells,
|
|
384
|
+
"max_scan_cells": max_scan_cells,
|
|
385
|
+
"target_iupac_rule": "set-intersection" if resolved_mode == "iupac" else "literal",
|
|
386
|
+
"regex_safety": "restricted-dna-subset" if resolved_mode == "regex" else None,
|
|
387
|
+
"regex_work_multiplier": regex_work_multiplier if resolved_mode == "regex" else None,
|
|
388
|
+
},
|
|
389
|
+
hits=hits,
|
|
390
|
+
max_matches=max_matches,
|
|
391
|
+
truncated=truncated,
|
|
392
|
+
provenance=pattern_provenance(reimplementation=resolved_mode == "regex"),
|
|
393
|
+
)
|
|
394
|
+
return cast(PatternResult[MotifHit], result)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _background(values: Mapping[str, float] | None) -> dict[str, float]:
|
|
398
|
+
if values is None:
|
|
399
|
+
return {base: 0.25 for base in "ACGT"}
|
|
400
|
+
if set(values) != set("ACGT"):
|
|
401
|
+
raise ConfigurationError("PWM background must contain exactly A, C, G, and T.")
|
|
402
|
+
resolved: dict[str, float] = {}
|
|
403
|
+
for base in "ACGT":
|
|
404
|
+
value = values[base]
|
|
405
|
+
if (
|
|
406
|
+
isinstance(value, bool)
|
|
407
|
+
or not isinstance(value, (int, float))
|
|
408
|
+
or not math.isfinite(value)
|
|
409
|
+
or value <= 0
|
|
410
|
+
):
|
|
411
|
+
raise ConfigurationError("PWM background probabilities must be finite and positive.")
|
|
412
|
+
resolved[base] = float(value)
|
|
413
|
+
total = math.fsum(resolved.values())
|
|
414
|
+
return {base: resolved[base] / total for base in "ACGT"}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _score_pwm(
|
|
418
|
+
text: str,
|
|
419
|
+
pwm: PWM,
|
|
420
|
+
background: Mapping[str, float],
|
|
421
|
+
pseudocount: float,
|
|
422
|
+
) -> float | None:
|
|
423
|
+
if any(base not in "ACGT" for base in text):
|
|
424
|
+
return None
|
|
425
|
+
scores: list[float] = []
|
|
426
|
+
for index, base in enumerate(text):
|
|
427
|
+
column_total = math.fsum(pwm.row(item)[index] + pseudocount for item in "ACGT")
|
|
428
|
+
probability = (pwm.row(base)[index] + pseudocount) / column_total
|
|
429
|
+
if probability <= 0:
|
|
430
|
+
return None
|
|
431
|
+
scores.append(math.log2(probability / background[base]))
|
|
432
|
+
return math.fsum(scores)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def scan_pwm(
|
|
436
|
+
value: SequenceInput,
|
|
437
|
+
pwm: PWM,
|
|
438
|
+
*,
|
|
439
|
+
threshold: float,
|
|
440
|
+
background: Mapping[str, float] | None = None,
|
|
441
|
+
pseudocount: float = 0.0,
|
|
442
|
+
strand: Strand | str = Strand.BOTH,
|
|
443
|
+
max_matches: int = 100_000,
|
|
444
|
+
max_scan_length: int = 1_000_000,
|
|
445
|
+
max_pwm_length: int = 10_000,
|
|
446
|
+
max_score_cells: int = 50_000_000,
|
|
447
|
+
) -> PatternResult[MotifHit]:
|
|
448
|
+
"""Scan a user-provided PWM with explicit log2-odds threshold and background."""
|
|
449
|
+
|
|
450
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
451
|
+
if not isinstance(pwm, PWM):
|
|
452
|
+
raise ConfigurationError("pwm must be a PWM object.")
|
|
453
|
+
validate_finite_score(threshold, "threshold")
|
|
454
|
+
validate_finite_score(pseudocount, "pseudocount")
|
|
455
|
+
if threshold is None:
|
|
456
|
+
raise ConfigurationError("threshold must be a finite number.")
|
|
457
|
+
if pseudocount is None:
|
|
458
|
+
raise ConfigurationError("pseudocount must be a finite non-negative number.")
|
|
459
|
+
if pseudocount < 0:
|
|
460
|
+
raise ConfigurationError("pseudocount must be non-negative.")
|
|
461
|
+
validate_positive_int(max_matches, "max_matches")
|
|
462
|
+
validate_positive_int(max_scan_length, "max_scan_length")
|
|
463
|
+
validate_positive_int(max_pwm_length, "max_pwm_length")
|
|
464
|
+
validate_positive_int(max_score_cells, "max_score_cells")
|
|
465
|
+
if sequence.symbol_length > max_scan_length:
|
|
466
|
+
raise ConfigurationError("Sequence exceeds max_scan_length.")
|
|
467
|
+
if pwm.length > max_pwm_length:
|
|
468
|
+
raise ConfigurationError("PWM exceeds max_pwm_length.")
|
|
469
|
+
resolved_background = _background(background)
|
|
470
|
+
circular = sequence.topology.value == "circular"
|
|
471
|
+
require_ungapped_circular(sequence, "PWM scanning")
|
|
472
|
+
if circular and pwm.length > sequence.symbol_length:
|
|
473
|
+
raise ConfigurationError("A circular PWM cannot be longer than the sequence.")
|
|
474
|
+
selected_strands = coerce_strands(strand)
|
|
475
|
+
possible_windows = sum(
|
|
476
|
+
len(item.text) if circular else max(0, len(item.text) - pwm.length + 1)
|
|
477
|
+
for item in segments(sequence)
|
|
478
|
+
) * len(selected_strands)
|
|
479
|
+
if possible_windows * pwm.length > max_score_cells:
|
|
480
|
+
raise ConfigurationError(
|
|
481
|
+
"PWM scan exceeds max_score_cells.",
|
|
482
|
+
context={
|
|
483
|
+
"possible_windows": possible_windows,
|
|
484
|
+
"pwm_length": pwm.length,
|
|
485
|
+
"max_score_cells": max_score_cells,
|
|
486
|
+
},
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
hits: list[MotifHit] = []
|
|
490
|
+
truncated = False
|
|
491
|
+
for item in segments(sequence):
|
|
492
|
+
for resolved_strand in selected_strands:
|
|
493
|
+
oriented = (
|
|
494
|
+
item.text
|
|
495
|
+
if resolved_strand is Strand.FORWARD
|
|
496
|
+
else reverse_complement_text(item.text)
|
|
497
|
+
)
|
|
498
|
+
searchable = (
|
|
499
|
+
oriented + oriented[: pwm.length - 1] if circular and pwm.length > 1 else oriented
|
|
500
|
+
)
|
|
501
|
+
limit = len(oriented) if circular else len(oriented) - pwm.length + 1
|
|
502
|
+
for start in range(max(0, limit)):
|
|
503
|
+
candidate = searchable[start : start + pwm.length]
|
|
504
|
+
score = _score_pwm(candidate, pwm, resolved_background, float(pseudocount))
|
|
505
|
+
if score is None or score < threshold:
|
|
506
|
+
continue
|
|
507
|
+
if len(hits) >= max_matches:
|
|
508
|
+
truncated = True
|
|
509
|
+
break
|
|
510
|
+
symbol_location, coordinate_location, wrapped = _mapped_location(
|
|
511
|
+
local_start=start,
|
|
512
|
+
local_end=start + pwm.length,
|
|
513
|
+
segment_text_length=len(item.text),
|
|
514
|
+
segment=item,
|
|
515
|
+
strand=resolved_strand,
|
|
516
|
+
circular=circular,
|
|
517
|
+
)
|
|
518
|
+
hits.append(
|
|
519
|
+
MotifHit(
|
|
520
|
+
motif_name=pwm.name,
|
|
521
|
+
matched_sequence=candidate,
|
|
522
|
+
strand=resolved_strand,
|
|
523
|
+
symbol_location=symbol_location,
|
|
524
|
+
coordinate_location=coordinate_location,
|
|
525
|
+
score=score,
|
|
526
|
+
threshold=float(threshold),
|
|
527
|
+
wraps_origin=wrapped,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
if truncated:
|
|
531
|
+
break
|
|
532
|
+
if truncated:
|
|
533
|
+
break
|
|
534
|
+
result = build_result(
|
|
535
|
+
sequence,
|
|
536
|
+
sequence_id,
|
|
537
|
+
name="pwm_scan",
|
|
538
|
+
method="log2_odds",
|
|
539
|
+
algorithm_version="1.0",
|
|
540
|
+
parameters={
|
|
541
|
+
"pwm_name": pwm.name,
|
|
542
|
+
"pwm_length": pwm.length,
|
|
543
|
+
"threshold": float(threshold),
|
|
544
|
+
"background": resolved_background,
|
|
545
|
+
"pseudocount": float(pseudocount),
|
|
546
|
+
"strand": str(strand),
|
|
547
|
+
"ambiguous_target_policy": "skip-window",
|
|
548
|
+
"max_scan_length": max_scan_length,
|
|
549
|
+
"max_pwm_length": max_pwm_length,
|
|
550
|
+
"score_cells": possible_windows * pwm.length,
|
|
551
|
+
"max_score_cells": max_score_cells,
|
|
552
|
+
},
|
|
553
|
+
hits=hits,
|
|
554
|
+
max_matches=max_matches,
|
|
555
|
+
truncated=truncated,
|
|
556
|
+
provenance=pattern_provenance(
|
|
557
|
+
reimplementation=True,
|
|
558
|
+
reference_name="FIMO-style PWM log-odds scoring boundary",
|
|
559
|
+
reference_version="DNAKit-1.0",
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
return cast(PatternResult[MotifHit], result)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def scan_promoter_motifs(
|
|
566
|
+
value: SequenceInput,
|
|
567
|
+
*,
|
|
568
|
+
motifs: Mapping[str, str] | None = None,
|
|
569
|
+
strand: Strand | str = Strand.BOTH,
|
|
570
|
+
max_matches: int = 100_000,
|
|
571
|
+
max_scan_length: int = 1_000_000,
|
|
572
|
+
max_scan_cells: int = 50_000_000,
|
|
573
|
+
max_motifs: int = 10_000,
|
|
574
|
+
) -> PatternResult[MotifHit]:
|
|
575
|
+
"""Scan predefined/user consensus motifs without predicting promoter activity."""
|
|
576
|
+
|
|
577
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
578
|
+
if motifs is not None and not isinstance(motifs, Mapping):
|
|
579
|
+
raise ConfigurationError("motifs must be a mapping from names to IUPAC definitions.")
|
|
580
|
+
definitions = dict(PROMOTER_MOTIFS if motifs is None else motifs)
|
|
581
|
+
if not definitions:
|
|
582
|
+
raise ConfigurationError("At least one promoter motif definition is required.")
|
|
583
|
+
if any(not isinstance(motif_name, str) or not motif_name.strip() for motif_name in definitions):
|
|
584
|
+
raise ConfigurationError("Promoter motif names must be non-empty strings.")
|
|
585
|
+
validate_positive_int(max_matches, "max_matches")
|
|
586
|
+
validate_positive_int(max_scan_length, "max_scan_length")
|
|
587
|
+
validate_positive_int(max_scan_cells, "max_scan_cells")
|
|
588
|
+
validate_positive_int(max_motifs, "max_motifs")
|
|
589
|
+
if len(definitions) > max_motifs:
|
|
590
|
+
raise ConfigurationError("Promoter motif panel exceeds max_motifs.")
|
|
591
|
+
definitions = {
|
|
592
|
+
motif_name: validate_iupac_text(motif, f"motifs[{motif_name!r}]")
|
|
593
|
+
for motif_name, motif in definitions.items()
|
|
594
|
+
}
|
|
595
|
+
selected_strands = coerce_strands(strand)
|
|
596
|
+
estimated_total_cells = (
|
|
597
|
+
sequence.symbol_length
|
|
598
|
+
* len(selected_strands)
|
|
599
|
+
* sum(len(motif) for motif in definitions.values())
|
|
600
|
+
)
|
|
601
|
+
if estimated_total_cells > max_scan_cells:
|
|
602
|
+
raise ConfigurationError(
|
|
603
|
+
"Promoter motif panel exceeds max_scan_cells.",
|
|
604
|
+
context={
|
|
605
|
+
"estimated_total_cells": estimated_total_cells,
|
|
606
|
+
"max_scan_cells": max_scan_cells,
|
|
607
|
+
},
|
|
608
|
+
)
|
|
609
|
+
hits: list[MotifHit] = []
|
|
610
|
+
truncated = False
|
|
611
|
+
for motif_name in sorted(definitions):
|
|
612
|
+
remaining = max_matches - len(hits)
|
|
613
|
+
if remaining <= 0:
|
|
614
|
+
truncated = True
|
|
615
|
+
break
|
|
616
|
+
partial = scan_motif(
|
|
617
|
+
sequence,
|
|
618
|
+
definitions[motif_name],
|
|
619
|
+
mode="iupac",
|
|
620
|
+
name=motif_name,
|
|
621
|
+
strand=strand,
|
|
622
|
+
max_matches=remaining,
|
|
623
|
+
max_scan_length=max_scan_length,
|
|
624
|
+
max_scan_cells=max_scan_cells,
|
|
625
|
+
)
|
|
626
|
+
hits.extend(partial.hits)
|
|
627
|
+
truncated = truncated or partial.truncated
|
|
628
|
+
result = build_result(
|
|
629
|
+
sequence,
|
|
630
|
+
sequence_id,
|
|
631
|
+
name="promoter_motif_scan",
|
|
632
|
+
method="iupac_consensus_candidates",
|
|
633
|
+
algorithm_version="1.0",
|
|
634
|
+
parameters={
|
|
635
|
+
"motifs": definitions,
|
|
636
|
+
"catalog": "DNAKit built-in boundary catalog" if motifs is None else "user-provided",
|
|
637
|
+
"catalog_version": PROMOTER_CATALOG_VERSION if motifs is None else None,
|
|
638
|
+
"activity_prediction": False,
|
|
639
|
+
"strand": str(strand),
|
|
640
|
+
"max_scan_length": max_scan_length,
|
|
641
|
+
"estimated_total_scan_cells": estimated_total_cells,
|
|
642
|
+
"max_scan_cells": max_scan_cells,
|
|
643
|
+
"max_motifs": max_motifs,
|
|
644
|
+
},
|
|
645
|
+
hits=hits,
|
|
646
|
+
max_matches=max_matches,
|
|
647
|
+
truncated=truncated,
|
|
648
|
+
provenance=pattern_provenance(
|
|
649
|
+
reimplementation=True,
|
|
650
|
+
reference_name="consensus promoter motif definitions",
|
|
651
|
+
reference_version=PROMOTER_CATALOG_VERSION if motifs is None else "user-provided",
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
return cast(PatternResult[MotifHit], result)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def scan_tf_pwm(
|
|
658
|
+
value: SequenceInput,
|
|
659
|
+
tf_name: str,
|
|
660
|
+
pwm: PWM,
|
|
661
|
+
*,
|
|
662
|
+
threshold: float,
|
|
663
|
+
background: Mapping[str, float] | None = None,
|
|
664
|
+
pseudocount: float = 0.0,
|
|
665
|
+
strand: Strand | str = Strand.BOTH,
|
|
666
|
+
max_matches: int = 100_000,
|
|
667
|
+
max_scan_length: int = 1_000_000,
|
|
668
|
+
max_pwm_length: int = 10_000,
|
|
669
|
+
max_score_cells: int = 50_000_000,
|
|
670
|
+
) -> PatternResult[MotifHit]:
|
|
671
|
+
"""Scan a user-supplied TF PWM; no binding-strength or database claim is made."""
|
|
672
|
+
|
|
673
|
+
if not isinstance(tf_name, str) or not tf_name.strip():
|
|
674
|
+
raise ConfigurationError("tf_name must be non-empty.")
|
|
675
|
+
if not isinstance(pwm, PWM):
|
|
676
|
+
raise ConfigurationError("pwm must be a PWM object.")
|
|
677
|
+
renamed = PWM(tf_name, {base: pwm.row(base) for base in "ACGT"})
|
|
678
|
+
result = scan_pwm(
|
|
679
|
+
value,
|
|
680
|
+
renamed,
|
|
681
|
+
threshold=threshold,
|
|
682
|
+
background=background,
|
|
683
|
+
pseudocount=pseudocount,
|
|
684
|
+
strand=strand,
|
|
685
|
+
max_matches=max_matches,
|
|
686
|
+
max_scan_length=max_scan_length,
|
|
687
|
+
max_pwm_length=max_pwm_length,
|
|
688
|
+
max_score_cells=max_score_cells,
|
|
689
|
+
)
|
|
690
|
+
parameters = dict(result.parameters)
|
|
691
|
+
parameters.update({"tf_name": tf_name, "binding_strength_prediction": False})
|
|
692
|
+
return PatternResult(
|
|
693
|
+
name="tf_motif_scan",
|
|
694
|
+
method=result.method,
|
|
695
|
+
algorithm_version=result.algorithm_version,
|
|
696
|
+
sequence_id=result.sequence_id,
|
|
697
|
+
parameters=type(result.parameters)(parameters),
|
|
698
|
+
hits=result.hits,
|
|
699
|
+
inspected_symbol_count=result.inspected_symbol_count,
|
|
700
|
+
gap_count=result.gap_count,
|
|
701
|
+
unknown_gap_count=result.unknown_gap_count,
|
|
702
|
+
max_matches=result.max_matches,
|
|
703
|
+
truncated=result.truncated,
|
|
704
|
+
coordinate_system=result.coordinate_system,
|
|
705
|
+
gap_policy=result.gap_policy,
|
|
706
|
+
topology=result.topology,
|
|
707
|
+
provenance=result.provenance,
|
|
708
|
+
issues=result.issues,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
__all__ = [
|
|
713
|
+
"MAX_PWM_LENGTH",
|
|
714
|
+
"PROMOTER_CATALOG_VERSION",
|
|
715
|
+
"PROMOTER_MOTIFS",
|
|
716
|
+
"PWM",
|
|
717
|
+
"scan_motif",
|
|
718
|
+
"scan_promoter_motifs",
|
|
719
|
+
"scan_pwm",
|
|
720
|
+
"scan_tf_pwm",
|
|
721
|
+
]
|