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,385 @@
|
|
|
1
|
+
"""Optical properties and unit-safe oligonucleotide concentration conversions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from itertools import islice
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from dnakit.core import DNASequence
|
|
11
|
+
from dnakit.exceptions import ConfigurationError
|
|
12
|
+
|
|
13
|
+
from ._shared import canonical_linear_symbols
|
|
14
|
+
from .calculations import extinction_coefficient_260nm, molecular_weight
|
|
15
|
+
from .results import (
|
|
16
|
+
A260ConcentrationResult,
|
|
17
|
+
LabelAbsorbanceCorrection,
|
|
18
|
+
OligoQuantityResult,
|
|
19
|
+
OpticalModification,
|
|
20
|
+
OpticalPropertiesResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_AVERAGE_DUPLEX_EXTINCTION_PER_BASE_PAIR = 13_200.0
|
|
24
|
+
_MAX_CORRECTIONS = 1_000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _finite(
|
|
28
|
+
value: object,
|
|
29
|
+
name: str,
|
|
30
|
+
*,
|
|
31
|
+
minimum: float | None = None,
|
|
32
|
+
strictly_positive: bool = False,
|
|
33
|
+
) -> float:
|
|
34
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
35
|
+
raise ConfigurationError(
|
|
36
|
+
f"{name} must be finite numeric data.",
|
|
37
|
+
code="INVALID_OPTICAL_VALUE",
|
|
38
|
+
context={"field": name, "value": value},
|
|
39
|
+
)
|
|
40
|
+
result = float(value)
|
|
41
|
+
if strictly_positive and result <= 0.0:
|
|
42
|
+
raise ConfigurationError(
|
|
43
|
+
f"{name} must be greater than zero.",
|
|
44
|
+
code="INVALID_OPTICAL_VALUE",
|
|
45
|
+
context={"field": name, "value": value},
|
|
46
|
+
)
|
|
47
|
+
if minimum is not None and result < minimum:
|
|
48
|
+
raise ConfigurationError(
|
|
49
|
+
f"{name} must be at least {minimum}.",
|
|
50
|
+
code="INVALID_OPTICAL_VALUE",
|
|
51
|
+
context={"field": name, "value": value},
|
|
52
|
+
)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bounded_tuple(
|
|
57
|
+
values: Iterable[object], expected_type: type[object], name: str
|
|
58
|
+
) -> tuple[object, ...]:
|
|
59
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Iterable):
|
|
60
|
+
raise ConfigurationError(f"{name} must be an iterable.", code="INVALID_OPTICAL_VALUE")
|
|
61
|
+
resolved = tuple(islice(iter(values), _MAX_CORRECTIONS + 1))
|
|
62
|
+
if len(resolved) > _MAX_CORRECTIONS:
|
|
63
|
+
raise ConfigurationError(
|
|
64
|
+
f"{name} exceeds the {_MAX_CORRECTIONS}-item safety limit.",
|
|
65
|
+
code="OPTICAL_CORRECTION_LIMIT_EXCEEDED",
|
|
66
|
+
)
|
|
67
|
+
if any(not isinstance(item, expected_type) for item in resolved):
|
|
68
|
+
raise ConfigurationError(
|
|
69
|
+
f"Every {name} item must be {expected_type.__name__}.",
|
|
70
|
+
code="INVALID_OPTICAL_VALUE",
|
|
71
|
+
)
|
|
72
|
+
return resolved
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def optical_properties(
|
|
76
|
+
sequence: DNASequence,
|
|
77
|
+
*,
|
|
78
|
+
strand_type: Literal["single", "double"] = "single",
|
|
79
|
+
complement: DNASequence | None = None,
|
|
80
|
+
duplex_method: Literal["average-base-pair", "strand-sum-hypochromicity"] = (
|
|
81
|
+
"average-base-pair"
|
|
82
|
+
),
|
|
83
|
+
hypochromicity_fraction: float | None = None,
|
|
84
|
+
modifications: Iterable[OpticalModification] = (),
|
|
85
|
+
) -> OpticalPropertiesResult:
|
|
86
|
+
"""Calculate epsilon260, molecular weight, nmol/OD and mass/OD.
|
|
87
|
+
|
|
88
|
+
``average-base-pair`` uses 13,200 M^-1 cm^-1 per base pair and is the
|
|
89
|
+
conventional 1 OD260 = 50 microgram/mL duplex approximation.
|
|
90
|
+
``strand-sum-hypochromicity`` requires the caller to provide the fractional
|
|
91
|
+
absorbance reduction on duplex formation instead of silently assuming one.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
if strand_type not in {"single", "double"}:
|
|
95
|
+
raise ConfigurationError(
|
|
96
|
+
"strand_type must be 'single' or 'double'.",
|
|
97
|
+
code="INVALID_OPTICAL_STRAND_TYPE",
|
|
98
|
+
)
|
|
99
|
+
symbols = canonical_linear_symbols(
|
|
100
|
+
sequence,
|
|
101
|
+
operation="optical_properties",
|
|
102
|
+
min_length=1,
|
|
103
|
+
max_length=1_000_000,
|
|
104
|
+
)
|
|
105
|
+
sequence_single = DNASequence(symbols)
|
|
106
|
+
resolved_modifications = _bounded_tuple(modifications, OpticalModification, "modifications")
|
|
107
|
+
typed_modifications = tuple(
|
|
108
|
+
item for item in resolved_modifications if isinstance(item, OpticalModification)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
sequences: tuple[str, ...]
|
|
112
|
+
assumptions: tuple[str, ...]
|
|
113
|
+
if strand_type == "single":
|
|
114
|
+
if complement is not None:
|
|
115
|
+
raise ConfigurationError(
|
|
116
|
+
"complement is only valid for double-stranded optical properties.",
|
|
117
|
+
code="UNUSED_OPTICAL_COMPLEMENT",
|
|
118
|
+
)
|
|
119
|
+
if hypochromicity_fraction is not None:
|
|
120
|
+
raise ConfigurationError(
|
|
121
|
+
"hypochromicity_fraction is only valid for a duplex strand-sum model.",
|
|
122
|
+
code="UNUSED_HYPOCHROMICITY",
|
|
123
|
+
)
|
|
124
|
+
extinction = extinction_coefficient_260nm(sequence_single)
|
|
125
|
+
native_epsilon = extinction.value_m_inverse_cm_inverse
|
|
126
|
+
mass = molecular_weight(sequence_single).value_dalton
|
|
127
|
+
sequences = (symbols,)
|
|
128
|
+
method = extinction.method
|
|
129
|
+
assumptions = (
|
|
130
|
+
"Single-stranded, linear, canonical and unmodified DNA before explicit corrections.",
|
|
131
|
+
"One OD260 unit means A260 multiplied by sample volume in mL equals one, "
|
|
132
|
+
"using a 1 cm reference path.",
|
|
133
|
+
)
|
|
134
|
+
provenance = extinction.provenance
|
|
135
|
+
else:
|
|
136
|
+
expected_complement = sequence_single.reverse_complement().symbols
|
|
137
|
+
if complement is None:
|
|
138
|
+
complement_symbols = expected_complement
|
|
139
|
+
else:
|
|
140
|
+
complement_symbols = canonical_linear_symbols(
|
|
141
|
+
complement,
|
|
142
|
+
operation="optical_properties complement",
|
|
143
|
+
min_length=1,
|
|
144
|
+
max_length=1_000_000,
|
|
145
|
+
)
|
|
146
|
+
if complement_symbols != expected_complement:
|
|
147
|
+
raise ConfigurationError(
|
|
148
|
+
"The native duplex optical model requires a full reverse complement.",
|
|
149
|
+
code="MISMATCHED_OPTICAL_DUPLEX",
|
|
150
|
+
context={"expected_complement_5to3": expected_complement},
|
|
151
|
+
)
|
|
152
|
+
if duplex_method == "average-base-pair":
|
|
153
|
+
if hypochromicity_fraction is not None:
|
|
154
|
+
raise ConfigurationError(
|
|
155
|
+
"average-base-pair does not accept hypochromicity_fraction.",
|
|
156
|
+
code="UNUSED_HYPOCHROMICITY",
|
|
157
|
+
)
|
|
158
|
+
native_epsilon = _AVERAGE_DUPLEX_EXTINCTION_PER_BASE_PAIR * len(symbols)
|
|
159
|
+
method = "average-dsdna-base-pair-extinction"
|
|
160
|
+
assumptions = (
|
|
161
|
+
"Fully complementary duplex with average epsilon260 of 13,200 M^-1 cm^-1 "
|
|
162
|
+
"per base pair.",
|
|
163
|
+
"This average conversion is not a sequence-specific hypochromicity prediction.",
|
|
164
|
+
"One OD260 unit means A260 multiplied by sample volume in mL equals one, "
|
|
165
|
+
"using a 1 cm reference path.",
|
|
166
|
+
)
|
|
167
|
+
elif duplex_method == "strand-sum-hypochromicity":
|
|
168
|
+
if hypochromicity_fraction is None:
|
|
169
|
+
raise ConfigurationError(
|
|
170
|
+
"strand-sum-hypochromicity requires hypochromicity_fraction.",
|
|
171
|
+
code="MISSING_HYPOCHROMICITY",
|
|
172
|
+
)
|
|
173
|
+
fraction = _finite(
|
|
174
|
+
hypochromicity_fraction,
|
|
175
|
+
"hypochromicity_fraction",
|
|
176
|
+
minimum=0.0,
|
|
177
|
+
)
|
|
178
|
+
if fraction >= 1.0:
|
|
179
|
+
raise ConfigurationError(
|
|
180
|
+
"hypochromicity_fraction must be in [0, 1).",
|
|
181
|
+
code="INVALID_HYPOCHROMICITY",
|
|
182
|
+
)
|
|
183
|
+
primary = extinction_coefficient_260nm(sequence_single)
|
|
184
|
+
secondary = extinction_coefficient_260nm(DNASequence(complement_symbols))
|
|
185
|
+
native_epsilon = (
|
|
186
|
+
primary.value_m_inverse_cm_inverse + secondary.value_m_inverse_cm_inverse
|
|
187
|
+
) * (1.0 - fraction)
|
|
188
|
+
method = "sequence-specific-strand-sum-with-explicit-hypochromicity"
|
|
189
|
+
assumptions = (
|
|
190
|
+
"Fully complementary duplex.",
|
|
191
|
+
"The hypochromicity fraction is caller-supplied experimental or literature input.",
|
|
192
|
+
"One OD260 unit means A260 multiplied by sample volume in mL equals one, "
|
|
193
|
+
"using a 1 cm reference path.",
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
raise ConfigurationError(
|
|
197
|
+
"Unknown duplex_method.",
|
|
198
|
+
code="UNKNOWN_DUPLEX_OPTICAL_METHOD",
|
|
199
|
+
context={"duplex_method": duplex_method},
|
|
200
|
+
)
|
|
201
|
+
mass_result = molecular_weight(sequence_single, strand="double")
|
|
202
|
+
mass = mass_result.value_dalton
|
|
203
|
+
provenance = mass_result.provenance
|
|
204
|
+
sequences = (symbols, complement_symbols)
|
|
205
|
+
|
|
206
|
+
modification_epsilon = math.fsum(
|
|
207
|
+
item.count * item.extinction_coefficient_260_delta_m_inverse_cm_inverse
|
|
208
|
+
for item in typed_modifications
|
|
209
|
+
)
|
|
210
|
+
modification_mass = math.fsum(
|
|
211
|
+
item.count * item.molecular_weight_delta_dalton for item in typed_modifications
|
|
212
|
+
)
|
|
213
|
+
corrected_epsilon = native_epsilon + modification_epsilon
|
|
214
|
+
corrected_mass = mass + modification_mass
|
|
215
|
+
if corrected_epsilon <= 0.0 or corrected_mass <= 0.0:
|
|
216
|
+
raise ConfigurationError(
|
|
217
|
+
"Modification corrections must leave positive epsilon260 and molecular weight.",
|
|
218
|
+
code="INVALID_OPTICAL_CORRECTION_TOTAL",
|
|
219
|
+
)
|
|
220
|
+
return OpticalPropertiesResult(
|
|
221
|
+
strand_type=strand_type,
|
|
222
|
+
sequences_5to3=sequences,
|
|
223
|
+
sequence_lengths=tuple(len(item) for item in sequences),
|
|
224
|
+
native_extinction_coefficient_260_m_inverse_cm_inverse=native_epsilon,
|
|
225
|
+
modification_extinction_coefficient_260_m_inverse_cm_inverse=modification_epsilon,
|
|
226
|
+
extinction_coefficient_260_m_inverse_cm_inverse=corrected_epsilon,
|
|
227
|
+
molecular_weight_dalton=corrected_mass,
|
|
228
|
+
one_od260_nmol=1_000_000.0 / corrected_epsilon,
|
|
229
|
+
one_od260_microgram=1_000.0 * corrected_mass / corrected_epsilon,
|
|
230
|
+
method=method,
|
|
231
|
+
modifications=typed_modifications,
|
|
232
|
+
assumptions=assumptions,
|
|
233
|
+
provenance=provenance,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def concentration_from_a260(
|
|
238
|
+
measured_a260: float,
|
|
239
|
+
properties: OpticalPropertiesResult,
|
|
240
|
+
*,
|
|
241
|
+
path_length_cm: float = 1.0,
|
|
242
|
+
dilution_factor: float = 1.0,
|
|
243
|
+
label_corrections: Iterable[LabelAbsorbanceCorrection] = (),
|
|
244
|
+
volume_liter: float | None = None,
|
|
245
|
+
) -> A260ConcentrationResult:
|
|
246
|
+
"""Apply dye correction and Beer-Lambert law to an A260 measurement."""
|
|
247
|
+
|
|
248
|
+
absorbance = _finite(measured_a260, "measured_a260", minimum=0.0)
|
|
249
|
+
path = _finite(path_length_cm, "path_length_cm", strictly_positive=True)
|
|
250
|
+
dilution = _finite(dilution_factor, "dilution_factor", strictly_positive=True)
|
|
251
|
+
if not isinstance(properties, OpticalPropertiesResult):
|
|
252
|
+
raise ConfigurationError(
|
|
253
|
+
"properties must be OpticalPropertiesResult.", code="INVALID_OPTICAL_PROPERTIES"
|
|
254
|
+
)
|
|
255
|
+
resolved_corrections = _bounded_tuple(
|
|
256
|
+
label_corrections, LabelAbsorbanceCorrection, "label_corrections"
|
|
257
|
+
)
|
|
258
|
+
typed_corrections = tuple(
|
|
259
|
+
item for item in resolved_corrections if isinstance(item, LabelAbsorbanceCorrection)
|
|
260
|
+
)
|
|
261
|
+
subtracted = math.fsum(
|
|
262
|
+
item.absorbance_at_label_max * item.a260_correction_factor for item in typed_corrections
|
|
263
|
+
)
|
|
264
|
+
corrected = absorbance - subtracted
|
|
265
|
+
if corrected < 0.0:
|
|
266
|
+
raise ConfigurationError(
|
|
267
|
+
"Label correction exceeds the measured A260.",
|
|
268
|
+
code="NEGATIVE_CORRECTED_A260",
|
|
269
|
+
context={"measured_a260": absorbance, "subtracted_a260": subtracted},
|
|
270
|
+
)
|
|
271
|
+
epsilon = properties.extinction_coefficient_260_m_inverse_cm_inverse
|
|
272
|
+
molar = corrected * dilution / (epsilon * path)
|
|
273
|
+
mass_g_per_l = molar * properties.molecular_weight_dalton
|
|
274
|
+
resolved_volume = (
|
|
275
|
+
None
|
|
276
|
+
if volume_liter is None
|
|
277
|
+
else _finite(volume_liter, "volume_liter", strictly_positive=True)
|
|
278
|
+
)
|
|
279
|
+
amount = None if resolved_volume is None else molar * resolved_volume
|
|
280
|
+
mass_microgram = (
|
|
281
|
+
None if resolved_volume is None else mass_g_per_l * resolved_volume * 1_000_000.0
|
|
282
|
+
)
|
|
283
|
+
return A260ConcentrationResult(
|
|
284
|
+
measured_a260=absorbance,
|
|
285
|
+
label_a260_subtracted=subtracted,
|
|
286
|
+
corrected_a260=corrected,
|
|
287
|
+
path_length_cm=path,
|
|
288
|
+
dilution_factor=dilution,
|
|
289
|
+
extinction_coefficient_260_m_inverse_cm_inverse=epsilon,
|
|
290
|
+
molar_concentration_molar=molar,
|
|
291
|
+
molar_concentration_micromolar=molar * 1_000_000.0,
|
|
292
|
+
molar_concentration_nanomolar=molar * 1_000_000_000.0,
|
|
293
|
+
mass_concentration_g_per_l=mass_g_per_l,
|
|
294
|
+
mass_concentration_ng_per_microliter=mass_g_per_l * 1_000.0,
|
|
295
|
+
volume_liter=resolved_volume,
|
|
296
|
+
amount_mol=amount,
|
|
297
|
+
mass_microgram=mass_microgram,
|
|
298
|
+
label_corrections=typed_corrections,
|
|
299
|
+
method="beer-lambert-a260-with-explicit-label-correction",
|
|
300
|
+
provenance=properties.provenance,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def convert_oligo_quantity(
|
|
305
|
+
molecular_weight_dalton: float,
|
|
306
|
+
*,
|
|
307
|
+
volume_liter: float | None = None,
|
|
308
|
+
molar_concentration_molar: float | None = None,
|
|
309
|
+
mass_concentration_g_per_l: float | None = None,
|
|
310
|
+
amount_mol: float | None = None,
|
|
311
|
+
mass_g: float | None = None,
|
|
312
|
+
) -> OligoQuantityResult:
|
|
313
|
+
"""Convert exactly one concentration or amount input into all four forms."""
|
|
314
|
+
|
|
315
|
+
molecular_weight = _finite(
|
|
316
|
+
molecular_weight_dalton, "molecular_weight_dalton", strictly_positive=True
|
|
317
|
+
)
|
|
318
|
+
provided = {
|
|
319
|
+
"molar_concentration_molar": molar_concentration_molar,
|
|
320
|
+
"mass_concentration_g_per_l": mass_concentration_g_per_l,
|
|
321
|
+
"amount_mol": amount_mol,
|
|
322
|
+
"mass_g": mass_g,
|
|
323
|
+
}
|
|
324
|
+
selected = tuple(name for name, value in provided.items() if value is not None)
|
|
325
|
+
if len(selected) != 1:
|
|
326
|
+
raise ConfigurationError(
|
|
327
|
+
"Provide exactly one concentration or amount input.",
|
|
328
|
+
code="OLIGO_QUANTITY_INPUT_COUNT",
|
|
329
|
+
context={"provided_fields": selected},
|
|
330
|
+
)
|
|
331
|
+
input_kind = selected[0]
|
|
332
|
+
raw_value = _finite(provided[input_kind], input_kind, minimum=0.0)
|
|
333
|
+
resolved_volume = (
|
|
334
|
+
None
|
|
335
|
+
if volume_liter is None
|
|
336
|
+
else _finite(volume_liter, "volume_liter", strictly_positive=True)
|
|
337
|
+
)
|
|
338
|
+
molar: float | None
|
|
339
|
+
mass_concentration: float | None
|
|
340
|
+
if (input_kind.endswith("per_l") or input_kind.endswith("molar")) and resolved_volume is None:
|
|
341
|
+
raise ConfigurationError(
|
|
342
|
+
"volume_liter is required when converting a concentration to an amount.",
|
|
343
|
+
code="OLIGO_QUANTITY_VOLUME_REQUIRED",
|
|
344
|
+
)
|
|
345
|
+
if input_kind == "molar_concentration_molar":
|
|
346
|
+
molar = raw_value
|
|
347
|
+
mass_concentration = molar * molecular_weight
|
|
348
|
+
assert resolved_volume is not None
|
|
349
|
+
amount = molar * resolved_volume
|
|
350
|
+
mass = amount * molecular_weight
|
|
351
|
+
elif input_kind == "mass_concentration_g_per_l":
|
|
352
|
+
mass_concentration = raw_value
|
|
353
|
+
molar = mass_concentration / molecular_weight
|
|
354
|
+
assert resolved_volume is not None
|
|
355
|
+
mass = mass_concentration * resolved_volume
|
|
356
|
+
amount = mass / molecular_weight
|
|
357
|
+
elif input_kind == "amount_mol":
|
|
358
|
+
amount = raw_value
|
|
359
|
+
mass = amount * molecular_weight
|
|
360
|
+
molar = None if resolved_volume is None else amount / resolved_volume
|
|
361
|
+
mass_concentration = None if resolved_volume is None else mass / resolved_volume
|
|
362
|
+
else:
|
|
363
|
+
mass = raw_value
|
|
364
|
+
amount = mass / molecular_weight
|
|
365
|
+
molar = None if resolved_volume is None else amount / resolved_volume
|
|
366
|
+
mass_concentration = None if resolved_volume is None else mass / resolved_volume
|
|
367
|
+
return OligoQuantityResult(
|
|
368
|
+
molecular_weight_dalton=molecular_weight,
|
|
369
|
+
volume_liter=resolved_volume,
|
|
370
|
+
molar_concentration_molar=molar,
|
|
371
|
+
mass_concentration_g_per_l=mass_concentration,
|
|
372
|
+
amount_mol=amount,
|
|
373
|
+
amount_nmol=amount * 1_000_000_000.0,
|
|
374
|
+
mass_g=mass,
|
|
375
|
+
mass_microgram=mass * 1_000_000.0,
|
|
376
|
+
input_kind=input_kind,
|
|
377
|
+
method="molar-mass-volume-dimensional-conversion",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
__all__ = [
|
|
382
|
+
"concentration_from_a260",
|
|
383
|
+
"convert_oligo_quantity",
|
|
384
|
+
"optical_properties",
|
|
385
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Versioned native DNA/DNA nearest-neighbor parameters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from dnakit.core._json import to_json_compatible
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class NearestNeighborParameter:
|
|
14
|
+
"""One enthalpy/entropy contribution in explicit units."""
|
|
15
|
+
|
|
16
|
+
delta_h_kcal_per_mol: float
|
|
17
|
+
delta_s_cal_per_k_mol: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class NearestNeighborParameterSet:
|
|
22
|
+
"""Immutable identity and values for a published parameter table."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
version: str
|
|
26
|
+
citation: str
|
|
27
|
+
doi: str
|
|
28
|
+
reference_sodium_molar: float
|
|
29
|
+
stacking: MappingProxyType[str, NearestNeighborParameter]
|
|
30
|
+
terminal_at: NearestNeighborParameter
|
|
31
|
+
terminal_gc: NearestNeighborParameter
|
|
32
|
+
symmetry: NearestNeighborParameter
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
|
+
return cast(dict[str, Any], to_json_compatible(self))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_STACKING = {
|
|
39
|
+
"AA": NearestNeighborParameter(-7.9, -22.2),
|
|
40
|
+
"TT": NearestNeighborParameter(-7.9, -22.2),
|
|
41
|
+
"AT": NearestNeighborParameter(-7.2, -20.4),
|
|
42
|
+
"TA": NearestNeighborParameter(-7.2, -21.3),
|
|
43
|
+
"CA": NearestNeighborParameter(-8.5, -22.7),
|
|
44
|
+
"TG": NearestNeighborParameter(-8.5, -22.7),
|
|
45
|
+
"GT": NearestNeighborParameter(-8.4, -22.4),
|
|
46
|
+
"AC": NearestNeighborParameter(-8.4, -22.4),
|
|
47
|
+
"CT": NearestNeighborParameter(-7.8, -21.0),
|
|
48
|
+
"AG": NearestNeighborParameter(-7.8, -21.0),
|
|
49
|
+
"GA": NearestNeighborParameter(-8.2, -22.2),
|
|
50
|
+
"TC": NearestNeighborParameter(-8.2, -22.2),
|
|
51
|
+
"CG": NearestNeighborParameter(-10.6, -27.2),
|
|
52
|
+
"GC": NearestNeighborParameter(-9.8, -24.4),
|
|
53
|
+
"GG": NearestNeighborParameter(-8.0, -19.9),
|
|
54
|
+
"CC": NearestNeighborParameter(-8.0, -19.9),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
SANTALUCIA_1998 = NearestNeighborParameterSet(
|
|
58
|
+
name="SantaLucia unified DNA/DNA nearest-neighbor",
|
|
59
|
+
version="santalucia1998-v1",
|
|
60
|
+
citation=(
|
|
61
|
+
"SantaLucia J Jr. A unified view of polymer, dumbbell, and "
|
|
62
|
+
"oligonucleotide DNA nearest-neighbor thermodynamics. PNAS 1998."
|
|
63
|
+
),
|
|
64
|
+
doi="10.1073/pnas.95.4.1460",
|
|
65
|
+
reference_sodium_molar=1.0,
|
|
66
|
+
stacking=MappingProxyType(_STACKING),
|
|
67
|
+
terminal_at=NearestNeighborParameter(2.3, 4.1),
|
|
68
|
+
terminal_gc=NearestNeighborParameter(0.1, -2.8),
|
|
69
|
+
symmetry=NearestNeighborParameter(0.0, -1.4),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
PARAMETER_SETS = MappingProxyType({SANTALUCIA_1998.version: SANTALUCIA_1998})
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"PARAMETER_SETS",
|
|
76
|
+
"SANTALUCIA_1998",
|
|
77
|
+
"NearestNeighborParameter",
|
|
78
|
+
"NearestNeighborParameterSet",
|
|
79
|
+
]
|