dnakit 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ ]