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
dnakit/molbio/results.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""Immutable JSON-serializable molecular-biology simulation results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
from dnakit.core import DNASequence, Issue, Provenance, Strand
|
|
10
|
+
from dnakit.core._json import FrozenDict, to_json_compatible
|
|
11
|
+
from dnakit.exceptions import ConfigurationError
|
|
12
|
+
from dnakit.patterns.results import GuideCandidate
|
|
13
|
+
|
|
14
|
+
EndPolarity = Literal["blunt", "5prime", "3prime"]
|
|
15
|
+
EndSide = Literal["left", "right"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Serializable:
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
return cast(dict[str, Any], to_json_compatible(self))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, kw_only=True)
|
|
24
|
+
class MolBioResult(_Serializable):
|
|
25
|
+
"""Common audit envelope carried directly by every public result."""
|
|
26
|
+
|
|
27
|
+
method: str
|
|
28
|
+
algorithm_version: str
|
|
29
|
+
parameters: FrozenDict
|
|
30
|
+
provenance: Provenance
|
|
31
|
+
issues: tuple[Issue, ...]
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
for name in ("method", "algorithm_version"):
|
|
35
|
+
value = getattr(self, name)
|
|
36
|
+
if not isinstance(value, str) or not value.strip():
|
|
37
|
+
raise ConfigurationError(f"{name} must be a non-empty string.")
|
|
38
|
+
if not isinstance(self.parameters, FrozenDict):
|
|
39
|
+
raise ConfigurationError("parameters must be a FrozenDict.")
|
|
40
|
+
if not isinstance(self.provenance, Provenance):
|
|
41
|
+
raise ConfigurationError("provenance must be Provenance.")
|
|
42
|
+
if any(not isinstance(issue, Issue) for issue in self.issues):
|
|
43
|
+
raise ConfigurationError("issues must contain Issue objects.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EndDescriptor(_Serializable):
|
|
48
|
+
"""One abstract double-stranded fragment end in top-strand coordinates."""
|
|
49
|
+
|
|
50
|
+
polarity: EndPolarity
|
|
51
|
+
overhang_sequence_5to3: str
|
|
52
|
+
cohesive_key: str
|
|
53
|
+
side: EndSide
|
|
54
|
+
five_prime_phosphorylated: bool
|
|
55
|
+
top_cut: int | None
|
|
56
|
+
bottom_cut: int | None
|
|
57
|
+
source: str
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
if self.polarity not in ("blunt", "5prime", "3prime"):
|
|
61
|
+
raise ConfigurationError("Unknown end polarity.")
|
|
62
|
+
if self.side not in ("left", "right"):
|
|
63
|
+
raise ConfigurationError("End side must be left or right.")
|
|
64
|
+
if set(self.overhang_sequence_5to3) - set("ACGTRYSWKMBDHVN"):
|
|
65
|
+
raise ConfigurationError("End overhang must contain uppercase DNA IUPAC symbols.")
|
|
66
|
+
if self.polarity == "blunt" and self.overhang_sequence_5to3:
|
|
67
|
+
raise ConfigurationError("A blunt end cannot carry an overhang sequence.")
|
|
68
|
+
if self.polarity != "blunt" and not self.overhang_sequence_5to3:
|
|
69
|
+
raise ConfigurationError("A sticky end requires an overhang sequence.")
|
|
70
|
+
if set(self.cohesive_key) - set("ACGTRYSWKMBDHVN"):
|
|
71
|
+
raise ConfigurationError("End cohesive_key must contain uppercase DNA IUPAC symbols.")
|
|
72
|
+
if self.polarity == "blunt" and self.cohesive_key:
|
|
73
|
+
raise ConfigurationError("A blunt end must use an empty cohesive_key.")
|
|
74
|
+
if self.polarity != "blunt" and not self.cohesive_key:
|
|
75
|
+
raise ConfigurationError("A sticky end requires a cohesive_key.")
|
|
76
|
+
if not isinstance(self.five_prime_phosphorylated, bool):
|
|
77
|
+
raise ConfigurationError("five_prime_phosphorylated must be boolean.")
|
|
78
|
+
for name in ("top_cut", "bottom_cut"):
|
|
79
|
+
value = getattr(self, name)
|
|
80
|
+
if value is not None and (
|
|
81
|
+
isinstance(value, bool) or not isinstance(value, int) or value < 0
|
|
82
|
+
):
|
|
83
|
+
raise ConfigurationError(f"{name} must be a non-negative integer or None.")
|
|
84
|
+
if not isinstance(self.source, str) or not self.source.strip():
|
|
85
|
+
raise ConfigurationError("End source must be non-empty.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class DigestCut(_Serializable):
|
|
90
|
+
enzymes: tuple[str, ...]
|
|
91
|
+
top_cut: int
|
|
92
|
+
bottom_cut: int
|
|
93
|
+
polarity: EndPolarity
|
|
94
|
+
overhang_sequence_5to3: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class DigestFragment(_Serializable):
|
|
99
|
+
id: str
|
|
100
|
+
sequence: DNASequence
|
|
101
|
+
source_start: int
|
|
102
|
+
source_end: int
|
|
103
|
+
wraps_origin: bool
|
|
104
|
+
left_end: EndDescriptor
|
|
105
|
+
right_end: EndDescriptor
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class RestrictionDigestResult(MolBioResult):
|
|
110
|
+
source_length: int
|
|
111
|
+
source_topology: str
|
|
112
|
+
cuts: tuple[DigestCut, ...]
|
|
113
|
+
fragments: tuple[DigestFragment, ...]
|
|
114
|
+
coordinate_system: str = "0-based-half-open"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class EndTypeResult(MolBioResult):
|
|
119
|
+
end: EndDescriptor
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class LigationCompatibilityResult(MolBioResult):
|
|
124
|
+
compatible: bool
|
|
125
|
+
reason: str
|
|
126
|
+
left: EndDescriptor
|
|
127
|
+
right: EndDescriptor
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class LigationResult(MolBioResult):
|
|
132
|
+
product: DNASequence
|
|
133
|
+
fragment_ids: tuple[str, ...]
|
|
134
|
+
junction_count: int
|
|
135
|
+
circularized: bool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class PrimerBindingHit(_Serializable):
|
|
140
|
+
primer_name: str
|
|
141
|
+
strand: Strand
|
|
142
|
+
start: int
|
|
143
|
+
end: int
|
|
144
|
+
matched_template: str
|
|
145
|
+
mismatch_positions_5to3: tuple[int, ...]
|
|
146
|
+
three_prime_mismatch_count: int
|
|
147
|
+
wraps_origin: bool
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def mismatch_count(self) -> int:
|
|
151
|
+
return len(self.mismatch_positions_5to3)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class PrimerMatchResult(MolBioResult):
|
|
156
|
+
primer_sequence: str
|
|
157
|
+
template_length: int
|
|
158
|
+
template_topology: str
|
|
159
|
+
hits: tuple[PrimerBindingHit, ...]
|
|
160
|
+
truncated: bool
|
|
161
|
+
coordinate_system: str = "0-based-half-open"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class Amplicon(_Serializable):
|
|
166
|
+
sequence: DNASequence
|
|
167
|
+
template_start: int
|
|
168
|
+
template_end: int
|
|
169
|
+
wraps_origin: bool
|
|
170
|
+
forward_binding: PrimerBindingHit
|
|
171
|
+
reverse_binding: PrimerBindingHit
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class PCRResult(MolBioResult):
|
|
176
|
+
template_length: int
|
|
177
|
+
template_topology: str
|
|
178
|
+
amplicons: tuple[Amplicon, ...]
|
|
179
|
+
truncated: bool
|
|
180
|
+
coordinate_system: str = "0-based-half-open"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class ConditionalAnalysis(_Serializable):
|
|
185
|
+
capability: str
|
|
186
|
+
status: str
|
|
187
|
+
available: bool
|
|
188
|
+
backend_required: str
|
|
189
|
+
automatic_probe: bool
|
|
190
|
+
automatic_execution: bool
|
|
191
|
+
reason: str
|
|
192
|
+
execution_performed: bool = False
|
|
193
|
+
structure_found: bool | None = None
|
|
194
|
+
tm_celsius: float | None = None
|
|
195
|
+
delta_g_kcal_per_mol: float | None = None
|
|
196
|
+
delta_h_kcal_per_mol: float | None = None
|
|
197
|
+
delta_s_cal_per_k_mol: float | None = None
|
|
198
|
+
ascii_structure: str | None = None
|
|
199
|
+
backend_name: str | None = None
|
|
200
|
+
backend_version: str | None = None
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
for name in ("capability", "status", "backend_required", "reason"):
|
|
204
|
+
value = getattr(self, name)
|
|
205
|
+
if not isinstance(value, str) or not value.strip():
|
|
206
|
+
raise ConfigurationError(f"Conditional analysis {name} must be non-empty.")
|
|
207
|
+
for name in (
|
|
208
|
+
"available",
|
|
209
|
+
"automatic_probe",
|
|
210
|
+
"automatic_execution",
|
|
211
|
+
"execution_performed",
|
|
212
|
+
):
|
|
213
|
+
if not isinstance(getattr(self, name), bool):
|
|
214
|
+
raise ConfigurationError(f"Conditional analysis {name} must be boolean.")
|
|
215
|
+
if self.available != self.execution_performed:
|
|
216
|
+
raise ConfigurationError(
|
|
217
|
+
"Conditional analysis availability must match explicit execution status."
|
|
218
|
+
)
|
|
219
|
+
if self.structure_found is not None and not isinstance(self.structure_found, bool):
|
|
220
|
+
raise ConfigurationError("structure_found must be boolean or None.")
|
|
221
|
+
values = (
|
|
222
|
+
self.tm_celsius,
|
|
223
|
+
self.delta_g_kcal_per_mol,
|
|
224
|
+
self.delta_h_kcal_per_mol,
|
|
225
|
+
self.delta_s_cal_per_k_mol,
|
|
226
|
+
)
|
|
227
|
+
if any(value is not None and not math.isfinite(value) for value in values):
|
|
228
|
+
raise ConfigurationError("Structure thermodynamic values must be finite or None.")
|
|
229
|
+
if self.available and any(value is None for value in values):
|
|
230
|
+
raise ConfigurationError("An available structure analysis requires all numeric values.")
|
|
231
|
+
if not self.available and any(value is not None for value in values):
|
|
232
|
+
raise ConfigurationError("An unavailable structure analysis cannot contain values.")
|
|
233
|
+
if self.available and self.structure_found is None:
|
|
234
|
+
raise ConfigurationError("An available structure analysis requires structure_found.")
|
|
235
|
+
if not self.available and (
|
|
236
|
+
self.structure_found is not None
|
|
237
|
+
or self.ascii_structure is not None
|
|
238
|
+
or self.backend_name is not None
|
|
239
|
+
or self.backend_version is not None
|
|
240
|
+
):
|
|
241
|
+
raise ConfigurationError("An unavailable structure analysis cannot claim backend data.")
|
|
242
|
+
if self.available and (
|
|
243
|
+
not isinstance(self.backend_name, str) or not self.backend_name.strip()
|
|
244
|
+
):
|
|
245
|
+
raise ConfigurationError("An available structure analysis requires backend_name.")
|
|
246
|
+
for name in ("backend_name", "backend_version"):
|
|
247
|
+
value = getattr(self, name)
|
|
248
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
249
|
+
raise ConfigurationError(f"{name} must be a non-empty string or None.")
|
|
250
|
+
if self.ascii_structure is not None and (
|
|
251
|
+
not isinstance(self.ascii_structure, str) or len(self.ascii_structure) > 100_000
|
|
252
|
+
):
|
|
253
|
+
raise ConfigurationError("ascii_structure must be text within 100,000 characters.")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class PrimerPropertiesResult(MolBioResult):
|
|
258
|
+
primer_sequence: str
|
|
259
|
+
gc_fraction: float
|
|
260
|
+
tm_celsius: float
|
|
261
|
+
tm_method: str
|
|
262
|
+
hairpin: ConditionalAnalysis
|
|
263
|
+
self_dimer: ConditionalAnalysis
|
|
264
|
+
heterodimer: ConditionalAnalysis | None
|
|
265
|
+
paired_primer_sequence: str | None
|
|
266
|
+
paired_gc_fraction: float | None
|
|
267
|
+
paired_tm_celsius: float | None
|
|
268
|
+
paired_hairpin: ConditionalAnalysis | None
|
|
269
|
+
paired_self_dimer: ConditionalAnalysis | None
|
|
270
|
+
|
|
271
|
+
def __post_init__(self) -> None:
|
|
272
|
+
super().__post_init__()
|
|
273
|
+
if not math.isfinite(self.gc_fraction) or not 0.0 <= self.gc_fraction <= 1.0:
|
|
274
|
+
raise ConfigurationError("gc_fraction must be finite and between 0 and 1.")
|
|
275
|
+
if not math.isfinite(self.tm_celsius):
|
|
276
|
+
raise ConfigurationError("tm_celsius must be finite.")
|
|
277
|
+
missing_pair = self.paired_primer_sequence is None
|
|
278
|
+
if missing_pair != (self.paired_gc_fraction is None) or missing_pair != (
|
|
279
|
+
self.paired_tm_celsius is None
|
|
280
|
+
):
|
|
281
|
+
raise ConfigurationError("Paired primer sequence, GC, and Tm must be set together.")
|
|
282
|
+
if self.paired_gc_fraction is not None and (
|
|
283
|
+
not math.isfinite(self.paired_gc_fraction) or not 0.0 <= self.paired_gc_fraction <= 1.0
|
|
284
|
+
):
|
|
285
|
+
raise ConfigurationError("paired_gc_fraction must be finite and between 0 and 1.")
|
|
286
|
+
if self.paired_tm_celsius is not None and not math.isfinite(self.paired_tm_celsius):
|
|
287
|
+
raise ConfigurationError("paired_tm_celsius must be finite.")
|
|
288
|
+
if missing_pair != (self.paired_hairpin is None) or missing_pair != (
|
|
289
|
+
self.paired_self_dimer is None
|
|
290
|
+
):
|
|
291
|
+
raise ConfigurationError(
|
|
292
|
+
"Paired hairpin and self-dimer analyses must follow paired primer presence."
|
|
293
|
+
)
|
|
294
|
+
if missing_pair != (self.heterodimer is None):
|
|
295
|
+
raise ConfigurationError("Heterodimer analysis must follow paired primer presence.")
|
|
296
|
+
analyses = (
|
|
297
|
+
self.hairpin,
|
|
298
|
+
self.self_dimer,
|
|
299
|
+
self.heterodimer,
|
|
300
|
+
self.paired_hairpin,
|
|
301
|
+
self.paired_self_dimer,
|
|
302
|
+
)
|
|
303
|
+
if any(
|
|
304
|
+
analysis is not None and not isinstance(analysis, ConditionalAnalysis)
|
|
305
|
+
for analysis in analyses
|
|
306
|
+
):
|
|
307
|
+
raise ConfigurationError("Primer structure fields must be ConditionalAnalysis values.")
|
|
308
|
+
expected_capabilities = (
|
|
309
|
+
(self.hairpin, "hairpin"),
|
|
310
|
+
(self.self_dimer, "self_dimer"),
|
|
311
|
+
(self.heterodimer, "heterodimer"),
|
|
312
|
+
(self.paired_hairpin, "hairpin"),
|
|
313
|
+
(self.paired_self_dimer, "self_dimer"),
|
|
314
|
+
)
|
|
315
|
+
if any(
|
|
316
|
+
analysis is not None and analysis.capability != capability
|
|
317
|
+
for analysis, capability in expected_capabilities
|
|
318
|
+
):
|
|
319
|
+
raise ConfigurationError("Primer structure capability labels are inconsistent.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass(frozen=True)
|
|
323
|
+
class PrimerDesignRequest(_Serializable):
|
|
324
|
+
template: DNASequence
|
|
325
|
+
target_start: int
|
|
326
|
+
target_end: int
|
|
327
|
+
primer_length_range: tuple[int, int]
|
|
328
|
+
tm_range_celsius: tuple[float, float]
|
|
329
|
+
gc_range: tuple[float, float]
|
|
330
|
+
product_length_range: tuple[int, int]
|
|
331
|
+
excluded_regions: tuple[tuple[int, int], ...]
|
|
332
|
+
candidate_count: int
|
|
333
|
+
thermodynamic_conditions: FrozenDict
|
|
334
|
+
|
|
335
|
+
def __post_init__(self) -> None:
|
|
336
|
+
if not isinstance(self.template, DNASequence):
|
|
337
|
+
raise ConfigurationError("Primer design template must be DNASequence.")
|
|
338
|
+
length = self.template.symbol_length
|
|
339
|
+
if not 0 <= self.target_start < self.target_end <= length:
|
|
340
|
+
raise ConfigurationError("Primer design target interval is invalid.")
|
|
341
|
+
for name, integer_bounds in (
|
|
342
|
+
("primer_length_range", self.primer_length_range),
|
|
343
|
+
("product_length_range", self.product_length_range),
|
|
344
|
+
):
|
|
345
|
+
if (
|
|
346
|
+
len(integer_bounds) != 2
|
|
347
|
+
or any(
|
|
348
|
+
isinstance(item, bool) or not isinstance(item, int) for item in integer_bounds
|
|
349
|
+
)
|
|
350
|
+
or integer_bounds[0] <= 0
|
|
351
|
+
or integer_bounds[0] > integer_bounds[1]
|
|
352
|
+
):
|
|
353
|
+
raise ConfigurationError(f"{name} is invalid.")
|
|
354
|
+
for name, float_bounds in (
|
|
355
|
+
("tm_range_celsius", self.tm_range_celsius),
|
|
356
|
+
("gc_range", self.gc_range),
|
|
357
|
+
):
|
|
358
|
+
if (
|
|
359
|
+
len(float_bounds) != 2
|
|
360
|
+
or any(not math.isfinite(item) for item in float_bounds)
|
|
361
|
+
or float_bounds[0] > float_bounds[1]
|
|
362
|
+
):
|
|
363
|
+
raise ConfigurationError(f"{name} is invalid.")
|
|
364
|
+
if not 0.0 <= self.gc_range[0] <= self.gc_range[1] <= 1.0:
|
|
365
|
+
raise ConfigurationError("gc_range must lie between zero and one.")
|
|
366
|
+
for start, end in self.excluded_regions:
|
|
367
|
+
if not 0 <= start < end <= length:
|
|
368
|
+
raise ConfigurationError("An excluded primer-design interval is invalid.")
|
|
369
|
+
if (
|
|
370
|
+
isinstance(self.candidate_count, bool)
|
|
371
|
+
or not isinstance(self.candidate_count, int)
|
|
372
|
+
or self.candidate_count <= 0
|
|
373
|
+
):
|
|
374
|
+
raise ConfigurationError("candidate_count must be positive.")
|
|
375
|
+
if not isinstance(self.thermodynamic_conditions, FrozenDict):
|
|
376
|
+
raise ConfigurationError("thermodynamic_conditions must be FrozenDict.")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True)
|
|
380
|
+
class PrimerDesignCandidate(_Serializable):
|
|
381
|
+
"""One bounded Primer3 primer-pair candidate in template coordinates."""
|
|
382
|
+
|
|
383
|
+
rank: int
|
|
384
|
+
left_primer_sequence: str
|
|
385
|
+
right_primer_sequence: str
|
|
386
|
+
left_start: int
|
|
387
|
+
left_end: int
|
|
388
|
+
right_start: int
|
|
389
|
+
right_end: int
|
|
390
|
+
product_size: int
|
|
391
|
+
left_tm_celsius: float
|
|
392
|
+
right_tm_celsius: float
|
|
393
|
+
left_gc_fraction: float
|
|
394
|
+
right_gc_fraction: float
|
|
395
|
+
pair_penalty: float | None
|
|
396
|
+
|
|
397
|
+
def __post_init__(self) -> None:
|
|
398
|
+
if isinstance(self.rank, bool) or not isinstance(self.rank, int) or self.rank < 0:
|
|
399
|
+
raise ConfigurationError("Primer candidate rank must be a non-negative integer.")
|
|
400
|
+
for name in ("left_primer_sequence", "right_primer_sequence"):
|
|
401
|
+
value = getattr(self, name)
|
|
402
|
+
if (
|
|
403
|
+
not isinstance(value, str)
|
|
404
|
+
or not value
|
|
405
|
+
or len(value) > 1_000
|
|
406
|
+
or set(value) - set("ACGT")
|
|
407
|
+
):
|
|
408
|
+
raise ConfigurationError(f"{name} must be bounded canonical DNA.")
|
|
409
|
+
coordinates = (self.left_start, self.left_end, self.right_start, self.right_end)
|
|
410
|
+
if any(isinstance(value, bool) or not isinstance(value, int) for value in coordinates):
|
|
411
|
+
raise ConfigurationError("Primer candidate coordinates must be integers.")
|
|
412
|
+
if not 0 <= self.left_start < self.left_end <= self.right_start < self.right_end:
|
|
413
|
+
raise ConfigurationError("Primer candidate intervals are invalid or overlap.")
|
|
414
|
+
if (
|
|
415
|
+
isinstance(self.product_size, bool)
|
|
416
|
+
or not isinstance(self.product_size, int)
|
|
417
|
+
or self.product_size <= 0
|
|
418
|
+
or self.product_size != self.right_end - self.left_start
|
|
419
|
+
):
|
|
420
|
+
raise ConfigurationError("Primer candidate product_size is inconsistent.")
|
|
421
|
+
for name in ("left_tm_celsius", "right_tm_celsius"):
|
|
422
|
+
if not math.isfinite(getattr(self, name)):
|
|
423
|
+
raise ConfigurationError(f"{name} must be finite.")
|
|
424
|
+
for name in ("left_gc_fraction", "right_gc_fraction"):
|
|
425
|
+
value = getattr(self, name)
|
|
426
|
+
if not math.isfinite(value) or not 0.0 <= value <= 1.0:
|
|
427
|
+
raise ConfigurationError(f"{name} must be finite and between zero and one.")
|
|
428
|
+
if self.pair_penalty is not None and not math.isfinite(self.pair_penalty):
|
|
429
|
+
raise ConfigurationError("pair_penalty must be finite or None.")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@dataclass(frozen=True)
|
|
433
|
+
class PrimerDesignInterfaceResult(MolBioResult):
|
|
434
|
+
request: PrimerDesignRequest
|
|
435
|
+
backend_name: str
|
|
436
|
+
status: str
|
|
437
|
+
execution_performed: bool
|
|
438
|
+
candidates: tuple[PrimerDesignCandidate, ...]
|
|
439
|
+
reason: str
|
|
440
|
+
|
|
441
|
+
def __post_init__(self) -> None:
|
|
442
|
+
super().__post_init__()
|
|
443
|
+
if not isinstance(self.request, PrimerDesignRequest):
|
|
444
|
+
raise ConfigurationError("Primer design result requires a PrimerDesignRequest.")
|
|
445
|
+
for name in ("backend_name", "status", "reason"):
|
|
446
|
+
value = getattr(self, name)
|
|
447
|
+
if not isinstance(value, str) or not value.strip():
|
|
448
|
+
raise ConfigurationError(f"Primer design {name} must be non-empty.")
|
|
449
|
+
if not isinstance(self.execution_performed, bool):
|
|
450
|
+
raise ConfigurationError("execution_performed must be boolean.")
|
|
451
|
+
if any(not isinstance(candidate, PrimerDesignCandidate) for candidate in self.candidates):
|
|
452
|
+
raise ConfigurationError("candidates must contain PrimerDesignCandidate values.")
|
|
453
|
+
if len(self.candidates) > self.request.candidate_count:
|
|
454
|
+
raise ConfigurationError("Candidate output exceeds the requested candidate count.")
|
|
455
|
+
if self.execution_performed != self.status.startswith("execution-complete"):
|
|
456
|
+
raise ConfigurationError("Primer design status disagrees with execution_performed.")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@dataclass(frozen=True)
|
|
460
|
+
class AssemblyStep(_Serializable):
|
|
461
|
+
index: int
|
|
462
|
+
left_fragment_id: str
|
|
463
|
+
right_fragment_id: str
|
|
464
|
+
operation: str
|
|
465
|
+
junction_sequence: str
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass(frozen=True)
|
|
469
|
+
class AssemblyResult(MolBioResult):
|
|
470
|
+
assembly_method: str
|
|
471
|
+
product: DNASequence
|
|
472
|
+
steps: tuple[AssemblyStep, ...]
|
|
473
|
+
circularized: bool
|
|
474
|
+
complete: bool
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@dataclass(frozen=True)
|
|
478
|
+
class OffTargetHit(_Serializable):
|
|
479
|
+
guide_sequence: str
|
|
480
|
+
reference_id: str
|
|
481
|
+
strand: Strand
|
|
482
|
+
start: int
|
|
483
|
+
end: int
|
|
484
|
+
mismatch_positions_5to3: tuple[int, ...]
|
|
485
|
+
wraps_origin: bool
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@dataclass(frozen=True)
|
|
489
|
+
class CrisprScanResult(MolBioResult):
|
|
490
|
+
candidates: tuple[GuideCandidate, ...]
|
|
491
|
+
off_targets: tuple[OffTargetHit, ...]
|
|
492
|
+
candidate_truncated: bool
|
|
493
|
+
off_target_truncated: bool
|
|
494
|
+
efficiency_prediction_performed: bool
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@dataclass(frozen=True)
|
|
498
|
+
class SequenceChange(_Serializable):
|
|
499
|
+
position: int
|
|
500
|
+
before: str
|
|
501
|
+
after: str
|
|
502
|
+
reason: str
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@dataclass(frozen=True)
|
|
506
|
+
class RuleOptimizationResult(MolBioResult):
|
|
507
|
+
original: DNASequence
|
|
508
|
+
optimized: DNASequence
|
|
509
|
+
changes: tuple[SequenceChange, ...]
|
|
510
|
+
initial_score: float
|
|
511
|
+
final_score: float
|
|
512
|
+
iterations: int
|
|
513
|
+
constraints_satisfied: bool
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@dataclass(frozen=True)
|
|
517
|
+
class CodonOptimizationResult(MolBioResult):
|
|
518
|
+
original: DNASequence
|
|
519
|
+
optimized: DNASequence
|
|
520
|
+
original_translation: str
|
|
521
|
+
optimized_translation: str
|
|
522
|
+
changes: tuple[SequenceChange, ...]
|
|
523
|
+
original_cai: float
|
|
524
|
+
optimized_cai: float
|
|
525
|
+
original_gc_fraction: float
|
|
526
|
+
optimized_gc_fraction: float
|
|
527
|
+
usage_table_name: str
|
|
528
|
+
usage_table_version: str
|
|
529
|
+
usage_table_checksum: str
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@dataclass(frozen=True)
|
|
533
|
+
class Mutation(_Serializable):
|
|
534
|
+
position: int
|
|
535
|
+
reference: str
|
|
536
|
+
alternate: str
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@dataclass(frozen=True)
|
|
540
|
+
class SequenceVariant(_Serializable):
|
|
541
|
+
id: str
|
|
542
|
+
sequence: DNASequence
|
|
543
|
+
mutations: tuple[Mutation, ...]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@dataclass(frozen=True)
|
|
547
|
+
class MutationLibraryResult(MolBioResult):
|
|
548
|
+
source: DNASequence
|
|
549
|
+
variants: tuple[SequenceVariant, ...]
|
|
550
|
+
total_possible_variants: int
|
|
551
|
+
sampled: bool
|
|
552
|
+
seed: int
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
__all__ = [
|
|
556
|
+
"Amplicon",
|
|
557
|
+
"AssemblyResult",
|
|
558
|
+
"AssemblyStep",
|
|
559
|
+
"CodonOptimizationResult",
|
|
560
|
+
"ConditionalAnalysis",
|
|
561
|
+
"CrisprScanResult",
|
|
562
|
+
"DigestCut",
|
|
563
|
+
"DigestFragment",
|
|
564
|
+
"EndDescriptor",
|
|
565
|
+
"EndTypeResult",
|
|
566
|
+
"LigationCompatibilityResult",
|
|
567
|
+
"LigationResult",
|
|
568
|
+
"MolBioResult",
|
|
569
|
+
"Mutation",
|
|
570
|
+
"MutationLibraryResult",
|
|
571
|
+
"OffTargetHit",
|
|
572
|
+
"PCRResult",
|
|
573
|
+
"PrimerBindingHit",
|
|
574
|
+
"PrimerDesignCandidate",
|
|
575
|
+
"PrimerDesignInterfaceResult",
|
|
576
|
+
"PrimerDesignRequest",
|
|
577
|
+
"PrimerMatchResult",
|
|
578
|
+
"PrimerPropertiesResult",
|
|
579
|
+
"RestrictionDigestResult",
|
|
580
|
+
"RuleOptimizationResult",
|
|
581
|
+
"SequenceChange",
|
|
582
|
+
"SequenceVariant",
|
|
583
|
+
]
|