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,410 @@
|
|
|
1
|
+
"""Backend-neutral dot-bracket parsing and probability-derived metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from itertools import islice
|
|
8
|
+
|
|
9
|
+
from dnakit.core import DNASequence
|
|
10
|
+
from dnakit.exceptions import ConfigurationError
|
|
11
|
+
from dnakit.thermodynamics._shared import canonical_linear_symbols
|
|
12
|
+
|
|
13
|
+
from .results import (
|
|
14
|
+
AccessibilityWindow,
|
|
15
|
+
PairProbabilityResult,
|
|
16
|
+
SecondaryBasePair,
|
|
17
|
+
SecondaryStem,
|
|
18
|
+
SecondaryStructureSummary,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_OPEN_TO_CLOSE = {"(": ")", "[": "]", "{": "}", "<": ">"}
|
|
22
|
+
_CLOSE_TO_OPEN = {value: key for key, value in _OPEN_TO_CLOSE.items()}
|
|
23
|
+
_MAX_STRANDS = 100
|
|
24
|
+
_MAX_TOTAL_NT = 10_000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _strands(values: Iterable[DNASequence]) -> tuple[str, ...]:
|
|
28
|
+
if not isinstance(values, Iterable):
|
|
29
|
+
raise ConfigurationError(
|
|
30
|
+
"strands must be an iterable of DNASequence objects.",
|
|
31
|
+
code="INVALID_SECONDARY_STRUCTURE_STRANDS",
|
|
32
|
+
)
|
|
33
|
+
items = tuple(islice(iter(values), _MAX_STRANDS + 1))
|
|
34
|
+
if not 1 <= len(items) <= _MAX_STRANDS:
|
|
35
|
+
raise ConfigurationError(
|
|
36
|
+
f"strands must contain 1-{_MAX_STRANDS} entries.",
|
|
37
|
+
code="INVALID_SECONDARY_STRUCTURE_STRANDS",
|
|
38
|
+
)
|
|
39
|
+
if any(not isinstance(item, DNASequence) for item in items):
|
|
40
|
+
raise ConfigurationError(
|
|
41
|
+
"Every strand must be a DNASequence.",
|
|
42
|
+
code="INVALID_SECONDARY_STRUCTURE_STRANDS",
|
|
43
|
+
)
|
|
44
|
+
symbols = tuple(
|
|
45
|
+
canonical_linear_symbols(
|
|
46
|
+
item,
|
|
47
|
+
operation="secondary structure",
|
|
48
|
+
min_length=1,
|
|
49
|
+
max_length=_MAX_TOTAL_NT,
|
|
50
|
+
)
|
|
51
|
+
for item in items
|
|
52
|
+
)
|
|
53
|
+
if sum(map(len, symbols)) > _MAX_TOTAL_NT:
|
|
54
|
+
raise ConfigurationError(
|
|
55
|
+
f"Secondary-structure input exceeds {_MAX_TOTAL_NT} total nucleotides.",
|
|
56
|
+
code="SECONDARY_STRUCTURE_SIZE_LIMIT",
|
|
57
|
+
)
|
|
58
|
+
return symbols
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _locations(strands: tuple[str, ...]) -> tuple[tuple[int, int], ...]:
|
|
62
|
+
return tuple(
|
|
63
|
+
(strand_index, local_index)
|
|
64
|
+
for strand_index, sequence in enumerate(strands)
|
|
65
|
+
for local_index in range(len(sequence))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def analyze_dot_bracket(
|
|
70
|
+
strands: Iterable[DNASequence],
|
|
71
|
+
dot_bracket: str,
|
|
72
|
+
*,
|
|
73
|
+
three_prime_window: int = 5,
|
|
74
|
+
) -> SecondaryStructureSummary:
|
|
75
|
+
"""Parse dot-parens-plus or extended bracket notation into structure metrics."""
|
|
76
|
+
|
|
77
|
+
symbols = _strands(strands)
|
|
78
|
+
if not isinstance(dot_bracket, str) or not dot_bracket:
|
|
79
|
+
raise ConfigurationError("dot_bracket must be non-empty text.", code="INVALID_DOT_BRACKET")
|
|
80
|
+
if (
|
|
81
|
+
isinstance(three_prime_window, bool)
|
|
82
|
+
or not isinstance(three_prime_window, int)
|
|
83
|
+
or not 1 <= three_prime_window <= _MAX_TOTAL_NT
|
|
84
|
+
):
|
|
85
|
+
raise ConfigurationError(
|
|
86
|
+
"three_prime_window must be a positive bounded integer.",
|
|
87
|
+
code="INVALID_DOT_BRACKET_WINDOW",
|
|
88
|
+
)
|
|
89
|
+
parts = dot_bracket.split("+")
|
|
90
|
+
if len(parts) != len(symbols) or any(
|
|
91
|
+
len(part) != len(sequence) for part, sequence in zip(parts, symbols, strict=True)
|
|
92
|
+
):
|
|
93
|
+
raise ConfigurationError(
|
|
94
|
+
"Dot-bracket strand separators and lengths must match the input strands.",
|
|
95
|
+
code="DOT_BRACKET_LENGTH_MISMATCH",
|
|
96
|
+
context={
|
|
97
|
+
"expected_lengths": tuple(map(len, symbols)),
|
|
98
|
+
"observed_lengths": tuple(map(len, parts)),
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
structure = "".join(parts)
|
|
102
|
+
invalid = sorted(set(structure) - {".", *_OPEN_TO_CLOSE, *_CLOSE_TO_OPEN})
|
|
103
|
+
if invalid:
|
|
104
|
+
raise ConfigurationError(
|
|
105
|
+
"Dot-bracket text contains unsupported symbols.",
|
|
106
|
+
code="INVALID_DOT_BRACKET_SYMBOL",
|
|
107
|
+
context={"symbols": tuple(invalid)},
|
|
108
|
+
)
|
|
109
|
+
locations = _locations(symbols)
|
|
110
|
+
stacks: dict[str, list[int]] = {character: [] for character in _OPEN_TO_CLOSE}
|
|
111
|
+
pairs: list[SecondaryBasePair] = []
|
|
112
|
+
for global_index, character in enumerate(structure):
|
|
113
|
+
if character in _OPEN_TO_CLOSE:
|
|
114
|
+
stacks[character].append(global_index)
|
|
115
|
+
elif character in _CLOSE_TO_OPEN:
|
|
116
|
+
opener = _CLOSE_TO_OPEN[character]
|
|
117
|
+
if not stacks[opener]:
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
"Dot-bracket text closes a pair before it opens.",
|
|
120
|
+
code="UNBALANCED_DOT_BRACKET",
|
|
121
|
+
context={"global_index": global_index, "symbol": character},
|
|
122
|
+
)
|
|
123
|
+
first = stacks[opener].pop()
|
|
124
|
+
first_strand, first_local = locations[first]
|
|
125
|
+
second_strand, second_local = locations[global_index]
|
|
126
|
+
pairs.append(
|
|
127
|
+
SecondaryBasePair(
|
|
128
|
+
first_global_index=first,
|
|
129
|
+
second_global_index=global_index,
|
|
130
|
+
first_strand_index=first_strand,
|
|
131
|
+
first_local_index=first_local,
|
|
132
|
+
second_strand_index=second_strand,
|
|
133
|
+
second_local_index=second_local,
|
|
134
|
+
bracket_type=opener + character,
|
|
135
|
+
inter_strand=first_strand != second_strand,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
unclosed = tuple((symbol, tuple(indices)) for symbol, indices in stacks.items() if indices)
|
|
139
|
+
if unclosed:
|
|
140
|
+
raise ConfigurationError(
|
|
141
|
+
"Dot-bracket text contains unclosed pairs.",
|
|
142
|
+
code="UNBALANCED_DOT_BRACKET",
|
|
143
|
+
context={"unclosed": unclosed},
|
|
144
|
+
)
|
|
145
|
+
ordered_pairs = tuple(sorted(pairs, key=lambda item: item.first_global_index))
|
|
146
|
+
by_indices = {
|
|
147
|
+
(item.first_global_index, item.second_global_index): item for item in ordered_pairs
|
|
148
|
+
}
|
|
149
|
+
remaining = set(by_indices)
|
|
150
|
+
stems: list[SecondaryStem] = []
|
|
151
|
+
while remaining:
|
|
152
|
+
start = min(remaining)
|
|
153
|
+
run: list[SecondaryBasePair] = []
|
|
154
|
+
current = start
|
|
155
|
+
while current in remaining:
|
|
156
|
+
pair = by_indices[current]
|
|
157
|
+
run.append(pair)
|
|
158
|
+
remaining.remove(current)
|
|
159
|
+
current = (current[0] + 1, current[1] - 1)
|
|
160
|
+
stems.append(
|
|
161
|
+
SecondaryStem(
|
|
162
|
+
base_pairs=tuple(run),
|
|
163
|
+
length=len(run),
|
|
164
|
+
inter_strand=all(item.inter_strand for item in run),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
hairpin_loops: list[int] = []
|
|
168
|
+
for stem in stems:
|
|
169
|
+
innermost = stem.base_pairs[-1]
|
|
170
|
+
if innermost.inter_strand:
|
|
171
|
+
continue
|
|
172
|
+
nested_pair_exists = any(
|
|
173
|
+
innermost.first_global_index < other.first_global_index
|
|
174
|
+
and other.second_global_index < innermost.second_global_index
|
|
175
|
+
for other in ordered_pairs
|
|
176
|
+
)
|
|
177
|
+
if not nested_pair_exists:
|
|
178
|
+
hairpin_loops.append(innermost.second_local_index - innermost.first_local_index - 1)
|
|
179
|
+
three_prime_stems = tuple(
|
|
180
|
+
stem
|
|
181
|
+
for stem in stems
|
|
182
|
+
if stem.inter_strand
|
|
183
|
+
and any(
|
|
184
|
+
pair.first_local_index >= len(symbols[pair.first_strand_index]) - three_prime_window
|
|
185
|
+
or pair.second_local_index
|
|
186
|
+
>= len(symbols[pair.second_strand_index]) - three_prime_window
|
|
187
|
+
for pair in stem.base_pairs
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
if not ordered_pairs:
|
|
191
|
+
structure_type = "unstructured" if len(symbols) == 1 else "unbound-strands"
|
|
192
|
+
elif len(symbols) == 1:
|
|
193
|
+
structure_type = "hairpin" if hairpin_loops else "intramolecular-fold"
|
|
194
|
+
elif len(symbols) == 2 and symbols[0] == symbols[1]:
|
|
195
|
+
structure_type = "self-dimer"
|
|
196
|
+
elif len(symbols) == 2:
|
|
197
|
+
structure_type = "heterodimer"
|
|
198
|
+
else:
|
|
199
|
+
structure_type = "multi-strand-complex"
|
|
200
|
+
total_nt = sum(map(len, symbols))
|
|
201
|
+
stem_lengths = tuple(stem.length for stem in stems)
|
|
202
|
+
return SecondaryStructureSummary(
|
|
203
|
+
strands_5to3=symbols,
|
|
204
|
+
dot_bracket=dot_bracket,
|
|
205
|
+
base_pairs=ordered_pairs,
|
|
206
|
+
stems=tuple(stems),
|
|
207
|
+
structure_type=structure_type,
|
|
208
|
+
base_pair_count=len(ordered_pairs),
|
|
209
|
+
paired_base_fraction=(2.0 * len(ordered_pairs) / total_nt),
|
|
210
|
+
stem_lengths=stem_lengths,
|
|
211
|
+
hairpin_count=len(hairpin_loops),
|
|
212
|
+
hairpin_loop_lengths=tuple(hairpin_loops),
|
|
213
|
+
max_contiguous_pair_count=max(stem_lengths, default=0),
|
|
214
|
+
three_prime_window=three_prime_window,
|
|
215
|
+
three_prime_dimer=bool(three_prime_stems),
|
|
216
|
+
three_prime_dimer_max_contiguous_pairs=max(
|
|
217
|
+
(stem.length for stem in three_prime_stems), default=0
|
|
218
|
+
),
|
|
219
|
+
method="deterministic-dot-bracket-parser-v1",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def pair_probability_metrics(
|
|
224
|
+
strands: Iterable[DNASequence],
|
|
225
|
+
probability_matrix: Iterable[Iterable[float]],
|
|
226
|
+
*,
|
|
227
|
+
accessibility_window_size: int = 4,
|
|
228
|
+
) -> PairProbabilityResult:
|
|
229
|
+
"""Validate a dense NUPACK-style matrix and derive per-base accessibility."""
|
|
230
|
+
|
|
231
|
+
symbols = _strands(strands)
|
|
232
|
+
total_nt = sum(map(len, symbols))
|
|
233
|
+
if (
|
|
234
|
+
isinstance(accessibility_window_size, bool)
|
|
235
|
+
or not isinstance(accessibility_window_size, int)
|
|
236
|
+
or accessibility_window_size <= 0
|
|
237
|
+
):
|
|
238
|
+
raise ConfigurationError(
|
|
239
|
+
"accessibility_window_size must be a positive integer.",
|
|
240
|
+
code="INVALID_ACCESSIBILITY_WINDOW",
|
|
241
|
+
)
|
|
242
|
+
if not isinstance(probability_matrix, Iterable):
|
|
243
|
+
raise ConfigurationError(
|
|
244
|
+
"probability_matrix must be a square numeric matrix.",
|
|
245
|
+
code="INVALID_PAIR_PROBABILITY_MATRIX",
|
|
246
|
+
)
|
|
247
|
+
raw_rows = tuple(islice(iter(probability_matrix), total_nt + 1))
|
|
248
|
+
if len(raw_rows) != total_nt:
|
|
249
|
+
raise ConfigurationError(
|
|
250
|
+
"Pair-probability matrix size does not match total strand length.",
|
|
251
|
+
code="PAIR_PROBABILITY_SIZE_MISMATCH",
|
|
252
|
+
)
|
|
253
|
+
rows: list[tuple[float, ...]] = []
|
|
254
|
+
for row in raw_rows:
|
|
255
|
+
if isinstance(row, (str, bytes)) or not isinstance(row, Iterable):
|
|
256
|
+
raise ConfigurationError(
|
|
257
|
+
"Every pair-probability row must be iterable.",
|
|
258
|
+
code="INVALID_PAIR_PROBABILITY_MATRIX",
|
|
259
|
+
)
|
|
260
|
+
raw_values = tuple(islice(iter(row), total_nt + 1))
|
|
261
|
+
if len(raw_values) != total_nt:
|
|
262
|
+
raise ConfigurationError(
|
|
263
|
+
"Pair-probability matrix must be square.",
|
|
264
|
+
code="INVALID_PAIR_PROBABILITY_MATRIX",
|
|
265
|
+
)
|
|
266
|
+
values: list[float] = []
|
|
267
|
+
for value in raw_values:
|
|
268
|
+
if (
|
|
269
|
+
isinstance(value, bool)
|
|
270
|
+
or not isinstance(value, (int, float))
|
|
271
|
+
or not math.isfinite(value)
|
|
272
|
+
or not 0.0 <= value <= 1.0
|
|
273
|
+
):
|
|
274
|
+
raise ConfigurationError(
|
|
275
|
+
"Pair probabilities must be finite values in [0, 1].",
|
|
276
|
+
code="INVALID_PAIR_PROBABILITY_MATRIX",
|
|
277
|
+
)
|
|
278
|
+
values.append(float(value))
|
|
279
|
+
if not math.isclose(math.fsum(values), 1.0, abs_tol=1e-5):
|
|
280
|
+
raise ConfigurationError(
|
|
281
|
+
"Each dense pair-probability row must sum to one including its diagonal.",
|
|
282
|
+
code="INVALID_PAIR_PROBABILITY_ROW_SUM",
|
|
283
|
+
)
|
|
284
|
+
rows.append(tuple(values))
|
|
285
|
+
for first in range(total_nt):
|
|
286
|
+
for second in range(first + 1, total_nt):
|
|
287
|
+
if not math.isclose(rows[first][second], rows[second][first], abs_tol=1e-5):
|
|
288
|
+
raise ConfigurationError(
|
|
289
|
+
"Pair-probability matrix must be symmetric.",
|
|
290
|
+
code="ASYMMETRIC_PAIR_PROBABILITY_MATRIX",
|
|
291
|
+
)
|
|
292
|
+
unpaired = tuple(rows[index][index] for index in range(total_nt))
|
|
293
|
+
pairing = tuple(math.fsum(rows[index]) - rows[index][index] for index in range(total_nt))
|
|
294
|
+
off_diagonal = tuple(
|
|
295
|
+
tuple(0.0 if row_index == column_index else value for column_index, value in enumerate(row))
|
|
296
|
+
for row_index, row in enumerate(rows)
|
|
297
|
+
)
|
|
298
|
+
windows: list[AccessibilityWindow] = []
|
|
299
|
+
offset = 0
|
|
300
|
+
for sequence in symbols:
|
|
301
|
+
if len(sequence) >= accessibility_window_size:
|
|
302
|
+
for local_start in range(len(sequence) - accessibility_window_size + 1):
|
|
303
|
+
start = offset + local_start
|
|
304
|
+
end = start + accessibility_window_size
|
|
305
|
+
windows.append(
|
|
306
|
+
AccessibilityWindow(
|
|
307
|
+
start=start,
|
|
308
|
+
end=end,
|
|
309
|
+
mean_unpaired_probability=math.fsum(unpaired[start:end])
|
|
310
|
+
/ accessibility_window_size,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
offset += len(sequence)
|
|
314
|
+
most_accessible = (
|
|
315
|
+
None
|
|
316
|
+
if not windows
|
|
317
|
+
else max(windows, key=lambda item: (item.mean_unpaired_probability, -item.start)).start
|
|
318
|
+
)
|
|
319
|
+
return PairProbabilityResult(
|
|
320
|
+
strands_5to3=symbols,
|
|
321
|
+
pair_probabilities=off_diagonal,
|
|
322
|
+
pairing_probabilities_by_base=pairing,
|
|
323
|
+
unpaired_probabilities_by_base=unpaired,
|
|
324
|
+
accessibility_window_size=accessibility_window_size,
|
|
325
|
+
accessibility_windows=tuple(windows),
|
|
326
|
+
most_accessible_window_start=most_accessible,
|
|
327
|
+
method="nupack-dense-pair-matrix-derived-marginals-v1",
|
|
328
|
+
applicability=(
|
|
329
|
+
"Window accessibility is the arithmetic mean of marginal unpaired probabilities; "
|
|
330
|
+
"it is not the joint probability that every base in the window is unpaired."
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def ensemble_defect_from_probabilities(
|
|
336
|
+
target: SecondaryStructureSummary,
|
|
337
|
+
probabilities: PairProbabilityResult,
|
|
338
|
+
) -> float:
|
|
339
|
+
"""Return the normalized expected number of incorrectly paired nucleotides."""
|
|
340
|
+
|
|
341
|
+
if not isinstance(target, SecondaryStructureSummary) or not isinstance(
|
|
342
|
+
probabilities, PairProbabilityResult
|
|
343
|
+
):
|
|
344
|
+
raise ConfigurationError(
|
|
345
|
+
"target and probabilities have invalid types.",
|
|
346
|
+
code="INVALID_ENSEMBLE_DEFECT_INPUT",
|
|
347
|
+
)
|
|
348
|
+
total_nt = sum(map(len, target.strands_5to3))
|
|
349
|
+
if probabilities.strands_5to3 != target.strands_5to3:
|
|
350
|
+
raise ConfigurationError(
|
|
351
|
+
"Target and probability results must describe identical strands.",
|
|
352
|
+
code="ENSEMBLE_DEFECT_STRAND_MISMATCH",
|
|
353
|
+
)
|
|
354
|
+
if len(probabilities.unpaired_probabilities_by_base) != total_nt:
|
|
355
|
+
raise ConfigurationError(
|
|
356
|
+
"Target and probability matrix lengths differ.",
|
|
357
|
+
code="ENSEMBLE_DEFECT_LENGTH_MISMATCH",
|
|
358
|
+
)
|
|
359
|
+
target_partner = {index: index for index in range(total_nt)}
|
|
360
|
+
for pair in target.base_pairs:
|
|
361
|
+
target_partner[pair.first_global_index] = pair.second_global_index
|
|
362
|
+
target_partner[pair.second_global_index] = pair.first_global_index
|
|
363
|
+
correct_probabilities = []
|
|
364
|
+
for index, partner in target_partner.items():
|
|
365
|
+
if partner == index:
|
|
366
|
+
correct_probabilities.append(probabilities.unpaired_probabilities_by_base[index])
|
|
367
|
+
else:
|
|
368
|
+
correct_probabilities.append(probabilities.pair_probabilities[index][partner])
|
|
369
|
+
return math.fsum(1.0 - value for value in correct_probabilities) / total_nt
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def target_structure_probability(
|
|
373
|
+
target_free_energy_kcal_per_mol: float,
|
|
374
|
+
ensemble_free_energy_kcal_per_mol: float,
|
|
375
|
+
*,
|
|
376
|
+
temperature_celsius: float = 37.0,
|
|
377
|
+
) -> float:
|
|
378
|
+
"""Calculate P(target) from target and ensemble standard free energies."""
|
|
379
|
+
|
|
380
|
+
values = (target_free_energy_kcal_per_mol, ensemble_free_energy_kcal_per_mol)
|
|
381
|
+
if any(
|
|
382
|
+
isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value)
|
|
383
|
+
for value in values
|
|
384
|
+
):
|
|
385
|
+
raise ConfigurationError(
|
|
386
|
+
"Free energies must be finite numeric data.",
|
|
387
|
+
code="INVALID_TARGET_STRUCTURE_ENERGY",
|
|
388
|
+
)
|
|
389
|
+
if (
|
|
390
|
+
isinstance(temperature_celsius, bool)
|
|
391
|
+
or not isinstance(temperature_celsius, (int, float))
|
|
392
|
+
or not math.isfinite(temperature_celsius)
|
|
393
|
+
or not 0.0 <= temperature_celsius <= 100.0
|
|
394
|
+
):
|
|
395
|
+
raise ConfigurationError(
|
|
396
|
+
"temperature_celsius must be in [0, 100].",
|
|
397
|
+
code="INVALID_TARGET_STRUCTURE_TEMPERATURE",
|
|
398
|
+
)
|
|
399
|
+
exponent = -(
|
|
400
|
+
float(target_free_energy_kcal_per_mol) - float(ensemble_free_energy_kcal_per_mol)
|
|
401
|
+
) / (0.00198720425864083 * (float(temperature_celsius) + 273.15))
|
|
402
|
+
return min(1.0, max(0.0, math.exp(min(0.0, exponent))))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
__all__ = [
|
|
406
|
+
"analyze_dot_bracket",
|
|
407
|
+
"ensemble_defect_from_probabilities",
|
|
408
|
+
"pair_probability_metrics",
|
|
409
|
+
"target_structure_probability",
|
|
410
|
+
]
|