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,286 @@
|
|
|
1
|
+
"""Literal exact sequence and subsequence search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
|
|
7
|
+
from dnakit.core import DNASequence, DNASet, Strand
|
|
8
|
+
from dnakit.exceptions import ConfigurationError
|
|
9
|
+
from dnakit.similarity._shared import (
|
|
10
|
+
SequenceInput,
|
|
11
|
+
materialize_targets,
|
|
12
|
+
sequence_and_id,
|
|
13
|
+
validate_bool,
|
|
14
|
+
validate_positive_int,
|
|
15
|
+
)
|
|
16
|
+
from dnakit.similarity.results import SearchResult, SequenceMatch
|
|
17
|
+
|
|
18
|
+
DEFAULT_MAX_SEARCH_TARGETS = 100_000
|
|
19
|
+
DEFAULT_MAX_SEARCH_MATCHES = 1_000_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _coerce_strand(value: Strand | str) -> Strand:
|
|
23
|
+
try:
|
|
24
|
+
resolved = value if isinstance(value, Strand) else Strand(value)
|
|
25
|
+
except (TypeError, ValueError) as exc:
|
|
26
|
+
raise ConfigurationError(
|
|
27
|
+
"Unknown search strand.",
|
|
28
|
+
code="INVALID_SEARCH_STRAND",
|
|
29
|
+
context={"strand": value},
|
|
30
|
+
hint="Choose 'forward', 'reverse', or 'both'.",
|
|
31
|
+
) from exc
|
|
32
|
+
if resolved is Strand.UNKNOWN:
|
|
33
|
+
raise ConfigurationError(
|
|
34
|
+
"Search strand cannot be 'unknown'.",
|
|
35
|
+
code="INVALID_SEARCH_STRAND",
|
|
36
|
+
)
|
|
37
|
+
return resolved
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _literal_occurrences(query: str, target: str, *, overlapping: bool) -> Iterator[int]:
|
|
41
|
+
if query == "":
|
|
42
|
+
yield from range(len(target) + 1)
|
|
43
|
+
return
|
|
44
|
+
cursor = 0
|
|
45
|
+
step = 1 if overlapping else len(query)
|
|
46
|
+
while cursor <= len(target) - len(query):
|
|
47
|
+
position = target.find(query, cursor)
|
|
48
|
+
if position < 0:
|
|
49
|
+
break
|
|
50
|
+
yield position
|
|
51
|
+
cursor = position + step
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _orientations(query: DNASequence, strand: Strand) -> tuple[tuple[Strand, str], ...]:
|
|
55
|
+
selected: list[tuple[Strand, str]] = []
|
|
56
|
+
if strand in (Strand.FORWARD, Strand.BOTH):
|
|
57
|
+
selected.append((Strand.FORWARD, query.symbols))
|
|
58
|
+
if strand in (Strand.REVERSE, Strand.BOTH):
|
|
59
|
+
selected.append((Strand.REVERSE, query.reverse_complement().symbols))
|
|
60
|
+
return tuple(selected)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ordered_matches(matches: list[SequenceMatch]) -> tuple[SequenceMatch, ...]:
|
|
64
|
+
strand_order = {Strand.FORWARD: 0, Strand.REVERSE: 1, Strand.BOTH: 2}
|
|
65
|
+
return tuple(
|
|
66
|
+
sorted(
|
|
67
|
+
matches,
|
|
68
|
+
key=lambda match: (
|
|
69
|
+
match.target_index,
|
|
70
|
+
match.start,
|
|
71
|
+
match.end,
|
|
72
|
+
strand_order[match.strand],
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _BoundedMatchCollector:
|
|
79
|
+
"""Collect returned matches while enforcing a strict non-truncating limit."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, *, max_matches: int, merge_strands: bool) -> None:
|
|
82
|
+
self._max_matches = max_matches
|
|
83
|
+
self._merge_strands = merge_strands
|
|
84
|
+
self._matches: list[SequenceMatch] = []
|
|
85
|
+
self._groups: dict[tuple[int, str | None, int, int], set[Strand]] = {}
|
|
86
|
+
|
|
87
|
+
def add(self, match: SequenceMatch) -> None:
|
|
88
|
+
if not self._merge_strands:
|
|
89
|
+
if len(self._matches) >= self._max_matches:
|
|
90
|
+
self._raise_limit()
|
|
91
|
+
self._matches.append(match)
|
|
92
|
+
return
|
|
93
|
+
key = (match.target_index, match.target_id, match.start, match.end)
|
|
94
|
+
strands = self._groups.get(key)
|
|
95
|
+
if strands is None:
|
|
96
|
+
if len(self._groups) >= self._max_matches:
|
|
97
|
+
self._raise_limit()
|
|
98
|
+
self._groups[key] = {match.strand}
|
|
99
|
+
else:
|
|
100
|
+
strands.add(match.strand)
|
|
101
|
+
|
|
102
|
+
def materialize(self) -> tuple[SequenceMatch, ...]:
|
|
103
|
+
if not self._merge_strands:
|
|
104
|
+
return _ordered_matches(self._matches)
|
|
105
|
+
matches = [
|
|
106
|
+
SequenceMatch(
|
|
107
|
+
target_index,
|
|
108
|
+
target_id,
|
|
109
|
+
start,
|
|
110
|
+
end,
|
|
111
|
+
Strand.BOTH if len(strands) > 1 else next(iter(strands)),
|
|
112
|
+
)
|
|
113
|
+
for (target_index, target_id, start, end), strands in self._groups.items()
|
|
114
|
+
]
|
|
115
|
+
return _ordered_matches(matches)
|
|
116
|
+
|
|
117
|
+
def _raise_limit(self) -> None:
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
"Search result exceeds max_matches.",
|
|
120
|
+
code="SEARCH_MATCH_LIMIT_EXCEEDED",
|
|
121
|
+
context={
|
|
122
|
+
"match_count": self._max_matches + 1,
|
|
123
|
+
"match_count_is_lower_bound": True,
|
|
124
|
+
"max_matches": self._max_matches,
|
|
125
|
+
},
|
|
126
|
+
hint="Narrow the search or increase max_matches explicitly.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def exact_search(
|
|
131
|
+
query: SequenceInput,
|
|
132
|
+
targets: SequenceInput | DNASet | Iterable[SequenceInput],
|
|
133
|
+
*,
|
|
134
|
+
reverse_complement: bool = False,
|
|
135
|
+
merge_strands: bool = True,
|
|
136
|
+
max_targets: int = DEFAULT_MAX_SEARCH_TARGETS,
|
|
137
|
+
max_matches: int = DEFAULT_MAX_SEARCH_MATCHES,
|
|
138
|
+
) -> SearchResult:
|
|
139
|
+
"""Find targets whose complete symbol strings equal the query.
|
|
140
|
+
|
|
141
|
+
Record metadata, topology, and strandedness do not change literal symbol
|
|
142
|
+
equality. With ``reverse_complement=True``, a reverse-only match is marked
|
|
143
|
+
``Strand.REVERSE``; a palindromic hit is merged to ``Strand.BOTH`` by
|
|
144
|
+
default. Empty query matches only an empty target.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
validate_bool(reverse_complement, "reverse_complement")
|
|
148
|
+
validate_bool(merge_strands, "merge_strands")
|
|
149
|
+
validate_positive_int(max_targets, "max_targets")
|
|
150
|
+
validate_positive_int(max_matches, "max_matches")
|
|
151
|
+
query_sequence, query_id = sequence_and_id(query, role="query")
|
|
152
|
+
target_items = materialize_targets(targets, max_targets=max_targets)
|
|
153
|
+
strand = Strand.BOTH if reverse_complement else Strand.FORWARD
|
|
154
|
+
matches = _BoundedMatchCollector(
|
|
155
|
+
max_matches=max_matches,
|
|
156
|
+
merge_strands=merge_strands,
|
|
157
|
+
)
|
|
158
|
+
for target_index, target_input in enumerate(target_items):
|
|
159
|
+
target, target_id = sequence_and_id(target_input, role=f"target[{target_index}]")
|
|
160
|
+
for orientation, oriented_query in _orientations(query_sequence, strand):
|
|
161
|
+
if oriented_query == target.symbols:
|
|
162
|
+
matches.add(
|
|
163
|
+
SequenceMatch(
|
|
164
|
+
target_index,
|
|
165
|
+
target_id,
|
|
166
|
+
0,
|
|
167
|
+
target.symbol_length,
|
|
168
|
+
orientation,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
materialized = matches.materialize()
|
|
172
|
+
return SearchResult(
|
|
173
|
+
name="exact_search",
|
|
174
|
+
method="literal_full_sequence_equality",
|
|
175
|
+
query_id=query_id,
|
|
176
|
+
query_length=query_sequence.symbol_length,
|
|
177
|
+
target_count=len(target_items),
|
|
178
|
+
matches=materialized,
|
|
179
|
+
overlapping=False,
|
|
180
|
+
reverse_complement=reverse_complement,
|
|
181
|
+
merge_strands=merge_strands,
|
|
182
|
+
full_length=True,
|
|
183
|
+
max_targets=max_targets,
|
|
184
|
+
max_matches=max_matches,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def subsequence_search(
|
|
189
|
+
query: SequenceInput,
|
|
190
|
+
target: SequenceInput | DNASet | Iterable[SequenceInput],
|
|
191
|
+
*,
|
|
192
|
+
strand: Strand | str = Strand.FORWARD,
|
|
193
|
+
overlapping: bool = True,
|
|
194
|
+
merge_strands: bool = True,
|
|
195
|
+
max_targets: int = DEFAULT_MAX_SEARCH_TARGETS,
|
|
196
|
+
max_matches: int = DEFAULT_MAX_SEARCH_MATCHES,
|
|
197
|
+
) -> SearchResult:
|
|
198
|
+
"""Find literal query occurrences across one or more targets.
|
|
199
|
+
|
|
200
|
+
Coordinates are internal zero-based half-open intervals and never wrap a
|
|
201
|
+
circular origin. IUPAC symbols are ordinary exact symbols: ``N`` matches
|
|
202
|
+
``N``, not every canonical nucleotide. An empty query matches every target
|
|
203
|
+
boundary ``0..len(target)``.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
resolved_strand = _coerce_strand(strand)
|
|
207
|
+
validate_bool(overlapping, "overlapping")
|
|
208
|
+
validate_bool(merge_strands, "merge_strands")
|
|
209
|
+
validate_positive_int(max_targets, "max_targets")
|
|
210
|
+
validate_positive_int(max_matches, "max_matches")
|
|
211
|
+
query_sequence, query_id = sequence_and_id(query, role="query")
|
|
212
|
+
target_items = materialize_targets(target, max_targets=max_targets)
|
|
213
|
+
matches = _BoundedMatchCollector(
|
|
214
|
+
max_matches=max_matches,
|
|
215
|
+
merge_strands=merge_strands,
|
|
216
|
+
)
|
|
217
|
+
for target_index, target_input in enumerate(target_items):
|
|
218
|
+
target_sequence, target_id = sequence_and_id(
|
|
219
|
+
target_input,
|
|
220
|
+
role=f"target[{target_index}]",
|
|
221
|
+
)
|
|
222
|
+
for orientation, oriented_query in _orientations(query_sequence, resolved_strand):
|
|
223
|
+
for start in _literal_occurrences(
|
|
224
|
+
oriented_query,
|
|
225
|
+
target_sequence.symbols,
|
|
226
|
+
overlapping=overlapping,
|
|
227
|
+
):
|
|
228
|
+
matches.add(
|
|
229
|
+
SequenceMatch(
|
|
230
|
+
target_index,
|
|
231
|
+
target_id,
|
|
232
|
+
start,
|
|
233
|
+
start + len(oriented_query),
|
|
234
|
+
orientation,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
materialized = matches.materialize()
|
|
238
|
+
return SearchResult(
|
|
239
|
+
name="subsequence_search",
|
|
240
|
+
method="literal_exact_subsequence",
|
|
241
|
+
query_id=query_id,
|
|
242
|
+
query_length=query_sequence.symbol_length,
|
|
243
|
+
target_count=len(target_items),
|
|
244
|
+
matches=materialized,
|
|
245
|
+
overlapping=overlapping,
|
|
246
|
+
reverse_complement=resolved_strand in (Strand.REVERSE, Strand.BOTH),
|
|
247
|
+
merge_strands=merge_strands,
|
|
248
|
+
full_length=False,
|
|
249
|
+
max_targets=max_targets,
|
|
250
|
+
max_matches=max_matches,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def reverse_complement_search(
|
|
255
|
+
query: SequenceInput,
|
|
256
|
+
target: SequenceInput | DNASet | Iterable[SequenceInput],
|
|
257
|
+
*,
|
|
258
|
+
overlapping: bool = True,
|
|
259
|
+
merge_strands: bool = True,
|
|
260
|
+
max_targets: int = DEFAULT_MAX_SEARCH_TARGETS,
|
|
261
|
+
max_matches: int = DEFAULT_MAX_SEARCH_MATCHES,
|
|
262
|
+
) -> SearchResult:
|
|
263
|
+
"""Search both the supplied query and its reverse complement."""
|
|
264
|
+
|
|
265
|
+
return subsequence_search(
|
|
266
|
+
query,
|
|
267
|
+
target,
|
|
268
|
+
strand=Strand.BOTH,
|
|
269
|
+
overlapping=overlapping,
|
|
270
|
+
merge_strands=merge_strands,
|
|
271
|
+
max_targets=max_targets,
|
|
272
|
+
max_matches=max_matches,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
find_subsequence = subsequence_search
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
__all__ = [
|
|
280
|
+
"DEFAULT_MAX_SEARCH_MATCHES",
|
|
281
|
+
"DEFAULT_MAX_SEARCH_TARGETS",
|
|
282
|
+
"exact_search",
|
|
283
|
+
"find_subsequence",
|
|
284
|
+
"reverse_complement_search",
|
|
285
|
+
"subsequence_search",
|
|
286
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Similarity metrics for compatible DNAKit sketches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from dnakit.core._json import FrozenDict
|
|
8
|
+
from dnakit.exceptions import ConfigurationError
|
|
9
|
+
from dnakit.fingerprints import SketchResult
|
|
10
|
+
|
|
11
|
+
from .results import SketchSimilarityResult
|
|
12
|
+
|
|
13
|
+
SketchMetric = Literal["jaccard", "containment"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_compatible(left: SketchResult, right: SketchResult) -> None:
|
|
17
|
+
if not isinstance(left, SketchResult) or not isinstance(right, SketchResult):
|
|
18
|
+
raise ConfigurationError(
|
|
19
|
+
"Sketch comparison requires two SketchResult objects.",
|
|
20
|
+
code="INVALID_SKETCH_INPUT",
|
|
21
|
+
)
|
|
22
|
+
fields = ("schema_version", "k", "canonical", "seed", "selection")
|
|
23
|
+
mismatches = tuple(name for name in fields if getattr(left, name) != getattr(right, name))
|
|
24
|
+
if left.selection == "bottom_k" and left.num_hashes != right.num_hashes:
|
|
25
|
+
mismatches += ("num_hashes",)
|
|
26
|
+
if left.selection == "scaled" and left.scaled != right.scaled:
|
|
27
|
+
mismatches += ("scaled",)
|
|
28
|
+
if mismatches:
|
|
29
|
+
raise ConfigurationError(
|
|
30
|
+
"Sketch schemas are incompatible.",
|
|
31
|
+
code="SKETCH_SCHEMA_MISMATCH",
|
|
32
|
+
context={"fields": mismatches},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sketch_similarity(
|
|
37
|
+
left: SketchResult,
|
|
38
|
+
right: SketchResult,
|
|
39
|
+
*,
|
|
40
|
+
metric: SketchMetric = "jaccard",
|
|
41
|
+
min_shared_hashes: int = 0,
|
|
42
|
+
) -> SketchSimilarityResult:
|
|
43
|
+
"""Compare compatible sketches as sets, with explicit empty-set semantics."""
|
|
44
|
+
|
|
45
|
+
_validate_compatible(left, right)
|
|
46
|
+
if metric not in {"jaccard", "containment"}:
|
|
47
|
+
raise ConfigurationError("Unknown sketch metric.", code="UNKNOWN_SKETCH_METRIC")
|
|
48
|
+
if (
|
|
49
|
+
isinstance(min_shared_hashes, bool)
|
|
50
|
+
or not isinstance(min_shared_hashes, int)
|
|
51
|
+
or min_shared_hashes < 0
|
|
52
|
+
):
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
"min_shared_hashes must be a non-negative integer.",
|
|
55
|
+
code="INVALID_MIN_SHARED_HASHES",
|
|
56
|
+
)
|
|
57
|
+
left_set = set(left.hashes)
|
|
58
|
+
right_set = set(right.hashes)
|
|
59
|
+
shared = len(left_set & right_set)
|
|
60
|
+
union = len(left_set | right_set)
|
|
61
|
+
if metric == "jaccard":
|
|
62
|
+
value = shared / union if union else 1.0
|
|
63
|
+
denominator = union
|
|
64
|
+
else:
|
|
65
|
+
denominator = len(left_set)
|
|
66
|
+
value = shared / denominator if denominator else (1.0 if not right_set else 0.0)
|
|
67
|
+
passed = shared >= min_shared_hashes
|
|
68
|
+
return SketchSimilarityResult(
|
|
69
|
+
name="sketch_similarity",
|
|
70
|
+
method=f"set-{metric}",
|
|
71
|
+
value=value,
|
|
72
|
+
metric=metric,
|
|
73
|
+
left_id=left.sequence_id,
|
|
74
|
+
right_id=right.sequence_id,
|
|
75
|
+
left_hash_count=len(left_set),
|
|
76
|
+
right_hash_count=len(right_set),
|
|
77
|
+
shared_hash_count=shared,
|
|
78
|
+
union_hash_count=union,
|
|
79
|
+
denominator=denominator,
|
|
80
|
+
passed_min_shared_hashes=passed,
|
|
81
|
+
parameters=FrozenDict(
|
|
82
|
+
{
|
|
83
|
+
"schema_version": left.schema_version,
|
|
84
|
+
"k": left.k,
|
|
85
|
+
"canonical": left.canonical,
|
|
86
|
+
"seed": left.seed,
|
|
87
|
+
"selection": left.selection,
|
|
88
|
+
"num_hashes": left.num_hashes,
|
|
89
|
+
"scaled": left.scaled,
|
|
90
|
+
"min_shared_hashes": min_shared_hashes,
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["SketchMetric", "sketch_similarity"]
|