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,101 @@
1
+ """Deterministic molecular-biology simulation and rule-design APIs."""
2
+
3
+ from .assembly import AssemblyFragment, simulate_assembly
4
+ from .crispr import scan_crispr_candidates
5
+ from .optimization import (
6
+ CodonUsageTable,
7
+ RuleOptimizationConfig,
8
+ generate_mutation_library,
9
+ optimize_codons,
10
+ optimize_protein_codons,
11
+ optimize_sequence_rules,
12
+ )
13
+ from .primer3_design import Primer3CLIDesignAdapter
14
+ from .primers import match_primer, prepare_primer_design, primer_properties, simulate_pcr
15
+ from .restriction import (
16
+ LigationFragment,
17
+ as_ligation_fragment,
18
+ check_end_compatibility,
19
+ classify_restriction_end,
20
+ digest_restriction,
21
+ ligate_fragments,
22
+ )
23
+ from .results import (
24
+ Amplicon,
25
+ AssemblyResult,
26
+ AssemblyStep,
27
+ CodonOptimizationResult,
28
+ ConditionalAnalysis,
29
+ CrisprScanResult,
30
+ DigestCut,
31
+ DigestFragment,
32
+ EndDescriptor,
33
+ EndTypeResult,
34
+ LigationCompatibilityResult,
35
+ LigationResult,
36
+ MolBioResult,
37
+ Mutation,
38
+ MutationLibraryResult,
39
+ OffTargetHit,
40
+ PCRResult,
41
+ PrimerBindingHit,
42
+ PrimerDesignCandidate,
43
+ PrimerDesignInterfaceResult,
44
+ PrimerDesignRequest,
45
+ PrimerMatchResult,
46
+ PrimerPropertiesResult,
47
+ RestrictionDigestResult,
48
+ RuleOptimizationResult,
49
+ SequenceChange,
50
+ SequenceVariant,
51
+ )
52
+
53
+ __all__ = [
54
+ "Amplicon",
55
+ "AssemblyFragment",
56
+ "AssemblyResult",
57
+ "AssemblyStep",
58
+ "CodonOptimizationResult",
59
+ "CodonUsageTable",
60
+ "ConditionalAnalysis",
61
+ "CrisprScanResult",
62
+ "DigestCut",
63
+ "DigestFragment",
64
+ "EndDescriptor",
65
+ "EndTypeResult",
66
+ "LigationCompatibilityResult",
67
+ "LigationFragment",
68
+ "LigationResult",
69
+ "MolBioResult",
70
+ "Mutation",
71
+ "MutationLibraryResult",
72
+ "OffTargetHit",
73
+ "PCRResult",
74
+ "Primer3CLIDesignAdapter",
75
+ "PrimerBindingHit",
76
+ "PrimerDesignCandidate",
77
+ "PrimerDesignInterfaceResult",
78
+ "PrimerDesignRequest",
79
+ "PrimerMatchResult",
80
+ "PrimerPropertiesResult",
81
+ "RestrictionDigestResult",
82
+ "RuleOptimizationConfig",
83
+ "RuleOptimizationResult",
84
+ "SequenceChange",
85
+ "SequenceVariant",
86
+ "as_ligation_fragment",
87
+ "check_end_compatibility",
88
+ "classify_restriction_end",
89
+ "digest_restriction",
90
+ "generate_mutation_library",
91
+ "ligate_fragments",
92
+ "match_primer",
93
+ "optimize_codons",
94
+ "optimize_protein_codons",
95
+ "optimize_sequence_rules",
96
+ "prepare_primer_design",
97
+ "primer_properties",
98
+ "scan_crispr_candidates",
99
+ "simulate_assembly",
100
+ "simulate_pcr",
101
+ ]
@@ -0,0 +1,266 @@
1
+ """Shared validation and audit helpers for molecular-biology simulations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Iterable, Mapping
7
+ from typing import TypeVar
8
+
9
+ from dnakit.core import (
10
+ DNA,
11
+ DNASequence,
12
+ ExecutionMode,
13
+ ImplementationInfo,
14
+ ImplementationLabel,
15
+ Issue,
16
+ OriginClass,
17
+ Provenance,
18
+ ReferenceInfo,
19
+ Topology,
20
+ )
21
+ from dnakit.core._json import FrozenDict, freeze_mapping
22
+ from dnakit.core.facade import resolve_single_dna
23
+ from dnakit.exceptions import ConfigurationError, InvalidAlphabetError, UnsupportedGapOperationError
24
+
25
+ _IUPAC = frozenset("ACGTRYSWKMBDHVN")
26
+ _CANONICAL = frozenset("ACGT")
27
+ T = TypeVar("T")
28
+ _COMPLEMENT = str.maketrans(
29
+ {
30
+ "A": "T",
31
+ "C": "G",
32
+ "G": "C",
33
+ "T": "A",
34
+ "R": "Y",
35
+ "Y": "R",
36
+ "S": "S",
37
+ "W": "W",
38
+ "K": "M",
39
+ "M": "K",
40
+ "B": "V",
41
+ "D": "H",
42
+ "H": "D",
43
+ "V": "B",
44
+ "N": "N",
45
+ }
46
+ )
47
+
48
+ IUPAC_BASES: Mapping[str, frozenset[str]] = {
49
+ "A": frozenset("A"),
50
+ "C": frozenset("C"),
51
+ "G": frozenset("G"),
52
+ "T": frozenset("T"),
53
+ "R": frozenset("AG"),
54
+ "Y": frozenset("CT"),
55
+ "S": frozenset("CG"),
56
+ "W": frozenset("AT"),
57
+ "K": frozenset("GT"),
58
+ "M": frozenset("AC"),
59
+ "B": frozenset("CGT"),
60
+ "D": frozenset("AGT"),
61
+ "H": frozenset("ACT"),
62
+ "V": frozenset("ACG"),
63
+ "N": frozenset("ACGT"),
64
+ }
65
+
66
+
67
+ def validate_positive_int(value: object, name: str, *, maximum: int | None = None) -> int:
68
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
69
+ raise ConfigurationError(f"{name} must be a positive integer.", context={name: value})
70
+ if maximum is not None and value > maximum:
71
+ raise ConfigurationError(
72
+ f"{name} exceeds its supported maximum.",
73
+ context={name: value, "maximum": maximum},
74
+ )
75
+ return value
76
+
77
+
78
+ def validate_non_negative_int(value: object, name: str) -> int:
79
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
80
+ raise ConfigurationError(f"{name} must be a non-negative integer.", context={name: value})
81
+ return value
82
+
83
+
84
+ def finite_fraction(value: object, name: str) -> float:
85
+ if (
86
+ isinstance(value, bool)
87
+ or not isinstance(value, (int, float))
88
+ or not math.isfinite(value)
89
+ or not 0.0 <= value <= 1.0
90
+ ):
91
+ raise ConfigurationError(f"{name} must be finite and between 0 and 1.")
92
+ return float(value)
93
+
94
+
95
+ def validate_text(value: object, name: str) -> str:
96
+ if not isinstance(value, str) or not value.strip():
97
+ raise ConfigurationError(f"{name} must be a non-empty string.")
98
+ return value
99
+
100
+
101
+ def materialize_bounded(
102
+ values: Iterable[T],
103
+ *,
104
+ max_items: int,
105
+ name: str,
106
+ reject_text: bool = True,
107
+ ) -> tuple[T, ...]:
108
+ """Materialize a finite iterable under an explicit item limit."""
109
+
110
+ validate_positive_int(max_items, f"max_{name}", maximum=100_000_000)
111
+ if reject_text and isinstance(values, (str, bytes)):
112
+ raise ConfigurationError(f"{name} must be a non-text iterable.")
113
+ resolved: list[T] = []
114
+ for item in values:
115
+ if len(resolved) >= max_items:
116
+ raise ConfigurationError(f"{name} exceeds its configured item limit.")
117
+ resolved.append(item)
118
+ return tuple(resolved)
119
+
120
+
121
+ def validate_iupac_text(value: object, name: str, *, allow_empty: bool = False) -> str:
122
+ if not isinstance(value, str):
123
+ raise ConfigurationError(f"{name} must be a DNA string.")
124
+ resolved = value.upper()
125
+ if not resolved and not allow_empty:
126
+ raise ConfigurationError(f"{name} must be non-empty.")
127
+ invalid = sorted(set(resolved) - _IUPAC)
128
+ if invalid:
129
+ raise InvalidAlphabetError(
130
+ f"{name} contains symbols outside the DNA IUPAC alphabet.",
131
+ context={"invalid_symbols": invalid},
132
+ )
133
+ return resolved
134
+
135
+
136
+ def require_sequence(
137
+ sequence: DNA | DNASequence,
138
+ *,
139
+ operation: str,
140
+ max_length: int,
141
+ canonical: bool = False,
142
+ allow_circular: bool = True,
143
+ allow_empty: bool = False,
144
+ ) -> str:
145
+ if isinstance(sequence, DNA):
146
+ sequence, _ = resolve_single_dna(sequence)
147
+ if not isinstance(sequence, DNASequence):
148
+ raise ConfigurationError(
149
+ f"{operation} requires a DNASequence.",
150
+ context={"input_type": type(sequence).__name__},
151
+ )
152
+ validate_positive_int(max_length, "max_length", maximum=100_000_000)
153
+ if sequence.is_gapped:
154
+ raise UnsupportedGapOperationError(
155
+ f"{operation} cannot silently omit explicit Gap objects.",
156
+ code="UNSUPPORTED_GAPPED_MOLBIO_OPERATION",
157
+ )
158
+ if not allow_circular and sequence.topology is Topology.CIRCULAR:
159
+ raise ConfigurationError(
160
+ f"{operation} requires linear topology.",
161
+ code="CIRCULAR_MOLBIO_OPERATION_UNSUPPORTED",
162
+ )
163
+ symbols = sequence.symbols
164
+ if not symbols and not allow_empty:
165
+ raise ConfigurationError(f"{operation} requires a non-empty sequence.")
166
+ if len(symbols) > max_length:
167
+ raise ConfigurationError(
168
+ f"{operation} input exceeds max_length.",
169
+ code="MOLBIO_SEQUENCE_LIMIT_EXCEEDED",
170
+ context={"sequence_length": len(symbols), "max_length": max_length},
171
+ )
172
+ if canonical and set(symbols) - _CANONICAL:
173
+ raise InvalidAlphabetError(
174
+ f"{operation} requires canonical A/C/G/T symbols.",
175
+ code="AMBIGUOUS_MOLBIO_SEQUENCE_UNSUPPORTED",
176
+ )
177
+ return symbols
178
+
179
+
180
+ def reverse_complement_text(value: str) -> str:
181
+ return value.translate(_COMPLEMENT)[::-1]
182
+
183
+
184
+ def iupac_compatible(left: str, right: str) -> bool:
185
+ return bool(IUPAC_BASES[left] & IUPAC_BASES[right])
186
+
187
+
188
+ def circular_slice(symbols: str, start: int, length: int) -> str:
189
+ if length < 0 or length > len(symbols):
190
+ raise ConfigurationError("Circular slice length must be within one template traversal.")
191
+ if not symbols:
192
+ return ""
193
+ start %= len(symbols)
194
+ return (symbols + symbols)[start : start + length]
195
+
196
+
197
+ def native_provenance(
198
+ *,
199
+ reimplementation: bool = False,
200
+ reference_name: str | None = None,
201
+ reference_version: str | None = None,
202
+ reference_checksum: str | None = None,
203
+ ) -> Provenance:
204
+ return Provenance(
205
+ implementation=ImplementationInfo(
206
+ label=(
207
+ ImplementationLabel.REIMPLEMENTATION
208
+ if reimplementation
209
+ else ImplementationLabel.NATIVE
210
+ ),
211
+ execution_mode=ExecutionMode.INTERNAL,
212
+ origin_class=(
213
+ OriginClass.PUBLISHED_ALGORITHM if reimplementation else OriginClass.DNAKIT
214
+ ),
215
+ ),
216
+ reference=(
217
+ None
218
+ if reference_name is None
219
+ else ReferenceInfo(
220
+ reference_name,
221
+ version=reference_version,
222
+ checksum=reference_checksum,
223
+ )
224
+ ),
225
+ )
226
+
227
+
228
+ def adapter_provenance(*, reference_name: str, reference_version: str | None = None) -> Provenance:
229
+ return Provenance(
230
+ implementation=ImplementationInfo(
231
+ label=ImplementationLabel.ADAPTER,
232
+ execution_mode=ExecutionMode.EXTERNAL,
233
+ origin_class=OriginClass.INTEGRATION,
234
+ ),
235
+ reference=ReferenceInfo(reference_name, version=reference_version),
236
+ )
237
+
238
+
239
+ def freeze_parameters(values: Mapping[str, object]) -> FrozenDict:
240
+ return freeze_mapping(values)
241
+
242
+
243
+ def validate_issues(issues: Iterable[Issue]) -> tuple[Issue, ...]:
244
+ resolved = tuple(issues)
245
+ if any(not isinstance(issue, Issue) for issue in resolved):
246
+ raise ConfigurationError("issues must contain only Issue objects.")
247
+ return resolved
248
+
249
+
250
+ __all__ = [
251
+ "IUPAC_BASES",
252
+ "adapter_provenance",
253
+ "circular_slice",
254
+ "finite_fraction",
255
+ "freeze_parameters",
256
+ "iupac_compatible",
257
+ "materialize_bounded",
258
+ "native_provenance",
259
+ "require_sequence",
260
+ "reverse_complement_text",
261
+ "validate_issues",
262
+ "validate_iupac_text",
263
+ "validate_non_negative_int",
264
+ "validate_positive_int",
265
+ "validate_text",
266
+ ]
@@ -0,0 +1,279 @@
1
+ """Deterministic ordered assembly simulations with explicit method boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+ from itertools import pairwise
8
+
9
+ from dnakit.core import DNAAlphabet, DNASequence, Issue, IssueSeverity, Topology
10
+ from dnakit.exceptions import ConfigurationError
11
+
12
+ from ._shared import (
13
+ freeze_parameters,
14
+ materialize_bounded,
15
+ native_provenance,
16
+ require_sequence,
17
+ validate_positive_int,
18
+ validate_text,
19
+ )
20
+ from .restriction import LigationFragment, as_ligation_fragment, ligate_fragments
21
+ from .results import AssemblyResult, AssemblyStep, DigestFragment
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class AssemblyFragment:
26
+ """Named linear fragment accepted by overlap-based assembly."""
27
+
28
+ id: str
29
+ sequence: DNASequence
30
+
31
+ def __post_init__(self) -> None:
32
+ validate_text(self.id, "assembly fragment id")
33
+ require_sequence(
34
+ self.sequence,
35
+ operation="assembly fragment construction",
36
+ max_length=100_000_000,
37
+ allow_circular=False,
38
+ )
39
+
40
+
41
+ def _overlap_fragment(
42
+ value: DNASequence | AssemblyFragment | DigestFragment | LigationFragment,
43
+ index: int,
44
+ ) -> AssemblyFragment:
45
+ if isinstance(value, AssemblyFragment):
46
+ return value
47
+ if isinstance(value, DNASequence):
48
+ return AssemblyFragment(f"fragment_{index}", value)
49
+ if isinstance(value, (DigestFragment, LigationFragment)):
50
+ return AssemblyFragment(value.id, value.sequence)
51
+ raise ConfigurationError("Unsupported assembly fragment type.")
52
+
53
+
54
+ def _longest_overlap(left: str, right: str, minimum: int, maximum: int) -> str:
55
+ upper = min(maximum, len(left), len(right))
56
+ for length in range(upper, minimum - 1, -1):
57
+ if left[-length:] == right[:length]:
58
+ return right[:length]
59
+ return ""
60
+
61
+
62
+ def _resolve_overlap(
63
+ left: str,
64
+ right: str,
65
+ *,
66
+ explicit: str | None,
67
+ minimum: int,
68
+ maximum: int,
69
+ ) -> str:
70
+ if explicit is not None:
71
+ if not isinstance(explicit, str) or set(explicit) - set("ACGTRYSWKMBDHVN"):
72
+ raise ConfigurationError("Explicit overlaps must be uppercase DNA IUPAC strings.")
73
+ if not minimum <= len(explicit) <= maximum:
74
+ raise ConfigurationError("Explicit overlap length lies outside configured bounds.")
75
+ if not left.endswith(explicit) or not right.startswith(explicit):
76
+ raise ConfigurationError("Explicit overlap is not an exact suffix/prefix junction.")
77
+ return explicit
78
+ overlap = _longest_overlap(left, right, minimum, maximum)
79
+ if not overlap:
80
+ raise ConfigurationError(
81
+ "No exact overlap satisfies the configured assembly boundary.",
82
+ code="ASSEMBLY_OVERLAP_NOT_FOUND",
83
+ )
84
+ return overlap
85
+
86
+
87
+ def simulate_assembly(
88
+ fragments: Iterable[DNASequence | AssemblyFragment | DigestFragment | LigationFragment],
89
+ *,
90
+ method: str,
91
+ overlaps: Iterable[str] | None = None,
92
+ min_overlap: int = 20,
93
+ max_overlap: int = 200,
94
+ circularize: bool = False,
95
+ allow_blunt: bool = False,
96
+ cycles: int = 1,
97
+ max_fragments: int = 1_000,
98
+ max_product_length: int = 100_000_000,
99
+ ) -> AssemblyResult:
100
+ """Simulate ordered Gibson/LCR overlaps or pre-digested GG/BioBrick ligation.
101
+
102
+ Golden Gate and BioBrick inputs must already carry validated end descriptors;
103
+ enzyme cutting, reaction kinetics, scar optimization, and cycle yield are not
104
+ inferred by this function.
105
+ """
106
+
107
+ normalized_method = method.lower() if isinstance(method, str) else ""
108
+ if normalized_method not in ("gibson", "lcr", "golden_gate", "biobrick"):
109
+ raise ConfigurationError("method must be gibson, lcr, golden_gate, or biobrick.")
110
+ validate_positive_int(max_fragments, "max_fragments", maximum=100_000)
111
+ validate_positive_int(max_product_length, "max_product_length", maximum=100_000_000)
112
+ validate_positive_int(cycles, "cycles", maximum=10_000)
113
+ if not isinstance(circularize, bool) or not isinstance(allow_blunt, bool):
114
+ raise ConfigurationError("Assembly boolean controls must be booleans.")
115
+ materialized: list[DNASequence | AssemblyFragment | DigestFragment | LigationFragment] = []
116
+ for item in fragments:
117
+ if len(materialized) >= max_fragments:
118
+ raise ConfigurationError("Assembly input exceeds max_fragments.")
119
+ materialized.append(item)
120
+ if len(materialized) < 2:
121
+ raise ConfigurationError("Assembly requires at least two fragments.")
122
+ issues = (
123
+ Issue(
124
+ "ASSEMBLY_KINETICS_NOT_MODELED",
125
+ IssueSeverity.INFO,
126
+ "The result is a deterministic sequence construction, not a reaction-yield model.",
127
+ ),
128
+ )
129
+ steps: list[AssemblyStep] = []
130
+ if normalized_method in ("golden_gate", "biobrick"):
131
+ ligation_inputs: list[LigationFragment] = []
132
+ for item in materialized:
133
+ if not isinstance(item, (DigestFragment, LigationFragment)):
134
+ raise ConfigurationError(
135
+ "Golden Gate/BioBrick simulation requires pre-digested fragments with ends.",
136
+ code="PREDIGESTED_ASSEMBLY_INPUT_REQUIRED",
137
+ )
138
+ ligation_inputs.append(as_ligation_fragment(item))
139
+ ligation = ligate_fragments(
140
+ ligation_inputs,
141
+ circularize=circularize,
142
+ allow_blunt=allow_blunt,
143
+ max_fragments=max_fragments,
144
+ max_product_length=max_product_length,
145
+ )
146
+ pairs = list(pairwise(ligation_inputs))
147
+ if circularize:
148
+ pairs.append((ligation_inputs[-1], ligation_inputs[0]))
149
+ for index, (left, right) in enumerate(pairs, 1):
150
+ steps.append(
151
+ AssemblyStep(
152
+ index=index,
153
+ left_fragment_id=left.id,
154
+ right_fragment_id=right.id,
155
+ operation="compatible_end_ligation",
156
+ junction_sequence=left.right_end.overhang_sequence_5to3,
157
+ )
158
+ )
159
+ product = ligation.product
160
+ else:
161
+ validate_positive_int(min_overlap, "min_overlap", maximum=100_000)
162
+ validate_positive_int(max_overlap, "max_overlap", maximum=100_000)
163
+ if min_overlap > max_overlap:
164
+ raise ConfigurationError("min_overlap cannot exceed max_overlap.")
165
+ overlap_fragments = tuple(
166
+ _overlap_fragment(item, index) for index, item in enumerate(materialized, 1)
167
+ )
168
+ expected = len(overlap_fragments) if circularize else len(overlap_fragments) - 1
169
+ overlap_values = (
170
+ None
171
+ if overlaps is None
172
+ else materialize_bounded(
173
+ overlaps,
174
+ max_items=expected + 1,
175
+ name="assembly overlaps",
176
+ )
177
+ )
178
+ if overlap_values is not None and len(overlap_values) != expected:
179
+ raise ConfigurationError(
180
+ "overlaps must provide exactly one sequence per assembly junction."
181
+ )
182
+ if any(
183
+ fragment.sequence.strandedness is not overlap_fragments[0].sequence.strandedness
184
+ for fragment in overlap_fragments[1:]
185
+ ):
186
+ raise ConfigurationError("All assembly fragments must use the same strandedness.")
187
+ product_symbols = overlap_fragments[0].sequence.symbols
188
+ if len(product_symbols) > max_product_length:
189
+ raise ConfigurationError("Assembly product exceeds max_product_length.")
190
+ for index, (overlap_left, overlap_right) in enumerate(pairwise(overlap_fragments), 1):
191
+ overlap = _resolve_overlap(
192
+ overlap_left.sequence.symbols,
193
+ overlap_right.sequence.symbols,
194
+ explicit=None if overlap_values is None else overlap_values[index - 1],
195
+ minimum=min_overlap,
196
+ maximum=max_overlap,
197
+ )
198
+ addition = overlap_right.sequence.symbols[len(overlap) :]
199
+ if len(product_symbols) + len(addition) > max_product_length:
200
+ raise ConfigurationError("Assembly product exceeds max_product_length.")
201
+ product_symbols += addition
202
+ steps.append(
203
+ AssemblyStep(
204
+ index=index,
205
+ left_fragment_id=overlap_left.id,
206
+ right_fragment_id=overlap_right.id,
207
+ operation="exact_overlap_merge",
208
+ junction_sequence=overlap,
209
+ )
210
+ )
211
+ if circularize:
212
+ closing = _resolve_overlap(
213
+ overlap_fragments[-1].sequence.symbols,
214
+ overlap_fragments[0].sequence.symbols,
215
+ explicit=None if overlap_values is None else overlap_values[-1],
216
+ minimum=min_overlap,
217
+ maximum=max_overlap,
218
+ )
219
+ if not product_symbols.endswith(closing):
220
+ raise ConfigurationError(
221
+ "Closing overlap was consumed or altered by previous junctions."
222
+ )
223
+ product_symbols = product_symbols[: -len(closing)]
224
+ steps.append(
225
+ AssemblyStep(
226
+ index=len(steps) + 1,
227
+ left_fragment_id=overlap_fragments[-1].id,
228
+ right_fragment_id=overlap_fragments[0].id,
229
+ operation="circular_exact_overlap_merge",
230
+ junction_sequence=closing,
231
+ )
232
+ )
233
+ if len(product_symbols) > max_product_length:
234
+ raise ConfigurationError("Assembly product exceeds max_product_length.")
235
+ alphabet = (
236
+ DNAAlphabet.IUPAC
237
+ if any(item.sequence.alphabet is DNAAlphabet.IUPAC for item in overlap_fragments)
238
+ else DNAAlphabet.STRICT
239
+ )
240
+ product = DNASequence(
241
+ product_symbols,
242
+ alphabet=alphabet,
243
+ topology=Topology.CIRCULAR if circularize else Topology.LINEAR,
244
+ strandedness=overlap_fragments[0].sequence.strandedness,
245
+ )
246
+ return AssemblyResult(
247
+ assembly_method=normalized_method,
248
+ product=product,
249
+ steps=tuple(steps),
250
+ circularized=circularize,
251
+ complete=True,
252
+ method="ordered_bounded_assembly_simulation",
253
+ algorithm_version="dnakit-assembly-v1",
254
+ parameters=freeze_parameters(
255
+ {
256
+ "assembly_method": normalized_method,
257
+ "fragment_count": len(materialized),
258
+ "min_overlap": min_overlap if normalized_method in ("gibson", "lcr") else None,
259
+ "max_overlap": max_overlap if normalized_method in ("gibson", "lcr") else None,
260
+ "explicit_overlaps": overlaps is not None,
261
+ "circularize": circularize,
262
+ "allow_blunt": allow_blunt,
263
+ "cycles": cycles,
264
+ "cycles_affect_sequence_result": False,
265
+ "kinetics_modeled": False,
266
+ "max_fragments": max_fragments,
267
+ "max_product_length": max_product_length,
268
+ }
269
+ ),
270
+ provenance=native_provenance(
271
+ reimplementation=True,
272
+ reference_name=f"{normalized_method} ordered assembly abstraction",
273
+ reference_version="boundary-v1",
274
+ ),
275
+ issues=issues,
276
+ )
277
+
278
+
279
+ __all__ = ["AssemblyFragment", "simulate_assembly"]