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