dnakit 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
]
|
dnakit/molbio/_shared.py
ADDED
|
@@ -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"]
|