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,917 @@
|
|
|
1
|
+
"""Unified calculation of the fixed 240-field DNA descriptor schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import product
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
from dnakit.core import (
|
|
12
|
+
Citation,
|
|
13
|
+
DNASequence,
|
|
14
|
+
ExecutionMode,
|
|
15
|
+
Gap,
|
|
16
|
+
ImplementationInfo,
|
|
17
|
+
ImplementationLabel,
|
|
18
|
+
OriginClass,
|
|
19
|
+
Provenance,
|
|
20
|
+
ReferenceInfo,
|
|
21
|
+
Topology,
|
|
22
|
+
)
|
|
23
|
+
from dnakit.core._json import FrozenDict, JSONScalar
|
|
24
|
+
from dnakit.descriptors._dinucleotide import (
|
|
25
|
+
DINUCLEOTIDE_PROPERTY_SPECS,
|
|
26
|
+
DINUCLEOTIDE_TABLE_SCHEMA_VERSION,
|
|
27
|
+
DinucleotidePropertyTable,
|
|
28
|
+
)
|
|
29
|
+
from dnakit.descriptors._shared import (
|
|
30
|
+
DescriptorAmbiguityPolicy,
|
|
31
|
+
SequenceInput,
|
|
32
|
+
canonical_runs,
|
|
33
|
+
coerce_ambiguity_policy,
|
|
34
|
+
fragments,
|
|
35
|
+
iter_kmers,
|
|
36
|
+
reject_ambiguity,
|
|
37
|
+
sequence_and_id,
|
|
38
|
+
)
|
|
39
|
+
from dnakit.descriptors.codon import codon_statistics
|
|
40
|
+
from dnakit.descriptors.complexity import exact_repeat_fraction
|
|
41
|
+
from dnakit.descriptors.results import AllDescriptorsResult
|
|
42
|
+
from dnakit.descriptors.schema import (
|
|
43
|
+
DESCRIPTOR_NAMES_V1,
|
|
44
|
+
DESCRIPTOR_SCHEMA_VERSION,
|
|
45
|
+
)
|
|
46
|
+
from dnakit.exceptions import ConfigurationError, DNAKitError
|
|
47
|
+
from dnakit.thermodynamics import (
|
|
48
|
+
ThermodynamicConditions,
|
|
49
|
+
extinction_coefficient_260nm,
|
|
50
|
+
melting_temperature,
|
|
51
|
+
molecular_weight,
|
|
52
|
+
nearest_neighbor,
|
|
53
|
+
stacking_interactions,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_COMPLEMENT = str.maketrans("ACGT", "TGCA")
|
|
57
|
+
_STANDARD_START = "ATG"
|
|
58
|
+
_STANDARD_STOPS = frozenset({"TAA", "TAG", "TGA"})
|
|
59
|
+
_MAX_LZ76_LENGTH = 10_000
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class _ValueBuilder:
|
|
64
|
+
values: dict[str, JSONScalar]
|
|
65
|
+
reasons: dict[str, str]
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
self.values = {}
|
|
69
|
+
self.reasons = {}
|
|
70
|
+
|
|
71
|
+
def add(self, name: str, value: JSONScalar, reason: str | None = None) -> None:
|
|
72
|
+
if name in self.values:
|
|
73
|
+
raise AssertionError(f"Descriptor {name!r} was assigned more than once.")
|
|
74
|
+
if value is None:
|
|
75
|
+
if reason is None or not reason.strip():
|
|
76
|
+
raise AssertionError(f"Unavailable descriptor {name!r} requires a reason.")
|
|
77
|
+
self.reasons[name] = reason
|
|
78
|
+
elif reason is not None:
|
|
79
|
+
raise AssertionError(f"Available descriptor {name!r} cannot have a reason.")
|
|
80
|
+
self.values[name] = value
|
|
81
|
+
|
|
82
|
+
def ordered(self) -> tuple[dict[str, JSONScalar], dict[str, str]]:
|
|
83
|
+
missing = set(DESCRIPTOR_NAMES_V1) - set(self.values)
|
|
84
|
+
extra = set(self.values) - set(DESCRIPTOR_NAMES_V1)
|
|
85
|
+
if missing or extra:
|
|
86
|
+
raise AssertionError(
|
|
87
|
+
f"All-descriptor assignments differ from schema: missing={missing}, extra={extra}."
|
|
88
|
+
)
|
|
89
|
+
return (
|
|
90
|
+
{name: self.values[name] for name in DESCRIPTOR_NAMES_V1},
|
|
91
|
+
{name: self.reasons[name] for name in DESCRIPTOR_NAMES_V1 if name in self.reasons},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ratio(numerator: int | float, denominator: int | float) -> float | None:
|
|
96
|
+
return numerator / denominator if denominator else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _entropy(counts: Counter[str], total: int) -> float | None:
|
|
100
|
+
if total == 0:
|
|
101
|
+
return None
|
|
102
|
+
return -math.fsum(
|
|
103
|
+
(count / total) * math.log2(count / total) for count in counts.values() if count
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _canonical_run_locations(sequence: DNASequence) -> tuple[tuple[str, int], ...]:
|
|
108
|
+
located: list[tuple[str, int]] = []
|
|
109
|
+
symbol_offset = 0
|
|
110
|
+
for part in sequence.parts:
|
|
111
|
+
if not isinstance(part, str):
|
|
112
|
+
continue
|
|
113
|
+
start = 0
|
|
114
|
+
for index, symbol in enumerate(part):
|
|
115
|
+
if symbol not in "ACGT":
|
|
116
|
+
if start < index:
|
|
117
|
+
located.append((part[start:index], symbol_offset + start))
|
|
118
|
+
start = index + 1
|
|
119
|
+
if start < len(part):
|
|
120
|
+
located.append((part[start:], symbol_offset + start))
|
|
121
|
+
symbol_offset += len(part)
|
|
122
|
+
return tuple(located)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cumulative_range(text: str, positive: str, negative: str) -> tuple[int, int, int]:
|
|
126
|
+
score = 0
|
|
127
|
+
minimum = 0
|
|
128
|
+
maximum = 0
|
|
129
|
+
for symbol in text:
|
|
130
|
+
if symbol == positive:
|
|
131
|
+
score += 1
|
|
132
|
+
elif symbol == negative:
|
|
133
|
+
score -= 1
|
|
134
|
+
minimum = min(minimum, score)
|
|
135
|
+
maximum = max(maximum, score)
|
|
136
|
+
return maximum, minimum, maximum - minimum
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _lz76_complexity(text: str) -> int:
|
|
140
|
+
"""Return exhaustive-history LZ76 phrase count using the Kaspar-Schuster scan."""
|
|
141
|
+
|
|
142
|
+
length = len(text)
|
|
143
|
+
if length <= 1:
|
|
144
|
+
return length
|
|
145
|
+
complexity = 1
|
|
146
|
+
prefix_index = 0
|
|
147
|
+
phrase_start = 1
|
|
148
|
+
match_length = 1
|
|
149
|
+
longest_match = 1
|
|
150
|
+
while True:
|
|
151
|
+
if text[prefix_index + match_length - 1] == text[phrase_start + match_length - 1]:
|
|
152
|
+
match_length += 1
|
|
153
|
+
if phrase_start + match_length > length:
|
|
154
|
+
complexity += 1
|
|
155
|
+
break
|
|
156
|
+
continue
|
|
157
|
+
longest_match = max(longest_match, match_length)
|
|
158
|
+
prefix_index += 1
|
|
159
|
+
if prefix_index == phrase_start:
|
|
160
|
+
complexity += 1
|
|
161
|
+
phrase_start += longest_match
|
|
162
|
+
if phrase_start + 1 > length:
|
|
163
|
+
break
|
|
164
|
+
prefix_index = 0
|
|
165
|
+
match_length = 1
|
|
166
|
+
longest_match = 1
|
|
167
|
+
else:
|
|
168
|
+
match_length = 1
|
|
169
|
+
return complexity
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _union_length(intervals: list[tuple[int, int]]) -> int:
|
|
173
|
+
if not intervals:
|
|
174
|
+
return 0
|
|
175
|
+
ordered = sorted(intervals)
|
|
176
|
+
total = 0
|
|
177
|
+
current_start, current_end = ordered[0]
|
|
178
|
+
for start, end in ordered[1:]:
|
|
179
|
+
if start <= current_end:
|
|
180
|
+
current_end = max(current_end, end)
|
|
181
|
+
else:
|
|
182
|
+
total += current_end - current_start
|
|
183
|
+
current_start, current_end = start, end
|
|
184
|
+
return total + current_end - current_start
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _six_frame_orfs(
|
|
188
|
+
runs: tuple[tuple[str, int], ...],
|
|
189
|
+
) -> tuple[int, int, int, int, list[tuple[int, int]]]:
|
|
190
|
+
forward_count = 0
|
|
191
|
+
reverse_count = 0
|
|
192
|
+
longest = 0
|
|
193
|
+
covered: list[tuple[int, int]] = []
|
|
194
|
+
for text, symbol_offset in runs:
|
|
195
|
+
run_length = len(text)
|
|
196
|
+
for reverse in (False, True):
|
|
197
|
+
oriented = text.translate(_COMPLEMENT)[::-1] if reverse else text
|
|
198
|
+
for frame in range(3):
|
|
199
|
+
open_starts: list[int] = []
|
|
200
|
+
coding_end = frame + ((run_length - frame) // 3) * 3
|
|
201
|
+
for position in range(frame, coding_end, 3):
|
|
202
|
+
codon = oriented[position : position + 3]
|
|
203
|
+
if codon == _STANDARD_START:
|
|
204
|
+
open_starts.append(position)
|
|
205
|
+
if codon not in _STANDARD_STOPS:
|
|
206
|
+
continue
|
|
207
|
+
if open_starts:
|
|
208
|
+
count = len(open_starts)
|
|
209
|
+
if reverse:
|
|
210
|
+
reverse_count += count
|
|
211
|
+
else:
|
|
212
|
+
forward_count += count
|
|
213
|
+
end = position + 3
|
|
214
|
+
earliest = open_starts[0]
|
|
215
|
+
longest = max(longest, end - earliest)
|
|
216
|
+
if reverse:
|
|
217
|
+
covered.append(
|
|
218
|
+
(
|
|
219
|
+
symbol_offset + run_length - end,
|
|
220
|
+
symbol_offset + run_length - earliest,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
covered.append((symbol_offset + earliest, symbol_offset + end))
|
|
225
|
+
open_starts.clear()
|
|
226
|
+
return forward_count + reverse_count, forward_count, reverse_count, longest, covered
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _exception_reason(error: DNAKitError) -> str:
|
|
230
|
+
return f"{error.code}: {error.message}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _provenance(table: DinucleotidePropertyTable | None) -> Provenance:
|
|
234
|
+
return Provenance(
|
|
235
|
+
implementation=ImplementationInfo(
|
|
236
|
+
label=ImplementationLabel.REIMPLEMENTATION,
|
|
237
|
+
execution_mode=ExecutionMode.INTERNAL,
|
|
238
|
+
origin_class=OriginClass.PUBLISHED_ALGORITHM,
|
|
239
|
+
license_expression="MIT",
|
|
240
|
+
citations=(
|
|
241
|
+
Citation(
|
|
242
|
+
"shannon1948",
|
|
243
|
+
title="A Mathematical Theory of Communication",
|
|
244
|
+
doi="10.1002/j.1538-7305.1948.tb01338.x",
|
|
245
|
+
),
|
|
246
|
+
Citation(
|
|
247
|
+
"lempel-ziv1976",
|
|
248
|
+
title="On the Complexity of Finite Sequences",
|
|
249
|
+
doi="10.1109/TIT.1976.1055501",
|
|
250
|
+
),
|
|
251
|
+
Citation(
|
|
252
|
+
"santalucia1998",
|
|
253
|
+
title="A unified view of DNA nearest-neighbor thermodynamics",
|
|
254
|
+
doi="10.1073/pnas.95.4.1460",
|
|
255
|
+
),
|
|
256
|
+
Citation(
|
|
257
|
+
"warshaw-tinoco1966",
|
|
258
|
+
title="Optical properties of sixteen dinucleoside phosphates",
|
|
259
|
+
doi="10.1016/0022-2836(66)90115-X",
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
reference=(
|
|
264
|
+
None
|
|
265
|
+
if table is None
|
|
266
|
+
else ReferenceInfo(
|
|
267
|
+
table.name,
|
|
268
|
+
version=table.version,
|
|
269
|
+
checksum=table.sha256,
|
|
270
|
+
filters={
|
|
271
|
+
"source_declared_by_user": table.source,
|
|
272
|
+
"schema_version": DINUCLEOTIDE_TABLE_SCHEMA_VERSION,
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def all_descriptors(
|
|
280
|
+
value: SequenceInput,
|
|
281
|
+
*,
|
|
282
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.IGNORE,
|
|
283
|
+
conditions: ThermodynamicConditions | None = None,
|
|
284
|
+
dinucleotide_property_table: DinucleotidePropertyTable | None = None,
|
|
285
|
+
) -> AllDescriptorsResult:
|
|
286
|
+
"""Calculate all 240 fields in ``descriptor_schema_v1``.
|
|
287
|
+
|
|
288
|
+
Exact words never cross an ambiguity symbol or explicit gap. A field outside
|
|
289
|
+
its mathematical or model domain is returned as ``None`` and receives a
|
|
290
|
+
stable entry in ``unavailable_reasons``; it is never silently replaced by 0.
|
|
291
|
+
The final 60 legacy ``diprodb_*`` fields require an explicitly loaded
|
|
292
|
+
user-supplied table because DNAKit bundles no third-party coefficients.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
296
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
297
|
+
reject_ambiguity(sequence, policy)
|
|
298
|
+
resolved_conditions = ThermodynamicConditions() if conditions is None else conditions
|
|
299
|
+
if not isinstance(resolved_conditions, ThermodynamicConditions):
|
|
300
|
+
raise ConfigurationError(
|
|
301
|
+
"conditions must be ThermodynamicConditions or None.",
|
|
302
|
+
code="INVALID_ALL_DESCRIPTOR_CONDITIONS",
|
|
303
|
+
)
|
|
304
|
+
if dinucleotide_property_table is not None and not isinstance(
|
|
305
|
+
dinucleotide_property_table, DinucleotidePropertyTable
|
|
306
|
+
):
|
|
307
|
+
raise ConfigurationError(
|
|
308
|
+
"dinucleotide_property_table must be DinucleotidePropertyTable or None.",
|
|
309
|
+
code="INVALID_DINUCLEOTIDE_PROPERTY_TABLE",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
builder = _ValueBuilder()
|
|
313
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
314
|
+
run_texts = tuple(
|
|
315
|
+
run
|
|
316
|
+
for fragment in fragments(sequence, cross_gaps=False)
|
|
317
|
+
for run in canonical_runs(fragment)
|
|
318
|
+
)
|
|
319
|
+
located_runs = _canonical_run_locations(sequence)
|
|
320
|
+
canonical_text = "".join(run_texts)
|
|
321
|
+
counts = Counter(canonical_text)
|
|
322
|
+
canonical_count = len(canonical_text)
|
|
323
|
+
symbol_length = sequence.symbol_length
|
|
324
|
+
unknown_gap_count = sum(gap.length is None for gap in gaps)
|
|
325
|
+
known_gap_nt = sum(gap.length or 0 for gap in gaps)
|
|
326
|
+
|
|
327
|
+
builder.add("symbol_length", symbol_length)
|
|
328
|
+
builder.add(
|
|
329
|
+
"coordinate_span",
|
|
330
|
+
sequence.coordinate_span,
|
|
331
|
+
"undefined because at least one explicit gap has unknown length"
|
|
332
|
+
if sequence.coordinate_span is None
|
|
333
|
+
else None,
|
|
334
|
+
)
|
|
335
|
+
builder.add("canonical_base_count", canonical_count)
|
|
336
|
+
builder.add("ambiguity_symbol_count", sequence.ambiguity_count)
|
|
337
|
+
builder.add("gap_object_count", len(gaps))
|
|
338
|
+
builder.add("known_gap_nt", known_gap_nt)
|
|
339
|
+
builder.add("unknown_gap_count", unknown_gap_count)
|
|
340
|
+
builder.add(
|
|
341
|
+
"canonical_symbol_fraction",
|
|
342
|
+
_ratio(canonical_count, symbol_length),
|
|
343
|
+
"requires at least one nucleotide symbol" if symbol_length == 0 else None,
|
|
344
|
+
)
|
|
345
|
+
builder.add(
|
|
346
|
+
"ambiguity_symbol_fraction",
|
|
347
|
+
_ratio(sequence.ambiguity_count, symbol_length),
|
|
348
|
+
"requires at least one nucleotide symbol" if symbol_length == 0 else None,
|
|
349
|
+
)
|
|
350
|
+
coordinate_span = sequence.coordinate_span
|
|
351
|
+
builder.add(
|
|
352
|
+
"known_gap_fraction",
|
|
353
|
+
_ratio(known_gap_nt, coordinate_span or 0) if coordinate_span is not None else None,
|
|
354
|
+
(
|
|
355
|
+
"requires a positive, fully known coordinate span"
|
|
356
|
+
if coordinate_span in {None, 0}
|
|
357
|
+
else None
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
builder.add("canonical_run_count", len(run_texts))
|
|
361
|
+
builder.add("longest_canonical_run_nt", max(map(len, run_texts), default=0))
|
|
362
|
+
|
|
363
|
+
group_counts = {
|
|
364
|
+
"purine": counts["A"] + counts["G"],
|
|
365
|
+
"pyrimidine": counts["C"] + counts["T"],
|
|
366
|
+
"amino": counts["A"] + counts["C"],
|
|
367
|
+
"keto": counts["G"] + counts["T"],
|
|
368
|
+
"weak": counts["A"] + counts["T"],
|
|
369
|
+
"strong": counts["C"] + counts["G"],
|
|
370
|
+
}
|
|
371
|
+
empty_base_reason = "requires at least one canonical A/C/G/T base"
|
|
372
|
+
for group in ("purine", "pyrimidine", "amino", "keto", "weak", "strong"):
|
|
373
|
+
builder.add(f"{group}_count", group_counts[group])
|
|
374
|
+
builder.add(
|
|
375
|
+
f"{group}_fraction",
|
|
376
|
+
_ratio(group_counts[group], canonical_count),
|
|
377
|
+
empty_base_reason if canonical_count == 0 else None,
|
|
378
|
+
)
|
|
379
|
+
for name, left, right in (
|
|
380
|
+
("purine_pyrimidine_skew", "purine", "pyrimidine"),
|
|
381
|
+
("amino_keto_skew", "amino", "keto"),
|
|
382
|
+
("weak_strong_skew", "weak", "strong"),
|
|
383
|
+
):
|
|
384
|
+
denominator = group_counts[left] + group_counts[right]
|
|
385
|
+
builder.add(
|
|
386
|
+
name,
|
|
387
|
+
_ratio(group_counts[left] - group_counts[right], denominator),
|
|
388
|
+
empty_base_reason if denominator == 0 else None,
|
|
389
|
+
)
|
|
390
|
+
builder.add(
|
|
391
|
+
"gc_at_ratio",
|
|
392
|
+
_ratio(group_counts["strong"], group_counts["weak"]),
|
|
393
|
+
"requires at least one A or T base" if group_counts["weak"] == 0 else None,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
kmer_counts: dict[int, Counter[str]] = {}
|
|
397
|
+
kmer_totals: dict[int, int] = {}
|
|
398
|
+
for k in range(1, 7):
|
|
399
|
+
observed = Counter(iter_kmers(sequence, k=k, overlapping=True, cross_gaps=False))
|
|
400
|
+
kmer_counts[k] = observed
|
|
401
|
+
kmer_totals[k] = sum(observed.values())
|
|
402
|
+
if k <= 3:
|
|
403
|
+
for symbols in product("ACGT", repeat=k):
|
|
404
|
+
word = "".join(symbols)
|
|
405
|
+
builder.add(
|
|
406
|
+
f"k{k}_{word}_frequency",
|
|
407
|
+
_ratio(observed[word], kmer_totals[k]),
|
|
408
|
+
f"requires at least one valid canonical {k}-mer position"
|
|
409
|
+
if kmer_totals[k] == 0
|
|
410
|
+
else None,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
gc_denominator = counts["G"] + counts["C"]
|
|
414
|
+
at_denominator = counts["A"] + counts["T"]
|
|
415
|
+
builder.add(
|
|
416
|
+
"gc_skew",
|
|
417
|
+
_ratio(counts["G"] - counts["C"], gc_denominator),
|
|
418
|
+
"requires at least one G or C base" if gc_denominator == 0 else None,
|
|
419
|
+
)
|
|
420
|
+
builder.add(
|
|
421
|
+
"at_skew",
|
|
422
|
+
_ratio(counts["A"] - counts["T"], at_denominator),
|
|
423
|
+
"requires at least one A or T base" if at_denominator == 0 else None,
|
|
424
|
+
)
|
|
425
|
+
dinucleotide_total = kmer_totals[2]
|
|
426
|
+
cpg_count = kmer_counts[2]["CG"]
|
|
427
|
+
gpc_count = kmer_counts[2]["GC"]
|
|
428
|
+
builder.add("cpg_count", cpg_count)
|
|
429
|
+
builder.add(
|
|
430
|
+
"cpg_density",
|
|
431
|
+
_ratio(cpg_count, dinucleotide_total),
|
|
432
|
+
"requires at least one valid canonical dinucleotide position"
|
|
433
|
+
if dinucleotide_total == 0
|
|
434
|
+
else None,
|
|
435
|
+
)
|
|
436
|
+
builder.add(
|
|
437
|
+
"cpg_observed_expected",
|
|
438
|
+
_ratio(cpg_count * canonical_count, counts["C"] * counts["G"]),
|
|
439
|
+
"requires at least one C and one G base" if counts["C"] == 0 or counts["G"] == 0 else None,
|
|
440
|
+
)
|
|
441
|
+
builder.add("gpc_count", gpc_count)
|
|
442
|
+
builder.add(
|
|
443
|
+
"gpc_density",
|
|
444
|
+
_ratio(gpc_count, dinucleotide_total),
|
|
445
|
+
"requires at least one valid canonical dinucleotide position"
|
|
446
|
+
if dinucleotide_total == 0
|
|
447
|
+
else None,
|
|
448
|
+
)
|
|
449
|
+
builder.add(
|
|
450
|
+
"cpg_gpc_ratio",
|
|
451
|
+
_ratio(cpg_count, gpc_count),
|
|
452
|
+
"requires at least one GC dinucleotide" if gpc_count == 0 else None,
|
|
453
|
+
)
|
|
454
|
+
if canonical_count:
|
|
455
|
+
gc_cumulative = _cumulative_range(canonical_text, "G", "C")
|
|
456
|
+
at_cumulative = _cumulative_range(canonical_text, "A", "T")
|
|
457
|
+
for name, value_item in zip(
|
|
458
|
+
("cumulative_gc_skew_max", "cumulative_gc_skew_min", "cumulative_gc_skew_range"),
|
|
459
|
+
gc_cumulative,
|
|
460
|
+
strict=True,
|
|
461
|
+
):
|
|
462
|
+
builder.add(name, value_item)
|
|
463
|
+
for name, value_item in zip(
|
|
464
|
+
("cumulative_at_skew_max", "cumulative_at_skew_min", "cumulative_at_skew_range"),
|
|
465
|
+
at_cumulative,
|
|
466
|
+
strict=True,
|
|
467
|
+
):
|
|
468
|
+
builder.add(name, value_item)
|
|
469
|
+
else:
|
|
470
|
+
for name in (
|
|
471
|
+
"cumulative_gc_skew_max",
|
|
472
|
+
"cumulative_gc_skew_min",
|
|
473
|
+
"cumulative_gc_skew_range",
|
|
474
|
+
"cumulative_at_skew_max",
|
|
475
|
+
"cumulative_at_skew_min",
|
|
476
|
+
"cumulative_at_skew_range",
|
|
477
|
+
):
|
|
478
|
+
builder.add(name, None, empty_base_reason)
|
|
479
|
+
if dinucleotide_total:
|
|
480
|
+
reverse_complement_frequency = 0.0
|
|
481
|
+
for word in ("".join(item) for item in product("ACGT", repeat=2)):
|
|
482
|
+
reverse_complement = word.translate(_COMPLEMENT)[::-1]
|
|
483
|
+
reverse_complement_frequency += abs(
|
|
484
|
+
kmer_counts[2][word] / dinucleotide_total
|
|
485
|
+
- kmer_counts[2][reverse_complement] / dinucleotide_total
|
|
486
|
+
)
|
|
487
|
+
builder.add("dinucleotide_rc_total_variation", 0.5 * reverse_complement_frequency)
|
|
488
|
+
else:
|
|
489
|
+
builder.add(
|
|
490
|
+
"dinucleotide_rc_total_variation",
|
|
491
|
+
None,
|
|
492
|
+
"requires at least one valid canonical dinucleotide position",
|
|
493
|
+
)
|
|
494
|
+
builder.add(
|
|
495
|
+
"mono_chargaff_l1_distance",
|
|
496
|
+
(
|
|
497
|
+
abs(counts["A"] / canonical_count - counts["T"] / canonical_count)
|
|
498
|
+
+ abs(counts["C"] / canonical_count - counts["G"] / canonical_count)
|
|
499
|
+
if canonical_count
|
|
500
|
+
else None
|
|
501
|
+
),
|
|
502
|
+
empty_base_reason if canonical_count == 0 else None,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
entropies: dict[int, float | None] = {}
|
|
506
|
+
for k in range(1, 4):
|
|
507
|
+
entropy = _entropy(kmer_counts[k], kmer_totals[k])
|
|
508
|
+
entropies[k] = entropy
|
|
509
|
+
builder.add(
|
|
510
|
+
f"shannon_entropy_k{k}_bits",
|
|
511
|
+
entropy,
|
|
512
|
+
f"requires at least one valid canonical {k}-mer position" if entropy is None else None,
|
|
513
|
+
)
|
|
514
|
+
for k in range(1, 4):
|
|
515
|
+
entropy = entropies[k]
|
|
516
|
+
builder.add(
|
|
517
|
+
f"normalized_entropy_k{k}",
|
|
518
|
+
entropy / (2 * k) if entropy is not None else None,
|
|
519
|
+
f"requires at least one valid canonical {k}-mer position" if entropy is None else None,
|
|
520
|
+
)
|
|
521
|
+
linguistic: dict[int, float | None] = {}
|
|
522
|
+
for k in range(1, 7):
|
|
523
|
+
possible = min(4**k, kmer_totals[k])
|
|
524
|
+
linguistic[k] = len(kmer_counts[k]) / possible if possible else None
|
|
525
|
+
if k >= 2:
|
|
526
|
+
builder.add(
|
|
527
|
+
f"linguistic_complexity_k{k}",
|
|
528
|
+
linguistic[k],
|
|
529
|
+
f"requires at least one valid canonical {k}-mer position"
|
|
530
|
+
if possible == 0
|
|
531
|
+
else None,
|
|
532
|
+
)
|
|
533
|
+
defined_linguistic = tuple(item for item in linguistic.values() if item is not None)
|
|
534
|
+
builder.add(
|
|
535
|
+
"linguistic_complexity_product_k1_k6",
|
|
536
|
+
math.prod(defined_linguistic) if defined_linguistic else None,
|
|
537
|
+
"requires at least one valid canonical k-mer position" if not defined_linguistic else None,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
lz_reason: str | None = None
|
|
541
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
542
|
+
lz_reason = "LZ76 descriptor requires a linear sequence"
|
|
543
|
+
elif gaps:
|
|
544
|
+
lz_reason = "LZ76 descriptor requires an ungapped sequence"
|
|
545
|
+
elif sequence.ambiguity_count:
|
|
546
|
+
lz_reason = "LZ76 descriptor requires only canonical A/C/G/T symbols"
|
|
547
|
+
elif canonical_count == 0:
|
|
548
|
+
lz_reason = empty_base_reason
|
|
549
|
+
elif canonical_count > _MAX_LZ76_LENGTH:
|
|
550
|
+
lz_reason = f"LZ76 descriptor is limited to {_MAX_LZ76_LENGTH} nt"
|
|
551
|
+
if lz_reason is None:
|
|
552
|
+
lz_value = _lz76_complexity(canonical_text)
|
|
553
|
+
builder.add("lz76_complexity", lz_value)
|
|
554
|
+
builder.add(
|
|
555
|
+
"normalized_lz76_complexity",
|
|
556
|
+
lz_value * math.log(canonical_count, 4) / canonical_count
|
|
557
|
+
if canonical_count > 1
|
|
558
|
+
else None,
|
|
559
|
+
"normalized LZ76 requires at least two canonical bases"
|
|
560
|
+
if canonical_count <= 1
|
|
561
|
+
else None,
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
builder.add("lz76_complexity", None, lz_reason)
|
|
565
|
+
builder.add("normalized_lz76_complexity", None, lz_reason)
|
|
566
|
+
|
|
567
|
+
longest_by_base = {base: 0 for base in "ACGT"}
|
|
568
|
+
for run in run_texts:
|
|
569
|
+
start = 0
|
|
570
|
+
for index in range(1, len(run) + 1):
|
|
571
|
+
if index == len(run) or run[index] != run[start]:
|
|
572
|
+
longest_by_base[run[start]] = max(longest_by_base[run[start]], index - start)
|
|
573
|
+
start = index
|
|
574
|
+
builder.add("longest_homopolymer_nt", max(longest_by_base.values(), default=0))
|
|
575
|
+
for base in "ACGT":
|
|
576
|
+
builder.add(f"longest_homopolymer_{base.lower()}_nt", longest_by_base[base])
|
|
577
|
+
if canonical_count == 0:
|
|
578
|
+
builder.add("exact_tandem_repeat_coverage_fraction", None, empty_base_reason)
|
|
579
|
+
else:
|
|
580
|
+
try:
|
|
581
|
+
repeat = exact_repeat_fraction(
|
|
582
|
+
value,
|
|
583
|
+
ambiguity_policy=policy,
|
|
584
|
+
cross_gaps=False,
|
|
585
|
+
)
|
|
586
|
+
builder.add("exact_tandem_repeat_coverage_fraction", repeat.repeat_fraction)
|
|
587
|
+
except DNAKitError as error:
|
|
588
|
+
builder.add(
|
|
589
|
+
"exact_tandem_repeat_coverage_fraction",
|
|
590
|
+
None,
|
|
591
|
+
_exception_reason(error),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
codons = codon_statistics(
|
|
595
|
+
value,
|
|
596
|
+
frame=0,
|
|
597
|
+
genetic_code=1,
|
|
598
|
+
ambiguity_policy=policy,
|
|
599
|
+
cross_gaps=False,
|
|
600
|
+
)
|
|
601
|
+
codon_counts = Counter({codon: cast(int, count) for codon, count in codons.counts.items()})
|
|
602
|
+
codon_entropy = _entropy(codon_counts, codons.codon_count)
|
|
603
|
+
builder.add("frame0_codon_count", codons.codon_count)
|
|
604
|
+
builder.add("frame0_unique_codon_count", len(codon_counts))
|
|
605
|
+
builder.add("frame0_start_codon_count", codons.start_count)
|
|
606
|
+
builder.add("frame0_stop_codon_count", codons.stop_count)
|
|
607
|
+
builder.add(
|
|
608
|
+
"frame0_start_codon_fraction",
|
|
609
|
+
codons.start_density,
|
|
610
|
+
"requires at least one valid forward frame-0 codon" if codons.codon_count == 0 else None,
|
|
611
|
+
)
|
|
612
|
+
builder.add(
|
|
613
|
+
"frame0_stop_codon_fraction",
|
|
614
|
+
codons.stop_density,
|
|
615
|
+
"requires at least one valid forward frame-0 codon" if codons.codon_count == 0 else None,
|
|
616
|
+
)
|
|
617
|
+
builder.add(
|
|
618
|
+
"frame0_codon_entropy_bits",
|
|
619
|
+
codon_entropy,
|
|
620
|
+
"requires at least one valid forward frame-0 codon" if codon_entropy is None else None,
|
|
621
|
+
)
|
|
622
|
+
builder.add(
|
|
623
|
+
"frame0_effective_number_of_codons",
|
|
624
|
+
2**codon_entropy if codon_entropy is not None else None,
|
|
625
|
+
"requires at least one valid forward frame-0 codon" if codon_entropy is None else None,
|
|
626
|
+
)
|
|
627
|
+
for position in range(3):
|
|
628
|
+
gc_count = sum(count for codon, count in codon_counts.items() if codon[position] in "GC")
|
|
629
|
+
builder.add(
|
|
630
|
+
f"frame0_gc{position + 1}_fraction",
|
|
631
|
+
_ratio(gc_count, codons.codon_count),
|
|
632
|
+
"requires at least one valid forward frame-0 codon"
|
|
633
|
+
if codons.codon_count == 0
|
|
634
|
+
else None,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
orf_names = (
|
|
638
|
+
"six_frame_complete_orf_count",
|
|
639
|
+
"six_frame_forward_complete_orf_count",
|
|
640
|
+
"six_frame_reverse_complete_orf_count",
|
|
641
|
+
"six_frame_longest_complete_orf_nt",
|
|
642
|
+
"six_frame_complete_orf_coverage_fraction",
|
|
643
|
+
)
|
|
644
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
645
|
+
for name in orf_names:
|
|
646
|
+
builder.add(name, None, "six-frame ORF descriptors require a linear sequence")
|
|
647
|
+
else:
|
|
648
|
+
total_orfs, forward_orfs, reverse_orfs, longest_orf, orf_intervals = _six_frame_orfs(
|
|
649
|
+
located_runs
|
|
650
|
+
)
|
|
651
|
+
builder.add("six_frame_complete_orf_count", total_orfs)
|
|
652
|
+
builder.add("six_frame_forward_complete_orf_count", forward_orfs)
|
|
653
|
+
builder.add("six_frame_reverse_complete_orf_count", reverse_orfs)
|
|
654
|
+
builder.add("six_frame_longest_complete_orf_nt", longest_orf)
|
|
655
|
+
builder.add(
|
|
656
|
+
"six_frame_complete_orf_coverage_fraction",
|
|
657
|
+
_ratio(_union_length(orf_intervals), canonical_count),
|
|
658
|
+
empty_base_reason if canonical_count == 0 else None,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
thermo_sequence: DNASequence | None = None
|
|
662
|
+
thermo_reason: str | None = None
|
|
663
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
664
|
+
thermo_reason = "thermodynamic descriptors require a linear sequence"
|
|
665
|
+
elif gaps:
|
|
666
|
+
thermo_reason = "thermodynamic descriptors require an ungapped sequence"
|
|
667
|
+
elif sequence.ambiguity_count:
|
|
668
|
+
thermo_reason = "thermodynamic descriptors require only canonical A/C/G/T symbols"
|
|
669
|
+
else:
|
|
670
|
+
thermo_sequence = DNASequence(canonical_text, strandedness="single")
|
|
671
|
+
|
|
672
|
+
mass_names = (
|
|
673
|
+
"mw_ss_oh_da",
|
|
674
|
+
"mw_ss_5p_phosphate_da",
|
|
675
|
+
"mw_ds_oh_da",
|
|
676
|
+
"mw_ds_5p_phosphate_da",
|
|
677
|
+
)
|
|
678
|
+
if thermo_sequence is None:
|
|
679
|
+
for name in mass_names:
|
|
680
|
+
builder.add(name, None, cast(str, thermo_reason))
|
|
681
|
+
else:
|
|
682
|
+
try:
|
|
683
|
+
mass_values = (
|
|
684
|
+
molecular_weight(thermo_sequence).value_dalton,
|
|
685
|
+
molecular_weight(
|
|
686
|
+
thermo_sequence,
|
|
687
|
+
five_prime_phosphorylated=True,
|
|
688
|
+
).value_dalton,
|
|
689
|
+
molecular_weight(thermo_sequence, strand="double").value_dalton,
|
|
690
|
+
molecular_weight(
|
|
691
|
+
thermo_sequence,
|
|
692
|
+
strand="double",
|
|
693
|
+
five_prime_phosphorylated=True,
|
|
694
|
+
).value_dalton,
|
|
695
|
+
)
|
|
696
|
+
for name, mass in zip(mass_names, mass_values, strict=True):
|
|
697
|
+
builder.add(name, mass)
|
|
698
|
+
except DNAKitError as error:
|
|
699
|
+
reason = _exception_reason(error)
|
|
700
|
+
for name in mass_names:
|
|
701
|
+
builder.add(name, None, reason)
|
|
702
|
+
|
|
703
|
+
epsilon_names = (
|
|
704
|
+
"epsilon260_ss_m_inverse_cm_inverse",
|
|
705
|
+
"nmol_per_a260_1ml_1cm",
|
|
706
|
+
"ug_per_a260_1ml_1cm",
|
|
707
|
+
)
|
|
708
|
+
if thermo_sequence is None:
|
|
709
|
+
for name in epsilon_names:
|
|
710
|
+
builder.add(name, None, cast(str, thermo_reason))
|
|
711
|
+
else:
|
|
712
|
+
try:
|
|
713
|
+
extinction = extinction_coefficient_260nm(thermo_sequence)
|
|
714
|
+
epsilon = extinction.value_m_inverse_cm_inverse
|
|
715
|
+
mw_value = builder.values["mw_ss_oh_da"]
|
|
716
|
+
builder.add("epsilon260_ss_m_inverse_cm_inverse", epsilon)
|
|
717
|
+
builder.add("nmol_per_a260_1ml_1cm", 1_000_000.0 / epsilon)
|
|
718
|
+
builder.add(
|
|
719
|
+
"ug_per_a260_1ml_1cm",
|
|
720
|
+
1000.0 * cast(float, mw_value) / epsilon if mw_value is not None else None,
|
|
721
|
+
"requires an available ssDNA molecular weight" if mw_value is None else None,
|
|
722
|
+
)
|
|
723
|
+
except DNAKitError as error:
|
|
724
|
+
reason = _exception_reason(error)
|
|
725
|
+
for name in epsilon_names:
|
|
726
|
+
builder.add(name, None, reason)
|
|
727
|
+
|
|
728
|
+
if thermo_sequence is None:
|
|
729
|
+
builder.add("tm_wallace_c", None, cast(str, thermo_reason))
|
|
730
|
+
else:
|
|
731
|
+
try:
|
|
732
|
+
wallace = melting_temperature(
|
|
733
|
+
thermo_sequence,
|
|
734
|
+
method="wallace",
|
|
735
|
+
conditions=resolved_conditions,
|
|
736
|
+
)
|
|
737
|
+
builder.add("tm_wallace_c", wallace.tm_celsius)
|
|
738
|
+
except DNAKitError as error:
|
|
739
|
+
builder.add("tm_wallace_c", None, _exception_reason(error))
|
|
740
|
+
|
|
741
|
+
stacking_names = (
|
|
742
|
+
"stacking_delta_h_kcal_per_mol",
|
|
743
|
+
"stacking_delta_s_cal_per_k_mol_k",
|
|
744
|
+
"stacking_delta_g37_kcal_per_mol",
|
|
745
|
+
)
|
|
746
|
+
if thermo_sequence is None:
|
|
747
|
+
for name in stacking_names:
|
|
748
|
+
builder.add(name, None, cast(str, thermo_reason))
|
|
749
|
+
else:
|
|
750
|
+
try:
|
|
751
|
+
stacking = stacking_interactions(thermo_sequence, temperature_celsius=37.0)
|
|
752
|
+
for name, item in zip(
|
|
753
|
+
stacking_names,
|
|
754
|
+
(
|
|
755
|
+
stacking.total_delta_h_kcal_per_mol,
|
|
756
|
+
stacking.total_delta_s_cal_per_k_mol,
|
|
757
|
+
stacking.total_delta_g_kcal_per_mol,
|
|
758
|
+
),
|
|
759
|
+
strict=True,
|
|
760
|
+
):
|
|
761
|
+
builder.add(name, item)
|
|
762
|
+
except DNAKitError as error:
|
|
763
|
+
reason = _exception_reason(error)
|
|
764
|
+
for name in stacking_names:
|
|
765
|
+
builder.add(name, None, reason)
|
|
766
|
+
|
|
767
|
+
nearest_neighbor_names = (
|
|
768
|
+
"nn_delta_h_kcal_per_mol",
|
|
769
|
+
"nn_delta_s_cal_per_mol_k",
|
|
770
|
+
"nn_delta_g37_kcal_per_mol",
|
|
771
|
+
"nn_tm_c",
|
|
772
|
+
)
|
|
773
|
+
if thermo_sequence is None:
|
|
774
|
+
for name in nearest_neighbor_names:
|
|
775
|
+
builder.add(name, None, cast(str, thermo_reason))
|
|
776
|
+
else:
|
|
777
|
+
try:
|
|
778
|
+
free_energy_conditions = ThermodynamicConditions(
|
|
779
|
+
temperature_celsius=37.0,
|
|
780
|
+
sodium_molar=resolved_conditions.sodium_molar,
|
|
781
|
+
potassium_molar=resolved_conditions.potassium_molar,
|
|
782
|
+
magnesium_molar=resolved_conditions.magnesium_molar,
|
|
783
|
+
dntp_molar=resolved_conditions.dntp_molar,
|
|
784
|
+
strand_concentration_molar=resolved_conditions.strand_concentration_molar,
|
|
785
|
+
dmso_percent=resolved_conditions.dmso_percent,
|
|
786
|
+
dmso_factor_celsius_per_percent=(
|
|
787
|
+
resolved_conditions.dmso_factor_celsius_per_percent
|
|
788
|
+
),
|
|
789
|
+
formamide_molar=resolved_conditions.formamide_molar,
|
|
790
|
+
)
|
|
791
|
+
nearest = nearest_neighbor(thermo_sequence, conditions=free_energy_conditions)
|
|
792
|
+
for name, item in zip(
|
|
793
|
+
nearest_neighbor_names,
|
|
794
|
+
(
|
|
795
|
+
nearest.delta_h_kcal_per_mol,
|
|
796
|
+
nearest.delta_s_cal_per_k_mol,
|
|
797
|
+
nearest.delta_g_kcal_per_mol,
|
|
798
|
+
nearest.tm_celsius,
|
|
799
|
+
),
|
|
800
|
+
strict=True,
|
|
801
|
+
):
|
|
802
|
+
builder.add(name, item)
|
|
803
|
+
except DNAKitError as error:
|
|
804
|
+
reason = _exception_reason(error)
|
|
805
|
+
for name in nearest_neighbor_names:
|
|
806
|
+
builder.add(name, None, reason)
|
|
807
|
+
|
|
808
|
+
if thermo_sequence is None:
|
|
809
|
+
builder.add("self_complementary", None, cast(str, thermo_reason))
|
|
810
|
+
elif canonical_count == 0:
|
|
811
|
+
builder.add("self_complementary", None, empty_base_reason)
|
|
812
|
+
else:
|
|
813
|
+
builder.add(
|
|
814
|
+
"self_complementary",
|
|
815
|
+
canonical_text == canonical_text.translate(_COMPLEMENT)[::-1],
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
for property_spec in DINUCLEOTIDE_PROPERTY_SPECS:
|
|
819
|
+
names = tuple(
|
|
820
|
+
f"diprodb_{property_spec.key}_{stat}" for stat in ("mean", "sd", "min", "max")
|
|
821
|
+
)
|
|
822
|
+
if dinucleotide_property_table is None:
|
|
823
|
+
for name in names:
|
|
824
|
+
builder.add(
|
|
825
|
+
name,
|
|
826
|
+
None,
|
|
827
|
+
(
|
|
828
|
+
"requires an explicit user-supplied DinucleotidePropertyTable; "
|
|
829
|
+
"DNAKit bundles no DiProDB numerical values"
|
|
830
|
+
),
|
|
831
|
+
)
|
|
832
|
+
continue
|
|
833
|
+
property_definition = dinucleotide_property_table.property(property_spec.key)
|
|
834
|
+
weighted_values = tuple(
|
|
835
|
+
(float(cast(int | float, property_definition.values[word])), count)
|
|
836
|
+
for word, count in kmer_counts[2].items()
|
|
837
|
+
if count
|
|
838
|
+
)
|
|
839
|
+
if not weighted_values:
|
|
840
|
+
for name in names:
|
|
841
|
+
builder.add(
|
|
842
|
+
name,
|
|
843
|
+
None,
|
|
844
|
+
"requires at least one valid canonical dinucleotide position",
|
|
845
|
+
)
|
|
846
|
+
continue
|
|
847
|
+
property_mean = (
|
|
848
|
+
math.fsum(property_value * count for property_value, count in weighted_values)
|
|
849
|
+
/ dinucleotide_total
|
|
850
|
+
)
|
|
851
|
+
statistics = (
|
|
852
|
+
property_mean,
|
|
853
|
+
math.sqrt(
|
|
854
|
+
math.fsum(
|
|
855
|
+
count * (property_value - property_mean) ** 2
|
|
856
|
+
for property_value, count in weighted_values
|
|
857
|
+
)
|
|
858
|
+
/ dinucleotide_total
|
|
859
|
+
),
|
|
860
|
+
min(property_value for property_value, _ in weighted_values),
|
|
861
|
+
max(property_value for property_value, _ in weighted_values),
|
|
862
|
+
)
|
|
863
|
+
for name, statistic in zip(names, statistics, strict=True):
|
|
864
|
+
builder.add(name, statistic)
|
|
865
|
+
|
|
866
|
+
ordered_values, ordered_reasons = builder.ordered()
|
|
867
|
+
return AllDescriptorsResult(
|
|
868
|
+
schema_version=DESCRIPTOR_SCHEMA_VERSION,
|
|
869
|
+
sequence_id=sequence_id,
|
|
870
|
+
values=FrozenDict(ordered_values),
|
|
871
|
+
unavailable_reasons=FrozenDict(ordered_reasons),
|
|
872
|
+
conditions=FrozenDict(
|
|
873
|
+
{
|
|
874
|
+
"ambiguity_policy": policy.value,
|
|
875
|
+
"cross_gaps": False,
|
|
876
|
+
"kmer_overlapping": True,
|
|
877
|
+
"kmer_lengths": [1, 2, 3],
|
|
878
|
+
"linguistic_k_range": [1, 6],
|
|
879
|
+
"lz76_max_sequence_length": _MAX_LZ76_LENGTH,
|
|
880
|
+
"orf_genetic_code": 1,
|
|
881
|
+
"orf_start_codons": [_STANDARD_START],
|
|
882
|
+
"orf_stop_codons": sorted(_STANDARD_STOPS),
|
|
883
|
+
"orf_complete_only": True,
|
|
884
|
+
"repeat_min_unit_length": 1,
|
|
885
|
+
"repeat_max_unit_length": 20,
|
|
886
|
+
"repeat_min_copies": 2,
|
|
887
|
+
"temperature_celsius": resolved_conditions.temperature_celsius,
|
|
888
|
+
"free_energy_reference_temperature_celsius": 37.0,
|
|
889
|
+
"sodium_molar": resolved_conditions.sodium_molar,
|
|
890
|
+
"potassium_molar": resolved_conditions.potassium_molar,
|
|
891
|
+
"magnesium_molar": resolved_conditions.magnesium_molar,
|
|
892
|
+
"dntp_molar": resolved_conditions.dntp_molar,
|
|
893
|
+
"strand_concentration_molar": (resolved_conditions.strand_concentration_molar),
|
|
894
|
+
"dmso_percent": resolved_conditions.dmso_percent,
|
|
895
|
+
"dmso_factor_celsius_per_percent": (
|
|
896
|
+
resolved_conditions.dmso_factor_celsius_per_percent
|
|
897
|
+
),
|
|
898
|
+
"formamide_molar": resolved_conditions.formamide_molar,
|
|
899
|
+
"dinucleotide_property_table": (
|
|
900
|
+
None
|
|
901
|
+
if dinucleotide_property_table is None
|
|
902
|
+
else {
|
|
903
|
+
"name": dinucleotide_property_table.name,
|
|
904
|
+
"version": dinucleotide_property_table.version,
|
|
905
|
+
"source": dinucleotide_property_table.source,
|
|
906
|
+
"sha256": dinucleotide_property_table.sha256,
|
|
907
|
+
"schema_version": DINUCLEOTIDE_TABLE_SCHEMA_VERSION,
|
|
908
|
+
}
|
|
909
|
+
),
|
|
910
|
+
"dinucleotide_sd": "population",
|
|
911
|
+
}
|
|
912
|
+
),
|
|
913
|
+
provenance=_provenance(dinucleotide_property_table),
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
__all__ = ["all_descriptors"]
|