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,175 @@
|
|
|
1
|
+
"""Validated conditions and safety limits for thermodynamic calculations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal, TypeAlias
|
|
8
|
+
|
|
9
|
+
from dnakit.exceptions import ConfigurationError
|
|
10
|
+
|
|
11
|
+
SaltModel: TypeAlias = Literal["santalucia1998-monovalent-entropy"]
|
|
12
|
+
TmMethod: TypeAlias = Literal["wallace", "nearest_neighbor"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _finite_number(value: object, name: str) -> float:
|
|
16
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
17
|
+
raise ConfigurationError(
|
|
18
|
+
f"{name} must be a finite number.",
|
|
19
|
+
code="INVALID_THERMODYNAMIC_CONDITION",
|
|
20
|
+
context={"field": name, "value": value},
|
|
21
|
+
)
|
|
22
|
+
return float(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _positive_integer(value: object, name: str) -> int:
|
|
26
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
27
|
+
raise ConfigurationError(
|
|
28
|
+
f"{name} must be a positive integer.",
|
|
29
|
+
code="INVALID_THERMODYNAMIC_LIMIT",
|
|
30
|
+
context={"field": name, "value": value},
|
|
31
|
+
)
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, init=False)
|
|
36
|
+
class ThermodynamicConditions:
|
|
37
|
+
"""Physical conditions recorded with every temperature-dependent result.
|
|
38
|
+
|
|
39
|
+
Concentrations are expressed in mol/L. ``strand_concentration_molar`` is
|
|
40
|
+
the total oligonucleotide strand concentration used by the native Tm
|
|
41
|
+
equation. Native calculations treat Na+ and K+ as an explicit total
|
|
42
|
+
monovalent concentration. Magnesium, dNTP and cosolvent fields are retained
|
|
43
|
+
for backend calculations or explicit empirical corrections.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
temperature_celsius: float
|
|
47
|
+
sodium_molar: float
|
|
48
|
+
potassium_molar: float
|
|
49
|
+
magnesium_molar: float
|
|
50
|
+
dntp_molar: float
|
|
51
|
+
strand_concentration_molar: float
|
|
52
|
+
dmso_percent: float
|
|
53
|
+
dmso_factor_celsius_per_percent: float
|
|
54
|
+
formamide_molar: float
|
|
55
|
+
salt_model: SaltModel
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
temperature_celsius: float = 37.0,
|
|
61
|
+
sodium_molar: float = 0.05,
|
|
62
|
+
potassium_molar: float = 0.0,
|
|
63
|
+
magnesium_molar: float = 0.0,
|
|
64
|
+
dntp_molar: float = 0.0,
|
|
65
|
+
strand_concentration_molar: float = 250e-9,
|
|
66
|
+
dmso_percent: float = 0.0,
|
|
67
|
+
dmso_factor_celsius_per_percent: float = 0.6,
|
|
68
|
+
formamide_molar: float = 0.0,
|
|
69
|
+
salt_model: SaltModel = "santalucia1998-monovalent-entropy",
|
|
70
|
+
) -> None:
|
|
71
|
+
temperature = _finite_number(temperature_celsius, "temperature_celsius")
|
|
72
|
+
sodium = _finite_number(sodium_molar, "sodium_molar")
|
|
73
|
+
potassium = _finite_number(potassium_molar, "potassium_molar")
|
|
74
|
+
magnesium = _finite_number(magnesium_molar, "magnesium_molar")
|
|
75
|
+
dntp = _finite_number(dntp_molar, "dntp_molar")
|
|
76
|
+
concentration = _finite_number(strand_concentration_molar, "strand_concentration_molar")
|
|
77
|
+
dmso = _finite_number(dmso_percent, "dmso_percent")
|
|
78
|
+
dmso_factor = _finite_number(
|
|
79
|
+
dmso_factor_celsius_per_percent, "dmso_factor_celsius_per_percent"
|
|
80
|
+
)
|
|
81
|
+
formamide = _finite_number(formamide_molar, "formamide_molar")
|
|
82
|
+
if not 0.0 <= temperature <= 100.0:
|
|
83
|
+
raise ConfigurationError(
|
|
84
|
+
"temperature_celsius must be in the modeled interval [0, 100] C.",
|
|
85
|
+
code="THERMODYNAMIC_TEMPERATURE_OUT_OF_DOMAIN",
|
|
86
|
+
)
|
|
87
|
+
if not 0.0 <= sodium <= 2.0 or not 0.0 <= potassium <= 2.0:
|
|
88
|
+
raise ConfigurationError(
|
|
89
|
+
"sodium_molar and potassium_molar must each be in [0, 2].",
|
|
90
|
+
code="THERMODYNAMIC_MONOVALENT_OUT_OF_DOMAIN",
|
|
91
|
+
)
|
|
92
|
+
if not 0.0 < sodium + potassium <= 2.0:
|
|
93
|
+
raise ConfigurationError(
|
|
94
|
+
"The total monovalent concentration (Na+ + K+) must be in (0, 2].",
|
|
95
|
+
code="THERMODYNAMIC_MONOVALENT_OUT_OF_DOMAIN",
|
|
96
|
+
)
|
|
97
|
+
if not 0.0 <= magnesium <= 1.0 or not 0.0 <= dntp <= 1.0:
|
|
98
|
+
raise ConfigurationError(
|
|
99
|
+
"magnesium_molar and dntp_molar must be in the interval [0, 1].",
|
|
100
|
+
code="THERMODYNAMIC_DIVALENT_OUT_OF_DOMAIN",
|
|
101
|
+
)
|
|
102
|
+
if not 0.0 < concentration <= 1.0:
|
|
103
|
+
raise ConfigurationError(
|
|
104
|
+
"strand_concentration_molar must be in the interval (0, 1].",
|
|
105
|
+
code="THERMODYNAMIC_CONCENTRATION_OUT_OF_DOMAIN",
|
|
106
|
+
)
|
|
107
|
+
if not 0.0 <= dmso <= 100.0 or not 0.0 <= dmso_factor <= 2.0:
|
|
108
|
+
raise ConfigurationError(
|
|
109
|
+
"dmso_percent must be in [0, 100] and its factor in [0, 2].",
|
|
110
|
+
code="THERMODYNAMIC_COSOLVENT_OUT_OF_DOMAIN",
|
|
111
|
+
)
|
|
112
|
+
if not 0.0 <= formamide <= 30.0:
|
|
113
|
+
raise ConfigurationError(
|
|
114
|
+
"formamide_molar must be in [0, 30].",
|
|
115
|
+
code="THERMODYNAMIC_COSOLVENT_OUT_OF_DOMAIN",
|
|
116
|
+
)
|
|
117
|
+
if salt_model != "santalucia1998-monovalent-entropy":
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
"The native implementation supports only the SantaLucia 1998 monovalent model.",
|
|
120
|
+
code="UNSUPPORTED_SALT_MODEL",
|
|
121
|
+
context={"salt_model": salt_model},
|
|
122
|
+
)
|
|
123
|
+
object.__setattr__(self, "temperature_celsius", temperature)
|
|
124
|
+
object.__setattr__(self, "sodium_molar", sodium)
|
|
125
|
+
object.__setattr__(self, "potassium_molar", potassium)
|
|
126
|
+
object.__setattr__(self, "magnesium_molar", magnesium)
|
|
127
|
+
object.__setattr__(self, "dntp_molar", dntp)
|
|
128
|
+
object.__setattr__(self, "strand_concentration_molar", concentration)
|
|
129
|
+
object.__setattr__(self, "dmso_percent", dmso)
|
|
130
|
+
object.__setattr__(self, "dmso_factor_celsius_per_percent", dmso_factor)
|
|
131
|
+
object.__setattr__(self, "formamide_molar", formamide)
|
|
132
|
+
object.__setattr__(self, "salt_model", salt_model)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def monovalent_molar(self) -> float:
|
|
136
|
+
"""Total Na+ plus K+ concentration used by supported monovalent models."""
|
|
137
|
+
|
|
138
|
+
return self.sodium_molar + self.potassium_molar
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True, init=False)
|
|
142
|
+
class NearestNeighborConfig:
|
|
143
|
+
"""Native model selection and hard sequence-size safety limit."""
|
|
144
|
+
|
|
145
|
+
parameter_set: str
|
|
146
|
+
max_sequence_length: int
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
parameter_set: str = "santalucia1998-v1",
|
|
152
|
+
max_sequence_length: int = 60,
|
|
153
|
+
) -> None:
|
|
154
|
+
if parameter_set != "santalucia1998-v1":
|
|
155
|
+
raise ConfigurationError(
|
|
156
|
+
"Unknown native nearest-neighbor parameter set.",
|
|
157
|
+
code="UNKNOWN_THERMODYNAMIC_PARAMETER_SET",
|
|
158
|
+
context={"parameter_set": parameter_set},
|
|
159
|
+
)
|
|
160
|
+
limit = _positive_integer(max_sequence_length, "max_sequence_length")
|
|
161
|
+
if not 2 <= limit <= 60:
|
|
162
|
+
raise ConfigurationError(
|
|
163
|
+
"The native nearest-neighbor applicability limit must be in [2, 60] nt.",
|
|
164
|
+
code="THERMODYNAMIC_APPLICABILITY_LIMIT",
|
|
165
|
+
)
|
|
166
|
+
object.__setattr__(self, "parameter_set", parameter_set)
|
|
167
|
+
object.__setattr__(self, "max_sequence_length", limit)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = [
|
|
171
|
+
"NearestNeighborConfig",
|
|
172
|
+
"SaltModel",
|
|
173
|
+
"ThermodynamicConditions",
|
|
174
|
+
"TmMethod",
|
|
175
|
+
]
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Duplex equilibrium, theoretical melting-curve and terminal-stability helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from itertools import islice, pairwise
|
|
9
|
+
from typing import TypeAlias
|
|
10
|
+
|
|
11
|
+
from dnakit.core import DNASequence
|
|
12
|
+
from dnakit.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
from ._shared import canonical_linear_symbols, native_provenance
|
|
15
|
+
from .calculations import nearest_neighbor
|
|
16
|
+
from .config import NearestNeighborConfig, ThermodynamicConditions
|
|
17
|
+
from .results import (
|
|
18
|
+
BindingEquilibriumResult,
|
|
19
|
+
CosolventCorrectionResult,
|
|
20
|
+
MeltingCurvePoint,
|
|
21
|
+
TerminalStabilityResult,
|
|
22
|
+
TheoreticalMeltingCurveResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
MeltingCurveProgress: TypeAlias = Callable[[int, int], None]
|
|
26
|
+
_GAS_CONSTANT_KCAL_PER_K_MOL = 0.00198720425864083
|
|
27
|
+
_MAX_CURVE_POINTS = 100_001
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _conditions(value: ThermodynamicConditions | None) -> ThermodynamicConditions:
|
|
31
|
+
resolved = ThermodynamicConditions() if value is None else value
|
|
32
|
+
if not isinstance(resolved, ThermodynamicConditions):
|
|
33
|
+
raise ConfigurationError(
|
|
34
|
+
"conditions must be ThermodynamicConditions or None.",
|
|
35
|
+
code="INVALID_THERMODYNAMIC_CONDITIONS",
|
|
36
|
+
)
|
|
37
|
+
return resolved
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _association_constant(delta_g_kcal_per_mol: float, temperature_celsius: float) -> float:
|
|
41
|
+
temperature_kelvin = temperature_celsius + 273.15
|
|
42
|
+
exponent = -delta_g_kcal_per_mol / (_GAS_CONSTANT_KCAL_PER_K_MOL * temperature_kelvin)
|
|
43
|
+
return math.exp(min(700.0, max(-700.0, exponent)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def binding_equilibrium(
|
|
47
|
+
sequence: DNASequence,
|
|
48
|
+
*,
|
|
49
|
+
complement: DNASequence | None = None,
|
|
50
|
+
conditions: ThermodynamicConditions | None = None,
|
|
51
|
+
config: NearestNeighborConfig | None = None,
|
|
52
|
+
) -> BindingEquilibriumResult:
|
|
53
|
+
"""Calculate Ka, Kd and exact two-state duplex fraction from mass balance."""
|
|
54
|
+
|
|
55
|
+
resolved = _conditions(conditions)
|
|
56
|
+
thermodynamics = nearest_neighbor(
|
|
57
|
+
sequence,
|
|
58
|
+
complement=complement,
|
|
59
|
+
conditions=resolved,
|
|
60
|
+
config=config,
|
|
61
|
+
)
|
|
62
|
+
association = _association_constant(
|
|
63
|
+
thermodynamics.delta_g_kcal_per_mol,
|
|
64
|
+
resolved.temperature_celsius,
|
|
65
|
+
)
|
|
66
|
+
total = resolved.strand_concentration_molar
|
|
67
|
+
if thermodynamics.self_complementary:
|
|
68
|
+
free = 2.0 * total / (1.0 + math.sqrt(1.0 + 8.0 * association * total))
|
|
69
|
+
duplex = max(0.0, (total - free) / 2.0)
|
|
70
|
+
fraction = 0.0 if total == 0.0 else 2.0 * duplex / total
|
|
71
|
+
else:
|
|
72
|
+
each_strand_total = total / 2.0
|
|
73
|
+
free = (
|
|
74
|
+
2.0 * each_strand_total / (1.0 + math.sqrt(1.0 + 4.0 * association * each_strand_total))
|
|
75
|
+
)
|
|
76
|
+
duplex = max(0.0, each_strand_total - free)
|
|
77
|
+
fraction = 0.0 if each_strand_total == 0.0 else duplex / each_strand_total
|
|
78
|
+
return BindingEquilibriumResult(
|
|
79
|
+
temperature_celsius=resolved.temperature_celsius,
|
|
80
|
+
delta_g_kcal_per_mol=thermodynamics.delta_g_kcal_per_mol,
|
|
81
|
+
association_constant_m_inverse=association,
|
|
82
|
+
dissociation_constant_molar=1.0 / association,
|
|
83
|
+
total_strand_concentration_molar=total,
|
|
84
|
+
free_strand_concentration_molar=free,
|
|
85
|
+
duplex_concentration_molar=duplex,
|
|
86
|
+
duplex_fraction=min(1.0, max(0.0, fraction)),
|
|
87
|
+
self_complementary=thermodynamics.self_complementary,
|
|
88
|
+
model="ideal-two-state-perfect-duplex-mass-balance-v1",
|
|
89
|
+
thermodynamics=thermodynamics,
|
|
90
|
+
provenance=thermodynamics.provenance,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _curve_conditions(
|
|
95
|
+
base: ThermodynamicConditions, temperature_celsius: float
|
|
96
|
+
) -> ThermodynamicConditions:
|
|
97
|
+
return ThermodynamicConditions(
|
|
98
|
+
temperature_celsius=temperature_celsius,
|
|
99
|
+
sodium_molar=base.sodium_molar,
|
|
100
|
+
potassium_molar=base.potassium_molar,
|
|
101
|
+
magnesium_molar=base.magnesium_molar,
|
|
102
|
+
dntp_molar=base.dntp_molar,
|
|
103
|
+
strand_concentration_molar=base.strand_concentration_molar,
|
|
104
|
+
dmso_percent=base.dmso_percent,
|
|
105
|
+
dmso_factor_celsius_per_percent=base.dmso_factor_celsius_per_percent,
|
|
106
|
+
formamide_molar=base.formamide_molar,
|
|
107
|
+
salt_model=base.salt_model,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def theoretical_melting_curve(
|
|
112
|
+
sequence: DNASequence,
|
|
113
|
+
temperatures_celsius: Iterable[float],
|
|
114
|
+
*,
|
|
115
|
+
complement: DNASequence | None = None,
|
|
116
|
+
conditions: ThermodynamicConditions | None = None,
|
|
117
|
+
config: NearestNeighborConfig | None = None,
|
|
118
|
+
progress: MeltingCurveProgress | None = None,
|
|
119
|
+
) -> TheoreticalMeltingCurveResult:
|
|
120
|
+
"""Evaluate the ideal two-state duplex fraction at bounded temperatures."""
|
|
121
|
+
|
|
122
|
+
if isinstance(temperatures_celsius, (str, bytes)) or not isinstance(
|
|
123
|
+
temperatures_celsius, Iterable
|
|
124
|
+
):
|
|
125
|
+
raise ConfigurationError(
|
|
126
|
+
"temperatures_celsius must be an iterable.", code="INVALID_MELTING_CURVE"
|
|
127
|
+
)
|
|
128
|
+
raw_temperatures = tuple(islice(iter(temperatures_celsius), _MAX_CURVE_POINTS + 1))
|
|
129
|
+
if not 2 <= len(raw_temperatures) <= _MAX_CURVE_POINTS:
|
|
130
|
+
raise ConfigurationError(
|
|
131
|
+
f"A melting curve requires 2-{_MAX_CURVE_POINTS} temperature points.",
|
|
132
|
+
code="INVALID_MELTING_CURVE",
|
|
133
|
+
)
|
|
134
|
+
temperatures: list[float] = []
|
|
135
|
+
for value in raw_temperatures:
|
|
136
|
+
if (
|
|
137
|
+
isinstance(value, bool)
|
|
138
|
+
or not isinstance(value, (int, float))
|
|
139
|
+
or not math.isfinite(value)
|
|
140
|
+
or not 0.0 <= value <= 100.0
|
|
141
|
+
):
|
|
142
|
+
raise ConfigurationError(
|
|
143
|
+
"Every melting-curve temperature must be finite and in [0, 100] C.",
|
|
144
|
+
code="INVALID_MELTING_CURVE_TEMPERATURE",
|
|
145
|
+
)
|
|
146
|
+
temperatures.append(float(value))
|
|
147
|
+
if any(right <= left for left, right in pairwise(temperatures)):
|
|
148
|
+
raise ConfigurationError(
|
|
149
|
+
"Melting-curve temperatures must be strictly increasing.",
|
|
150
|
+
code="UNSORTED_MELTING_CURVE_TEMPERATURES",
|
|
151
|
+
)
|
|
152
|
+
if progress is not None and not callable(progress):
|
|
153
|
+
raise ConfigurationError(
|
|
154
|
+
"progress must be callable or None.", code="INVALID_MELTING_CURVE_PROGRESS"
|
|
155
|
+
)
|
|
156
|
+
resolved = _conditions(conditions)
|
|
157
|
+
points: list[MeltingCurvePoint] = []
|
|
158
|
+
curve_provenance = None
|
|
159
|
+
for index, temperature in enumerate(temperatures, start=1):
|
|
160
|
+
equilibrium = binding_equilibrium(
|
|
161
|
+
sequence,
|
|
162
|
+
complement=complement,
|
|
163
|
+
conditions=_curve_conditions(resolved, temperature),
|
|
164
|
+
config=config,
|
|
165
|
+
)
|
|
166
|
+
points.append(
|
|
167
|
+
MeltingCurvePoint(
|
|
168
|
+
temperature_celsius=temperature,
|
|
169
|
+
duplex_fraction=equilibrium.duplex_fraction,
|
|
170
|
+
delta_g_kcal_per_mol=equilibrium.delta_g_kcal_per_mol,
|
|
171
|
+
association_constant_m_inverse=equilibrium.association_constant_m_inverse,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
curve_provenance = equilibrium.provenance
|
|
175
|
+
if progress is not None:
|
|
176
|
+
with suppress(Exception):
|
|
177
|
+
progress(index, len(temperatures))
|
|
178
|
+
midpoint: float | None = None
|
|
179
|
+
for left, right in pairwise(points):
|
|
180
|
+
if left.duplex_fraction == 0.5:
|
|
181
|
+
midpoint = left.temperature_celsius
|
|
182
|
+
break
|
|
183
|
+
if left.duplex_fraction >= 0.5 >= right.duplex_fraction:
|
|
184
|
+
span = left.duplex_fraction - right.duplex_fraction
|
|
185
|
+
fraction = 0.0 if span == 0.0 else (left.duplex_fraction - 0.5) / span
|
|
186
|
+
midpoint = left.temperature_celsius + fraction * (
|
|
187
|
+
right.temperature_celsius - left.temperature_celsius
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
if midpoint is None and points[-1].duplex_fraction == 0.5:
|
|
191
|
+
midpoint = points[-1].temperature_celsius
|
|
192
|
+
sequence_symbols = canonical_linear_symbols(
|
|
193
|
+
sequence,
|
|
194
|
+
operation="theoretical_melting_curve",
|
|
195
|
+
min_length=2,
|
|
196
|
+
max_length=60,
|
|
197
|
+
)
|
|
198
|
+
return TheoreticalMeltingCurveResult(
|
|
199
|
+
sequence=sequence_symbols,
|
|
200
|
+
temperatures_celsius=tuple(temperatures),
|
|
201
|
+
points=tuple(points),
|
|
202
|
+
midpoint_temperature_celsius=midpoint,
|
|
203
|
+
conditions=resolved,
|
|
204
|
+
model="ideal-two-state-perfect-duplex-mass-balance-v1",
|
|
205
|
+
applicability=(
|
|
206
|
+
"Equilibrium two-state curve for the native fully complementary nearest-neighbor "
|
|
207
|
+
"model; it is not an instrument response, kinetic trace, or heat-capacity model."
|
|
208
|
+
),
|
|
209
|
+
provenance=native_provenance() if curve_provenance is None else curve_provenance,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def terminal_stability(
|
|
214
|
+
sequence: DNASequence,
|
|
215
|
+
*,
|
|
216
|
+
window_size: int = 5,
|
|
217
|
+
conditions: ThermodynamicConditions | None = None,
|
|
218
|
+
config: NearestNeighborConfig | None = None,
|
|
219
|
+
) -> TerminalStabilityResult:
|
|
220
|
+
"""Compare nearest-neighbor stability of equal 5-prime and 3-prime windows."""
|
|
221
|
+
|
|
222
|
+
symbols = canonical_linear_symbols(
|
|
223
|
+
sequence,
|
|
224
|
+
operation="terminal_stability",
|
|
225
|
+
min_length=2,
|
|
226
|
+
max_length=60,
|
|
227
|
+
)
|
|
228
|
+
if (
|
|
229
|
+
isinstance(window_size, bool)
|
|
230
|
+
or not isinstance(window_size, int)
|
|
231
|
+
or not 2 <= window_size <= len(symbols)
|
|
232
|
+
):
|
|
233
|
+
raise ConfigurationError(
|
|
234
|
+
"window_size must be an integer in [2, sequence length].",
|
|
235
|
+
code="INVALID_TERMINAL_STABILITY_WINDOW",
|
|
236
|
+
)
|
|
237
|
+
resolved = _conditions(conditions)
|
|
238
|
+
five_sequence = symbols[:window_size]
|
|
239
|
+
three_sequence = symbols[-window_size:]
|
|
240
|
+
five = nearest_neighbor(DNASequence(five_sequence), conditions=resolved, config=config)
|
|
241
|
+
three = nearest_neighbor(DNASequence(three_sequence), conditions=resolved, config=config)
|
|
242
|
+
if math.isclose(five.delta_g_kcal_per_mol, three.delta_g_kcal_per_mol, abs_tol=1e-12):
|
|
243
|
+
less_stable = "equal"
|
|
244
|
+
elif five.delta_g_kcal_per_mol > three.delta_g_kcal_per_mol:
|
|
245
|
+
less_stable = "5-prime"
|
|
246
|
+
else:
|
|
247
|
+
less_stable = "3-prime"
|
|
248
|
+
return TerminalStabilityResult(
|
|
249
|
+
sequence=symbols,
|
|
250
|
+
window_size=window_size,
|
|
251
|
+
five_prime_sequence=five_sequence,
|
|
252
|
+
three_prime_sequence=three_sequence,
|
|
253
|
+
five_prime_delta_g_kcal_per_mol=five.delta_g_kcal_per_mol,
|
|
254
|
+
three_prime_delta_g_kcal_per_mol=three.delta_g_kcal_per_mol,
|
|
255
|
+
less_stable_end=less_stable,
|
|
256
|
+
conditions=resolved,
|
|
257
|
+
five_prime_thermodynamics=five,
|
|
258
|
+
three_prime_thermodynamics=three,
|
|
259
|
+
provenance=five.provenance,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cosolvent_tm_correction(
|
|
264
|
+
sequence: DNASequence,
|
|
265
|
+
uncorrected_tm_celsius: float,
|
|
266
|
+
*,
|
|
267
|
+
dmso_percent: float = 0.0,
|
|
268
|
+
dmso_factor_celsius_per_percent: float = 0.6,
|
|
269
|
+
formamide_molar: float = 0.0,
|
|
270
|
+
) -> CosolventCorrectionResult:
|
|
271
|
+
"""Apply explicit Primer3 empirical DMSO and formamide Tm corrections."""
|
|
272
|
+
|
|
273
|
+
symbols = canonical_linear_symbols(
|
|
274
|
+
sequence,
|
|
275
|
+
operation="cosolvent_tm_correction",
|
|
276
|
+
min_length=1,
|
|
277
|
+
max_length=1_000_000,
|
|
278
|
+
)
|
|
279
|
+
values = {
|
|
280
|
+
"uncorrected_tm_celsius": uncorrected_tm_celsius,
|
|
281
|
+
"dmso_percent": dmso_percent,
|
|
282
|
+
"dmso_factor_celsius_per_percent": dmso_factor_celsius_per_percent,
|
|
283
|
+
"formamide_molar": formamide_molar,
|
|
284
|
+
}
|
|
285
|
+
resolved_values: dict[str, float] = {}
|
|
286
|
+
for name, value in values.items():
|
|
287
|
+
if (
|
|
288
|
+
isinstance(value, bool)
|
|
289
|
+
or not isinstance(value, (int, float))
|
|
290
|
+
or not math.isfinite(value)
|
|
291
|
+
):
|
|
292
|
+
raise ConfigurationError(
|
|
293
|
+
f"{name} must be finite numeric data.",
|
|
294
|
+
code="INVALID_COSOLVENT_CORRECTION",
|
|
295
|
+
)
|
|
296
|
+
resolved_values[name] = float(value)
|
|
297
|
+
if not 0.0 <= resolved_values["dmso_percent"] <= 100.0:
|
|
298
|
+
raise ConfigurationError(
|
|
299
|
+
"dmso_percent must be in [0, 100].", code="INVALID_COSOLVENT_CORRECTION"
|
|
300
|
+
)
|
|
301
|
+
if not 0.0 <= resolved_values["dmso_factor_celsius_per_percent"] <= 2.0:
|
|
302
|
+
raise ConfigurationError(
|
|
303
|
+
"dmso_factor_celsius_per_percent must be in [0, 2].",
|
|
304
|
+
code="INVALID_COSOLVENT_CORRECTION",
|
|
305
|
+
)
|
|
306
|
+
if not 0.0 <= resolved_values["formamide_molar"] <= 30.0:
|
|
307
|
+
raise ConfigurationError(
|
|
308
|
+
"formamide_molar must be in [0, 30].", code="INVALID_COSOLVENT_CORRECTION"
|
|
309
|
+
)
|
|
310
|
+
gc_fraction = sum(base in "GC" for base in symbols) / len(symbols)
|
|
311
|
+
dmso_delta = -(
|
|
312
|
+
resolved_values["dmso_factor_celsius_per_percent"] * resolved_values["dmso_percent"]
|
|
313
|
+
)
|
|
314
|
+
formamide_delta = (0.453 * gc_fraction - 2.88) * resolved_values["formamide_molar"]
|
|
315
|
+
corrected = resolved_values["uncorrected_tm_celsius"] + dmso_delta + formamide_delta
|
|
316
|
+
return CosolventCorrectionResult(
|
|
317
|
+
uncorrected_tm_celsius=resolved_values["uncorrected_tm_celsius"],
|
|
318
|
+
dmso_percent=resolved_values["dmso_percent"],
|
|
319
|
+
dmso_factor_celsius_per_percent=resolved_values["dmso_factor_celsius_per_percent"],
|
|
320
|
+
dmso_delta_tm_celsius=dmso_delta,
|
|
321
|
+
formamide_molar=resolved_values["formamide_molar"],
|
|
322
|
+
gc_fraction=gc_fraction,
|
|
323
|
+
formamide_delta_tm_celsius=formamide_delta,
|
|
324
|
+
corrected_tm_celsius=corrected,
|
|
325
|
+
method="primer3-manual-empirical-cosolvent-correction-v1",
|
|
326
|
+
applicability=(
|
|
327
|
+
"Empirical additive correction only; DMSO and formamide effects are not a "
|
|
328
|
+
"mechanistic nearest-neighbor free-energy model."
|
|
329
|
+
),
|
|
330
|
+
provenance=native_provenance(citation=False),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
__all__ = [
|
|
335
|
+
"MeltingCurveProgress",
|
|
336
|
+
"binding_equilibrium",
|
|
337
|
+
"cosolvent_tm_correction",
|
|
338
|
+
"terminal_stability",
|
|
339
|
+
"theoretical_melting_curve",
|
|
340
|
+
]
|