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