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