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,445 @@
1
+ """Immutable, serializable thermodynamic results with explicit units."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Any, cast
8
+
9
+ from dnakit.core import BackendInfo, ImplementationLabel, Issue, Provenance
10
+ from dnakit.core._json import FrozenDict, to_json_compatible
11
+
12
+ from .config import ThermodynamicConditions
13
+
14
+
15
+ class _SerializableResult:
16
+ def to_dict(self) -> dict[str, Any]:
17
+ return cast(dict[str, Any], to_json_compatible(self))
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class MolecularWeightResult(_SerializableResult):
22
+ value_dalton: float
23
+ value_kilodalton: float
24
+ strand_count: int
25
+ sequence_length: int
26
+ five_prime_phosphorylated: bool
27
+ method: str
28
+ algorithm_version: str
29
+ applicability: str
30
+ parameters: FrozenDict
31
+ provenance: Provenance
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ExtinctionCoefficientResult(_SerializableResult):
36
+ value_m_inverse_cm_inverse: float
37
+ wavelength_nm: int
38
+ sequence_length: int
39
+ method: str
40
+ algorithm_version: str
41
+ applicability: str
42
+ parameters: FrozenDict
43
+ provenance: Provenance
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class SaltCorrectionResult(_SerializableResult):
48
+ delta_s_cal_per_k_mol: float
49
+ sequence_length: int
50
+ model: str
51
+ model_version: str
52
+ conditions: ThermodynamicConditions
53
+ applicability: str
54
+ provenance: Provenance
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class StackingStep(_SerializableResult):
59
+ index: int
60
+ top_5to3: str
61
+ bottom_3to5: str
62
+ delta_h_kcal_per_mol: float
63
+ delta_s_cal_per_k_mol: float
64
+ delta_g_kcal_per_mol: float
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class StackingResult(_SerializableResult):
69
+ sequence: str
70
+ complement_3to5: str
71
+ temperature_celsius: float
72
+ parameter_set: str
73
+ steps: tuple[StackingStep, ...]
74
+ total_delta_h_kcal_per_mol: float
75
+ total_delta_s_cal_per_k_mol: float
76
+ total_delta_g_kcal_per_mol: float
77
+ applicability: str
78
+ provenance: Provenance
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class NearestNeighborResult(_SerializableResult):
83
+ sequence: str
84
+ complement_5to3: str
85
+ sequence_length: int
86
+ parameter_set: str
87
+ conditions: ThermodynamicConditions
88
+ stacking_steps: tuple[StackingStep, ...]
89
+ stacking_delta_h_kcal_per_mol: float
90
+ stacking_delta_s_cal_per_k_mol: float
91
+ initiation_delta_h_kcal_per_mol: float
92
+ initiation_delta_s_cal_per_k_mol: float
93
+ symmetry_delta_h_kcal_per_mol: float
94
+ symmetry_delta_s_cal_per_k_mol: float
95
+ salt_delta_s_cal_per_k_mol: float
96
+ delta_h_kcal_per_mol: float
97
+ delta_s_cal_per_k_mol: float
98
+ delta_g_kcal_per_mol: float
99
+ tm_celsius: float
100
+ self_complementary: bool
101
+ concentration_divisor: int
102
+ gas_constant_cal_per_k_mol: float
103
+ reference_sodium_molar: float
104
+ tm_equation: str
105
+ method: str
106
+ algorithm_version: str
107
+ applicability: str
108
+ provenance: Provenance
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class MeltingTemperatureResult(_SerializableResult):
113
+ tm_celsius: float
114
+ sequence_length: int
115
+ method: str
116
+ algorithm_version: str
117
+ conditions: ThermodynamicConditions
118
+ parameter_set: str | None
119
+ applicability: str
120
+ provenance: Provenance
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class DuplexStabilityResult(_SerializableResult):
125
+ sequence_a_5to3: str
126
+ sequence_b_5to3: str
127
+ fully_complementary: bool
128
+ stable_at_temperature: bool
129
+ stability_criterion: str
130
+ delta_g_kcal_per_mol: float
131
+ tm_celsius: float
132
+ conditions: ThermodynamicConditions
133
+ model: str
134
+ applicability: str
135
+ thermodynamics: NearestNeighborResult | Primer3ThermodynamicResult
136
+ provenance: Provenance
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class WindowTmPoint(_SerializableResult):
141
+ start: int
142
+ end: int
143
+ sequence: str
144
+ tm_celsius: float
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class WindowTmResult(_SerializableResult):
149
+ sequence_length: int
150
+ window_size: int
151
+ step: int
152
+ method: str
153
+ conditions: ThermodynamicConditions
154
+ windows: tuple[WindowTmPoint, ...]
155
+ min_tm_celsius: float | None
156
+ max_tm_celsius: float | None
157
+ max_windows: int
158
+ coordinate_system: str
159
+ applicability: str
160
+ provenance: Provenance
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class ConditionalCapability(_SerializableResult):
165
+ requirement_id: str
166
+ capability: str
167
+ status: str
168
+ execution_supported: bool
169
+ automatic_install: bool
170
+ automatic_download: bool
171
+ compatible_backends: tuple[str, ...]
172
+ backend_info: BackendInfo | None
173
+ reason: str
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class Primer3ThermodynamicResult(_SerializableResult):
178
+ """One explicitly requested calculation from user-installed Primer3 CLI tools."""
179
+
180
+ capability: str
181
+ sequences_5to3: tuple[str, ...]
182
+ structure_found: bool | None
183
+ tm_celsius: float
184
+ delta_g_kcal_per_mol: float | None
185
+ delta_h_kcal_per_mol: float | None
186
+ delta_s_cal_per_k_mol: float | None
187
+ ascii_structure: str | None
188
+ conditions: ThermodynamicConditions
189
+ max_loop: int | None
190
+ method: str
191
+ algorithm_version: str
192
+ backend: BackendInfo
193
+ provenance: Provenance
194
+ issues: tuple[Issue, ...]
195
+
196
+ def __post_init__(self) -> None:
197
+ import math
198
+
199
+ if self.capability not in {"tm", "hairpin", "self_dimer", "heterodimer"}:
200
+ raise ValueError("Unknown Primer3 thermodynamic capability.")
201
+ expected_sequences = 2 if self.capability == "heterodimer" else 1
202
+ if len(self.sequences_5to3) != expected_sequences or any(
203
+ not sequence or len(sequence) > 60 or set(sequence) - set("ACGT")
204
+ for sequence in self.sequences_5to3
205
+ ):
206
+ raise ValueError("Primer3 sequences must contain 1-60 nt of canonical DNA.")
207
+ if self.structure_found is not None and not isinstance(self.structure_found, bool):
208
+ raise ValueError("structure_found must be boolean or None.")
209
+ if not math.isfinite(self.tm_celsius):
210
+ raise ValueError("tm_celsius must be finite.")
211
+ thermodynamic_values = (
212
+ self.delta_g_kcal_per_mol,
213
+ self.delta_h_kcal_per_mol,
214
+ self.delta_s_cal_per_k_mol,
215
+ )
216
+ if any(value is not None and not math.isfinite(value) for value in thermodynamic_values):
217
+ raise ValueError("Primer3 thermodynamic values must be finite or None.")
218
+ if self.capability == "tm" and any(value is not None for value in thermodynamic_values):
219
+ raise ValueError("Tm-only results cannot claim structure thermodynamics.")
220
+ if self.capability != "tm" and any(value is None for value in thermodynamic_values):
221
+ raise ValueError("Structure results require dG, dH, and dS values.")
222
+ if self.max_loop is not None and (
223
+ isinstance(self.max_loop, bool)
224
+ or not isinstance(self.max_loop, int)
225
+ or not 1 <= self.max_loop <= 30
226
+ ):
227
+ raise ValueError("max_loop must be None or an integer in [1, 30].")
228
+ if (self.capability == "tm") != (self.max_loop is None):
229
+ raise ValueError(
230
+ "Tm results require max_loop=None; structure results require max_loop."
231
+ )
232
+ if self.ascii_structure is not None and (
233
+ not isinstance(self.ascii_structure, str) or len(self.ascii_structure) > 100_000
234
+ ):
235
+ raise ValueError("ascii_structure must be text within 100,000 characters.")
236
+ for name in ("method", "algorithm_version"):
237
+ value = getattr(self, name)
238
+ if not isinstance(value, str) or not value.strip():
239
+ raise ValueError(f"{name} must be a non-empty string.")
240
+ if not isinstance(self.conditions, ThermodynamicConditions):
241
+ raise ValueError("conditions must be ThermodynamicConditions.")
242
+ if not isinstance(self.backend, BackendInfo) or not self.backend.available:
243
+ raise ValueError("Primer3 result requires an available BackendInfo.")
244
+ if self.capability not in self.backend.capabilities:
245
+ raise ValueError("Primer3 result capability is absent from BackendInfo.")
246
+ if not isinstance(self.provenance, Provenance):
247
+ raise ValueError("provenance must be Provenance.")
248
+ if self.provenance.backend != self.backend:
249
+ raise ValueError("Primer3 result backend and provenance backend must match.")
250
+ if self.provenance.implementation.label is not ImplementationLabel.ADAPTER:
251
+ raise ValueError("Primer3 results must identify an adapter implementation.")
252
+ if any(not isinstance(issue, Issue) for issue in self.issues):
253
+ raise ValueError("issues must contain Issue objects.")
254
+
255
+
256
+ @dataclass(frozen=True)
257
+ class OpticalModification(_SerializableResult):
258
+ """One explicitly counted label or chemical modification correction."""
259
+
260
+ name: str
261
+ count: int = 1
262
+ extinction_coefficient_260_delta_m_inverse_cm_inverse: float = 0.0
263
+ molecular_weight_delta_dalton: float = 0.0
264
+
265
+ def __post_init__(self) -> None:
266
+ if not isinstance(self.name, str) or not self.name.strip():
267
+ raise ValueError("Optical modification name must be non-empty text.")
268
+ if isinstance(self.count, bool) or not isinstance(self.count, int) or self.count <= 0:
269
+ raise ValueError("Optical modification count must be a positive integer.")
270
+ for field_name in (
271
+ "extinction_coefficient_260_delta_m_inverse_cm_inverse",
272
+ "molecular_weight_delta_dalton",
273
+ ):
274
+ value = getattr(self, field_name)
275
+ if (
276
+ isinstance(value, bool)
277
+ or not isinstance(value, (int, float))
278
+ or not math.isfinite(value)
279
+ ):
280
+ raise ValueError(f"{field_name} must be finite numeric data.")
281
+
282
+
283
+ @dataclass(frozen=True)
284
+ class LabelAbsorbanceCorrection(_SerializableResult):
285
+ """Measured dye-channel absorbance contribution to subtract from A260."""
286
+
287
+ name: str
288
+ absorbance_at_label_max: float
289
+ a260_correction_factor: float
290
+
291
+ def __post_init__(self) -> None:
292
+ if not isinstance(self.name, str) or not self.name.strip():
293
+ raise ValueError("Label correction name must be non-empty text.")
294
+ for field_name in ("absorbance_at_label_max", "a260_correction_factor"):
295
+ value = getattr(self, field_name)
296
+ if (
297
+ isinstance(value, bool)
298
+ or not isinstance(value, (int, float))
299
+ or not math.isfinite(value)
300
+ or value < 0.0
301
+ ):
302
+ raise ValueError(f"{field_name} must be finite and non-negative.")
303
+
304
+
305
+ @dataclass(frozen=True)
306
+ class OpticalPropertiesResult(_SerializableResult):
307
+ strand_type: str
308
+ sequences_5to3: tuple[str, ...]
309
+ sequence_lengths: tuple[int, ...]
310
+ native_extinction_coefficient_260_m_inverse_cm_inverse: float
311
+ modification_extinction_coefficient_260_m_inverse_cm_inverse: float
312
+ extinction_coefficient_260_m_inverse_cm_inverse: float
313
+ molecular_weight_dalton: float
314
+ one_od260_nmol: float
315
+ one_od260_microgram: float
316
+ method: str
317
+ modifications: tuple[OpticalModification, ...]
318
+ assumptions: tuple[str, ...]
319
+ provenance: Provenance
320
+
321
+
322
+ @dataclass(frozen=True)
323
+ class A260ConcentrationResult(_SerializableResult):
324
+ measured_a260: float
325
+ label_a260_subtracted: float
326
+ corrected_a260: float
327
+ path_length_cm: float
328
+ dilution_factor: float
329
+ extinction_coefficient_260_m_inverse_cm_inverse: float
330
+ molar_concentration_molar: float
331
+ molar_concentration_micromolar: float
332
+ molar_concentration_nanomolar: float
333
+ mass_concentration_g_per_l: float
334
+ mass_concentration_ng_per_microliter: float
335
+ volume_liter: float | None
336
+ amount_mol: float | None
337
+ mass_microgram: float | None
338
+ label_corrections: tuple[LabelAbsorbanceCorrection, ...]
339
+ method: str
340
+ provenance: Provenance
341
+
342
+
343
+ @dataclass(frozen=True)
344
+ class OligoQuantityResult(_SerializableResult):
345
+ molecular_weight_dalton: float
346
+ volume_liter: float | None
347
+ molar_concentration_molar: float | None
348
+ mass_concentration_g_per_l: float | None
349
+ amount_mol: float
350
+ amount_nmol: float
351
+ mass_g: float
352
+ mass_microgram: float
353
+ input_kind: str
354
+ method: str
355
+
356
+
357
+ @dataclass(frozen=True)
358
+ class BindingEquilibriumResult(_SerializableResult):
359
+ temperature_celsius: float
360
+ delta_g_kcal_per_mol: float
361
+ association_constant_m_inverse: float
362
+ dissociation_constant_molar: float
363
+ total_strand_concentration_molar: float
364
+ free_strand_concentration_molar: float
365
+ duplex_concentration_molar: float
366
+ duplex_fraction: float
367
+ self_complementary: bool
368
+ model: str
369
+ thermodynamics: NearestNeighborResult
370
+ provenance: Provenance
371
+
372
+
373
+ @dataclass(frozen=True)
374
+ class MeltingCurvePoint(_SerializableResult):
375
+ temperature_celsius: float
376
+ duplex_fraction: float
377
+ delta_g_kcal_per_mol: float
378
+ association_constant_m_inverse: float
379
+
380
+
381
+ @dataclass(frozen=True)
382
+ class TheoreticalMeltingCurveResult(_SerializableResult):
383
+ sequence: str
384
+ temperatures_celsius: tuple[float, ...]
385
+ points: tuple[MeltingCurvePoint, ...]
386
+ midpoint_temperature_celsius: float | None
387
+ conditions: ThermodynamicConditions
388
+ model: str
389
+ applicability: str
390
+ provenance: Provenance
391
+
392
+
393
+ @dataclass(frozen=True)
394
+ class TerminalStabilityResult(_SerializableResult):
395
+ sequence: str
396
+ window_size: int
397
+ five_prime_sequence: str
398
+ three_prime_sequence: str
399
+ five_prime_delta_g_kcal_per_mol: float
400
+ three_prime_delta_g_kcal_per_mol: float
401
+ less_stable_end: str
402
+ conditions: ThermodynamicConditions
403
+ five_prime_thermodynamics: NearestNeighborResult
404
+ three_prime_thermodynamics: NearestNeighborResult
405
+ provenance: Provenance
406
+
407
+
408
+ @dataclass(frozen=True)
409
+ class CosolventCorrectionResult(_SerializableResult):
410
+ uncorrected_tm_celsius: float
411
+ dmso_percent: float
412
+ dmso_factor_celsius_per_percent: float
413
+ dmso_delta_tm_celsius: float
414
+ formamide_molar: float
415
+ gc_fraction: float
416
+ formamide_delta_tm_celsius: float
417
+ corrected_tm_celsius: float
418
+ method: str
419
+ applicability: str
420
+ provenance: Provenance
421
+
422
+
423
+ __all__ = [
424
+ "A260ConcentrationResult",
425
+ "BindingEquilibriumResult",
426
+ "ConditionalCapability",
427
+ "CosolventCorrectionResult",
428
+ "DuplexStabilityResult",
429
+ "LabelAbsorbanceCorrection",
430
+ "MeltingCurvePoint",
431
+ "MeltingTemperatureResult",
432
+ "MolecularWeightResult",
433
+ "NearestNeighborResult",
434
+ "OligoQuantityResult",
435
+ "OpticalModification",
436
+ "OpticalPropertiesResult",
437
+ "Primer3ThermodynamicResult",
438
+ "SaltCorrectionResult",
439
+ "StackingResult",
440
+ "StackingStep",
441
+ "TerminalStabilityResult",
442
+ "TheoreticalMeltingCurveResult",
443
+ "WindowTmPoint",
444
+ "WindowTmResult",
445
+ ]
@@ -0,0 +1,43 @@
1
+ """Dependency-free, deterministic SVG visualizations for DNAKit results."""
2
+
3
+ from dnakit.visualization.advanced import (
4
+ plot_alignment,
5
+ plot_circular_map,
6
+ plot_linear_map,
7
+ )
8
+ from dnakit.visualization.config import (
9
+ HeatmapConfig,
10
+ ImageExportConfig,
11
+ LimitPolicy,
12
+ SaveConfig,
13
+ SequencePlotConfig,
14
+ SVGTheme,
15
+ )
16
+ from dnakit.visualization.export import PathLike, save_html_report, save_image, save_svg
17
+ from dnakit.visualization.matrix import plot_similarity_matrix
18
+ from dnakit.visualization.report import build_html_report
19
+ from dnakit.visualization.results import HTMLReportArtifact, SVGArtifact, VisualizationSaveResult
20
+ from dnakit.visualization.sequence import Highlight, plot_sequence
21
+
22
+ __all__ = [
23
+ "HTMLReportArtifact",
24
+ "HeatmapConfig",
25
+ "Highlight",
26
+ "ImageExportConfig",
27
+ "LimitPolicy",
28
+ "PathLike",
29
+ "SVGArtifact",
30
+ "SVGTheme",
31
+ "SaveConfig",
32
+ "SequencePlotConfig",
33
+ "VisualizationSaveResult",
34
+ "build_html_report",
35
+ "plot_alignment",
36
+ "plot_circular_map",
37
+ "plot_linear_map",
38
+ "plot_sequence",
39
+ "plot_similarity_matrix",
40
+ "save_html_report",
41
+ "save_image",
42
+ "save_svg",
43
+ ]
@@ -0,0 +1,185 @@
1
+ """Small deterministic SVG construction helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import xml.etree.ElementTree as ET
8
+ from collections.abc import Iterable, Mapping
9
+
10
+ from dnakit.core.issues import Issue
11
+ from dnakit.exceptions import ConfigurationError
12
+
13
+ from .config import SVGTheme
14
+ from .results import SVGArtifact
15
+
16
+ SVG_NAMESPACE = "http://www.w3.org/2000/svg"
17
+ ET.register_namespace("", SVG_NAMESPACE)
18
+
19
+
20
+ def number(value: int | float) -> str:
21
+ """Stable compact numeric formatting for SVG attributes and labels."""
22
+
23
+ if isinstance(value, int):
24
+ return str(value)
25
+ if not math.isfinite(value):
26
+ raise ValueError("SVG numbers must be finite.")
27
+ if math.isclose(value, 0.0, abs_tol=1e-12):
28
+ return "0"
29
+ return format(value, ".8g")
30
+
31
+
32
+ def _attributes(values: Mapping[str, object]) -> dict[str, str]:
33
+ resolved: dict[str, str] = {}
34
+ for name, value in values.items():
35
+ if value is None:
36
+ continue
37
+ attribute_name = name[:-1] if name.endswith("_") else name
38
+ attribute_name = attribute_name.replace("_", "-")
39
+ text = number(value) if isinstance(value, (int, float)) else str(value)
40
+ validate_xml_text(text, name=attribute_name)
41
+ resolved[attribute_name] = text
42
+ return resolved
43
+
44
+
45
+ def validate_xml_text(value: str, *, name: str = "text") -> None:
46
+ """Reject characters forbidden by XML 1.0 before serialization."""
47
+
48
+ if not isinstance(value, str):
49
+ raise TypeError(f"{name} must be a string.")
50
+ for character in value:
51
+ codepoint = ord(character)
52
+ if not (
53
+ codepoint in (0x09, 0x0A, 0x0D)
54
+ or 0x20 <= codepoint <= 0xD7FF
55
+ or 0xE000 <= codepoint <= 0xFFFD
56
+ or 0x10000 <= codepoint <= 0x10FFFF
57
+ ):
58
+ raise ValueError(f"{name} contains a character forbidden by XML 1.0.")
59
+
60
+
61
+ class SVGBuilder:
62
+ """Minimal ElementTree wrapper that keeps SVG output deterministic."""
63
+
64
+ def __init__(
65
+ self,
66
+ width: int,
67
+ height: int,
68
+ *,
69
+ title: str,
70
+ description: str,
71
+ kind: str,
72
+ theme: SVGTheme,
73
+ metadata: Mapping[str, object],
74
+ ) -> None:
75
+ self.width = width
76
+ self.height = height
77
+ self.kind = kind
78
+ self.metadata = dict(metadata)
79
+ try:
80
+ validate_xml_text(title, name="title")
81
+ validate_xml_text(description, name="description")
82
+ except ValueError as exc:
83
+ raise ConfigurationError(
84
+ "Visualization text contains a character forbidden by XML.",
85
+ code="INVALID_VISUALIZATION_TEXT",
86
+ ) from exc
87
+ self.root = ET.Element(
88
+ f"{{{SVG_NAMESPACE}}}svg",
89
+ {
90
+ "width": str(width),
91
+ "height": str(height),
92
+ "viewBox": f"0 0 {width} {height}",
93
+ "role": "img",
94
+ "aria-labelledby": "dnakit-title dnakit-description",
95
+ "data-kind": kind,
96
+ },
97
+ )
98
+ title_node = ET.SubElement(self.root, f"{{{SVG_NAMESPACE}}}title", {"id": "dnakit-title"})
99
+ title_node.text = title
100
+ description_node = ET.SubElement(
101
+ self.root, f"{{{SVG_NAMESPACE}}}desc", {"id": "dnakit-description"}
102
+ )
103
+ description_node.text = description
104
+ metadata_node = ET.SubElement(self.root, f"{{{SVG_NAMESPACE}}}metadata")
105
+ metadata_text = json.dumps(
106
+ metadata, ensure_ascii=False, sort_keys=True, separators=(",", ":")
107
+ )
108
+ try:
109
+ validate_xml_text(metadata_text, name="metadata")
110
+ except ValueError as exc:
111
+ raise ConfigurationError(
112
+ "Visualization metadata contains a character forbidden by XML.",
113
+ code="INVALID_VISUALIZATION_TEXT",
114
+ ) from exc
115
+ metadata_node.text = metadata_text
116
+ self.rect(0, 0, width, height, fill=theme.background, class_="background")
117
+
118
+ def element(self, tag: str, **attributes: object) -> ET.Element:
119
+ return ET.SubElement(
120
+ self.root,
121
+ f"{{{SVG_NAMESPACE}}}{tag}",
122
+ _attributes(attributes),
123
+ )
124
+
125
+ def rect(
126
+ self, x: float, y: float, width: float, height: float, **attributes: object
127
+ ) -> ET.Element:
128
+ return self.element("rect", x=x, y=y, width=width, height=height, **attributes)
129
+
130
+ def line(self, x1: float, y1: float, x2: float, y2: float, **attributes: object) -> ET.Element:
131
+ return self.element("line", x1=x1, y1=y1, x2=x2, y2=y2, **attributes)
132
+
133
+ def text(self, x: float, y: float, value: object, **attributes: object) -> ET.Element:
134
+ node = self.element("text", x=x, y=y, **attributes)
135
+ text = str(value)
136
+ validate_xml_text(text)
137
+ node.text = text
138
+ return node
139
+
140
+ def polyline(self, points: Iterable[tuple[float, float]], **attributes: object) -> ET.Element:
141
+ encoded = " ".join(f"{number(x)},{number(y)}" for x, y in points)
142
+ return self.element("polyline", points=encoded, **attributes)
143
+
144
+ def artifact(
145
+ self,
146
+ *,
147
+ issues: Iterable[Issue] = (),
148
+ metadata: Mapping[str, object] | None = None,
149
+ ) -> SVGArtifact:
150
+ svg = ET.tostring(self.root, encoding="unicode", short_empty_elements=True)
151
+ element_count = sum(1 for _ in self.root.iter())
152
+ return SVGArtifact(
153
+ self.kind,
154
+ svg,
155
+ self.width,
156
+ self.height,
157
+ element_count,
158
+ metadata=self.metadata if metadata is None else metadata,
159
+ issues=issues,
160
+ )
161
+
162
+
163
+ def parse_hex_color(value: str) -> tuple[int, int, int]:
164
+ """Return RGB for validated 3- or 6-digit hexadecimal colors."""
165
+
166
+ if len(value) == 4:
167
+ red, green, blue = (int(symbol * 2, 16) for symbol in value[1:])
168
+ return red, green, blue
169
+ if len(value) == 7:
170
+ red, green, blue = (int(value[index : index + 2], 16) for index in (1, 3, 5))
171
+ return red, green, blue
172
+ raise ValueError("Heatmap interpolation requires #RGB or #RRGGBB colors.")
173
+
174
+
175
+ def interpolate_color(low: str, high: str, fraction: float) -> str:
176
+ low_rgb = parse_hex_color(low)
177
+ high_rgb = parse_hex_color(high)
178
+ bounded = min(1.0, max(0.0, fraction))
179
+ channels = tuple(
180
+ round(start + (end - start) * bounded) for start, end in zip(low_rgb, high_rgb, strict=True)
181
+ )
182
+ return "#" + "".join(f"{channel:02x}" for channel in channels)
183
+
184
+
185
+ __all__ = ["SVG_NAMESPACE", "SVGBuilder", "interpolate_color", "number", "validate_xml_text"]