dnakit 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
dnakit/__init__.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Stable, lightweight DNAKit public package namespace."""
|
|
2
|
+
|
|
3
|
+
from dnakit import download, search
|
|
4
|
+
from dnakit._version import __version__
|
|
5
|
+
from dnakit.chunking import (
|
|
6
|
+
ChunkingConfig,
|
|
7
|
+
ChunkProgress,
|
|
8
|
+
ChunkProgressCallback,
|
|
9
|
+
ChunkStrategy,
|
|
10
|
+
LengthCurriculum,
|
|
11
|
+
LengthCurriculumStage,
|
|
12
|
+
SequenceChunk,
|
|
13
|
+
iter_fasta_chunks,
|
|
14
|
+
iter_sequence_chunks,
|
|
15
|
+
make_length_curriculum,
|
|
16
|
+
)
|
|
17
|
+
from dnakit.core import (
|
|
18
|
+
DNA,
|
|
19
|
+
DNAAlphabet,
|
|
20
|
+
DNAFeature,
|
|
21
|
+
DNARecord,
|
|
22
|
+
DNASequence,
|
|
23
|
+
DNASet,
|
|
24
|
+
Gap,
|
|
25
|
+
GapKind,
|
|
26
|
+
Interval,
|
|
27
|
+
MetricResult,
|
|
28
|
+
Topology,
|
|
29
|
+
)
|
|
30
|
+
from dnakit.io import (
|
|
31
|
+
ReadConfig,
|
|
32
|
+
ReadMode,
|
|
33
|
+
RecordSource,
|
|
34
|
+
WriteConfig,
|
|
35
|
+
WriteResult,
|
|
36
|
+
read,
|
|
37
|
+
read_one,
|
|
38
|
+
read_set,
|
|
39
|
+
write,
|
|
40
|
+
)
|
|
41
|
+
from dnakit.references import (
|
|
42
|
+
DownloadProgress,
|
|
43
|
+
GenomeAssembly,
|
|
44
|
+
GenomeDownloadResult,
|
|
45
|
+
download_genome,
|
|
46
|
+
resolve_genome_assembly,
|
|
47
|
+
)
|
|
48
|
+
from dnakit.standardize import (
|
|
49
|
+
DatasetValidationConfig,
|
|
50
|
+
DatasetValidationReport,
|
|
51
|
+
NormalizationConfig,
|
|
52
|
+
NormalizationResult,
|
|
53
|
+
ValidationConfig,
|
|
54
|
+
ValidationReport,
|
|
55
|
+
normalize,
|
|
56
|
+
validate,
|
|
57
|
+
validate_set,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"DNA",
|
|
62
|
+
"ChunkProgress",
|
|
63
|
+
"ChunkProgressCallback",
|
|
64
|
+
"ChunkStrategy",
|
|
65
|
+
"ChunkingConfig",
|
|
66
|
+
"DNAAlphabet",
|
|
67
|
+
"DNAFeature",
|
|
68
|
+
"DNARecord",
|
|
69
|
+
"DNASequence",
|
|
70
|
+
"DNASet",
|
|
71
|
+
"DatasetValidationConfig",
|
|
72
|
+
"DatasetValidationReport",
|
|
73
|
+
"DownloadProgress",
|
|
74
|
+
"Gap",
|
|
75
|
+
"GapKind",
|
|
76
|
+
"GenomeAssembly",
|
|
77
|
+
"GenomeDownloadResult",
|
|
78
|
+
"Interval",
|
|
79
|
+
"LengthCurriculum",
|
|
80
|
+
"LengthCurriculumStage",
|
|
81
|
+
"MetricResult",
|
|
82
|
+
"NormalizationConfig",
|
|
83
|
+
"NormalizationResult",
|
|
84
|
+
"ReadConfig",
|
|
85
|
+
"ReadMode",
|
|
86
|
+
"RecordSource",
|
|
87
|
+
"SequenceChunk",
|
|
88
|
+
"Topology",
|
|
89
|
+
"ValidationConfig",
|
|
90
|
+
"ValidationReport",
|
|
91
|
+
"WriteConfig",
|
|
92
|
+
"WriteResult",
|
|
93
|
+
"__version__",
|
|
94
|
+
"download",
|
|
95
|
+
"download_genome",
|
|
96
|
+
"iter_fasta_chunks",
|
|
97
|
+
"iter_sequence_chunks",
|
|
98
|
+
"make_length_curriculum",
|
|
99
|
+
"normalize",
|
|
100
|
+
"read",
|
|
101
|
+
"read_one",
|
|
102
|
+
"read_set",
|
|
103
|
+
"resolve_genome_assembly",
|
|
104
|
+
"search",
|
|
105
|
+
"validate",
|
|
106
|
+
"validate_set",
|
|
107
|
+
"write",
|
|
108
|
+
]
|
dnakit/_version.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Deterministic bounded native pairwise DNA alignment."""
|
|
2
|
+
|
|
3
|
+
from dnakit.alignment.pairwise import AlignmentConfig, align_pairwise
|
|
4
|
+
from dnakit.alignment.results import AlignmentColumn, AlignmentResult
|
|
5
|
+
|
|
6
|
+
__all__ = ["AlignmentColumn", "AlignmentConfig", "AlignmentResult", "align_pairwise"]
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Needleman-Wunsch and Smith-Waterman alignment with bounded memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from itertools import chain
|
|
8
|
+
from typing import Literal, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from dnakit.alignment.results import AlignmentColumn, AlignmentResult
|
|
11
|
+
from dnakit.core import (
|
|
12
|
+
Citation,
|
|
13
|
+
DNARecord,
|
|
14
|
+
DNASequence,
|
|
15
|
+
ExecutionMode,
|
|
16
|
+
ImplementationInfo,
|
|
17
|
+
ImplementationLabel,
|
|
18
|
+
OriginClass,
|
|
19
|
+
Provenance,
|
|
20
|
+
Topology,
|
|
21
|
+
)
|
|
22
|
+
from dnakit.core._json import FrozenDict
|
|
23
|
+
from dnakit.core.facade import DNA, resolve_single_dna
|
|
24
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
25
|
+
|
|
26
|
+
SequenceInput: TypeAlias = DNA | DNASequence | DNARecord
|
|
27
|
+
AlignmentMode: TypeAlias = Literal["global", "local", "semi_global"]
|
|
28
|
+
|
|
29
|
+
_STOP = 0
|
|
30
|
+
_DIAGONAL = 1
|
|
31
|
+
_DELETE = 2
|
|
32
|
+
_INSERT = 3
|
|
33
|
+
_FROM_MATCH = 1
|
|
34
|
+
_FROM_DELETE = 2
|
|
35
|
+
_FROM_INSERT = 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class AlignmentConfig:
|
|
40
|
+
"""Scoring and resource limits for native linear-gap alignment."""
|
|
41
|
+
|
|
42
|
+
mode: AlignmentMode = "global"
|
|
43
|
+
match_score: float = 1.0
|
|
44
|
+
mismatch_score: float = -1.0
|
|
45
|
+
gap_score: float = -1.0
|
|
46
|
+
gap_open_score: float | None = None
|
|
47
|
+
gap_extend_score: float | None = None
|
|
48
|
+
max_cells: int = 5_000_000
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if self.mode not in ("global", "local", "semi_global"):
|
|
52
|
+
raise ConfigurationError(
|
|
53
|
+
"mode must be global, local, or semi_global.", code="INVALID_ALIGNMENT_MODE"
|
|
54
|
+
)
|
|
55
|
+
for name in ("match_score", "mismatch_score", "gap_score"):
|
|
56
|
+
value = getattr(self, name)
|
|
57
|
+
if (
|
|
58
|
+
isinstance(value, bool)
|
|
59
|
+
or not isinstance(value, (int, float))
|
|
60
|
+
or not math.isfinite(value)
|
|
61
|
+
):
|
|
62
|
+
raise ConfigurationError(
|
|
63
|
+
f"{name} must be a finite number.", code="INVALID_ALIGNMENT_SCORE"
|
|
64
|
+
)
|
|
65
|
+
object.__setattr__(self, name, float(value))
|
|
66
|
+
for name in ("gap_open_score", "gap_extend_score"):
|
|
67
|
+
value = getattr(self, name)
|
|
68
|
+
if value is not None and (
|
|
69
|
+
isinstance(value, bool)
|
|
70
|
+
or not isinstance(value, (int, float))
|
|
71
|
+
or not math.isfinite(value)
|
|
72
|
+
):
|
|
73
|
+
raise ConfigurationError(
|
|
74
|
+
f"{name} must be a finite number or None.", code="INVALID_ALIGNMENT_SCORE"
|
|
75
|
+
)
|
|
76
|
+
if value is not None:
|
|
77
|
+
object.__setattr__(self, name, float(value))
|
|
78
|
+
if (self.gap_open_score is None) != (self.gap_extend_score is None):
|
|
79
|
+
raise ConfigurationError(
|
|
80
|
+
"gap_open_score and gap_extend_score must be provided together.",
|
|
81
|
+
code="INVALID_ALIGNMENT_SCORE",
|
|
82
|
+
)
|
|
83
|
+
if (
|
|
84
|
+
isinstance(self.max_cells, bool)
|
|
85
|
+
or not isinstance(self.max_cells, int)
|
|
86
|
+
or self.max_cells <= 0
|
|
87
|
+
):
|
|
88
|
+
raise ConfigurationError(
|
|
89
|
+
"max_cells must be a positive integer.", code="INVALID_ALIGNMENT_CELL_LIMIT"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _sequence_and_id(value: SequenceInput, role: str) -> tuple[DNASequence, str | None]:
|
|
94
|
+
if isinstance(value, (DNA, DNARecord, DNASequence)):
|
|
95
|
+
sequence, record = resolve_single_dna(value)
|
|
96
|
+
sequence_id = None if record is None else record.id
|
|
97
|
+
else:
|
|
98
|
+
raise ConfigurationError(
|
|
99
|
+
f"{role} must be DNASequence or DNARecord.",
|
|
100
|
+
code="INVALID_ALIGNMENT_INPUT",
|
|
101
|
+
context={"role": role, "type": type(value).__name__},
|
|
102
|
+
)
|
|
103
|
+
if sequence.is_gapped:
|
|
104
|
+
raise UnsupportedGapOperationError(
|
|
105
|
+
"Pairwise alignment requires gap-free input; '-' is reserved for its output.",
|
|
106
|
+
code="ALIGNMENT_INPUT_GAP_NOT_SUPPORTED",
|
|
107
|
+
context={"role": role},
|
|
108
|
+
)
|
|
109
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
110
|
+
raise ConfigurationError(
|
|
111
|
+
"Pairwise alignment requires an explicitly linearized sequence.",
|
|
112
|
+
code="ALIGNMENT_CIRCULAR_UNSUPPORTED",
|
|
113
|
+
context={"role": role},
|
|
114
|
+
hint="Rotate the sequence to the desired origin and convert it to linear topology.",
|
|
115
|
+
)
|
|
116
|
+
return sequence, sequence_id
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _choose(candidates: tuple[tuple[float, int], ...]) -> tuple[float, int]:
|
|
120
|
+
"""Choose the largest score and retain deterministic candidate order on ties."""
|
|
121
|
+
|
|
122
|
+
best_score, best_pointer = candidates[0]
|
|
123
|
+
for score, pointer in candidates[1:]:
|
|
124
|
+
if score > best_score:
|
|
125
|
+
best_score, best_pointer = score, pointer
|
|
126
|
+
return best_score, best_pointer
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _alignment_provenance(config: AlignmentConfig) -> Provenance:
|
|
130
|
+
primary = (
|
|
131
|
+
Citation(
|
|
132
|
+
"smith-waterman-1981",
|
|
133
|
+
title="Identification of common molecular subsequences",
|
|
134
|
+
doi="10.1016/0022-2836(81)90087-5",
|
|
135
|
+
)
|
|
136
|
+
if config.mode == "local"
|
|
137
|
+
else Citation(
|
|
138
|
+
"needleman-wunsch-1970",
|
|
139
|
+
title="A general method applicable to the search for similarities",
|
|
140
|
+
doi="10.1016/0022-2836(70)90057-4",
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
citations: tuple[Citation, ...] = (primary,)
|
|
144
|
+
if config.gap_open_score is not None:
|
|
145
|
+
citations += (
|
|
146
|
+
Citation(
|
|
147
|
+
"gotoh-1982",
|
|
148
|
+
title="An improved algorithm for matching biological sequences",
|
|
149
|
+
doi="10.1016/0022-2836(82)90398-9",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
return Provenance(
|
|
153
|
+
implementation=ImplementationInfo(
|
|
154
|
+
label=ImplementationLabel.REIMPLEMENTATION,
|
|
155
|
+
execution_mode=ExecutionMode.INTERNAL,
|
|
156
|
+
origin_class=OriginClass.PUBLISHED_ALGORITHM,
|
|
157
|
+
citations=citations,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _fill(
|
|
163
|
+
query: str,
|
|
164
|
+
target: str,
|
|
165
|
+
config: AlignmentConfig,
|
|
166
|
+
) -> tuple[list[list[float]], list[bytearray], tuple[int, int]]:
|
|
167
|
+
rows = len(query) + 1
|
|
168
|
+
columns = len(target) + 1
|
|
169
|
+
cells = rows * columns
|
|
170
|
+
if cells > config.max_cells:
|
|
171
|
+
raise ConfigurationError(
|
|
172
|
+
"Alignment dynamic-programming matrix exceeds max_cells.",
|
|
173
|
+
code="ALIGNMENT_CELL_LIMIT",
|
|
174
|
+
context={"dp_cells": cells, "max_cells": config.max_cells},
|
|
175
|
+
)
|
|
176
|
+
scores = [[0.0] * columns for _ in range(rows)]
|
|
177
|
+
pointers = [bytearray(columns) for _ in range(rows)]
|
|
178
|
+
if config.mode == "global":
|
|
179
|
+
for row in range(1, rows):
|
|
180
|
+
scores[row][0] = row * config.gap_score
|
|
181
|
+
pointers[row][0] = _DELETE
|
|
182
|
+
for column in range(1, columns):
|
|
183
|
+
scores[0][column] = column * config.gap_score
|
|
184
|
+
pointers[0][column] = _INSERT
|
|
185
|
+
best = (0, 0)
|
|
186
|
+
best_score = 0.0
|
|
187
|
+
for row in range(1, rows):
|
|
188
|
+
for column in range(1, columns):
|
|
189
|
+
diagonal = scores[row - 1][column - 1] + (
|
|
190
|
+
config.match_score
|
|
191
|
+
if query[row - 1] == target[column - 1]
|
|
192
|
+
else config.mismatch_score
|
|
193
|
+
)
|
|
194
|
+
deleted = scores[row - 1][column] + config.gap_score
|
|
195
|
+
inserted = scores[row][column - 1] + config.gap_score
|
|
196
|
+
candidates = (
|
|
197
|
+
((0.0, _STOP), (diagonal, _DIAGONAL), (deleted, _DELETE), (inserted, _INSERT))
|
|
198
|
+
if config.mode == "local"
|
|
199
|
+
else ((diagonal, _DIAGONAL), (deleted, _DELETE), (inserted, _INSERT))
|
|
200
|
+
)
|
|
201
|
+
score, pointer = _choose(candidates)
|
|
202
|
+
scores[row][column] = score
|
|
203
|
+
pointers[row][column] = pointer
|
|
204
|
+
if config.mode == "local" and score > best_score:
|
|
205
|
+
best_score = score
|
|
206
|
+
best = (row, column)
|
|
207
|
+
if config.mode == "semi_global":
|
|
208
|
+
_, end_row, end_column = max(
|
|
209
|
+
chain(
|
|
210
|
+
((scores[len(query)][column], len(query), column) for column in range(columns)),
|
|
211
|
+
((scores[row][len(target)], row, len(target)) for row in range(rows - 1)),
|
|
212
|
+
),
|
|
213
|
+
key=lambda item: item[0],
|
|
214
|
+
)
|
|
215
|
+
return scores, pointers, (end_row, end_column)
|
|
216
|
+
return scores, pointers, (best if config.mode == "local" else (len(query), len(target)))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _fill_affine(
|
|
220
|
+
query: str,
|
|
221
|
+
target: str,
|
|
222
|
+
config: AlignmentConfig,
|
|
223
|
+
) -> tuple[
|
|
224
|
+
list[list[float]],
|
|
225
|
+
tuple[list[bytearray], list[bytearray], list[bytearray]],
|
|
226
|
+
tuple[int, int, int],
|
|
227
|
+
]:
|
|
228
|
+
rows = len(query) + 1
|
|
229
|
+
columns = len(target) + 1
|
|
230
|
+
cells = rows * columns * 3
|
|
231
|
+
if cells > config.max_cells:
|
|
232
|
+
raise ConfigurationError(
|
|
233
|
+
"Alignment affine dynamic-programming matrices exceed max_cells.",
|
|
234
|
+
code="ALIGNMENT_CELL_LIMIT",
|
|
235
|
+
context={"dp_cells": cells, "max_cells": config.max_cells},
|
|
236
|
+
)
|
|
237
|
+
gap_open = cast(float, config.gap_open_score)
|
|
238
|
+
gap_extend = cast(float, config.gap_extend_score)
|
|
239
|
+
negative = float("-inf")
|
|
240
|
+
match = [[negative] * columns for _ in range(rows)]
|
|
241
|
+
deleted = [[negative] * columns for _ in range(rows)]
|
|
242
|
+
inserted = [[negative] * columns for _ in range(rows)]
|
|
243
|
+
scores = [[negative] * columns for _ in range(rows)]
|
|
244
|
+
match_pointers = [bytearray(columns) for _ in range(rows)]
|
|
245
|
+
delete_pointers = [bytearray(columns) for _ in range(rows)]
|
|
246
|
+
insert_pointers = [bytearray(columns) for _ in range(rows)]
|
|
247
|
+
match[0][0] = scores[0][0] = 0.0
|
|
248
|
+
if config.mode == "global":
|
|
249
|
+
for row in range(1, rows):
|
|
250
|
+
deleted[row][0] = scores[row][0] = gap_open + (row - 1) * gap_extend
|
|
251
|
+
delete_pointers[row][0] = _FROM_MATCH if row == 1 else _FROM_DELETE
|
|
252
|
+
for column in range(1, columns):
|
|
253
|
+
inserted[0][column] = scores[0][column] = gap_open + (column - 1) * gap_extend
|
|
254
|
+
insert_pointers[0][column] = _FROM_MATCH if column == 1 else _FROM_INSERT
|
|
255
|
+
elif config.mode in ("local", "semi_global"):
|
|
256
|
+
for row in range(rows):
|
|
257
|
+
match[row][0] = scores[row][0] = 0.0
|
|
258
|
+
for column in range(columns):
|
|
259
|
+
match[0][column] = scores[0][column] = 0.0
|
|
260
|
+
for row in range(1, rows):
|
|
261
|
+
for column in range(1, columns):
|
|
262
|
+
substitution = (
|
|
263
|
+
config.match_score
|
|
264
|
+
if query[row - 1] == target[column - 1]
|
|
265
|
+
else config.mismatch_score
|
|
266
|
+
)
|
|
267
|
+
match_candidates: tuple[tuple[float, int], ...] = (
|
|
268
|
+
(match[row - 1][column - 1] + substitution, _FROM_MATCH),
|
|
269
|
+
(deleted[row - 1][column - 1] + substitution, _FROM_DELETE),
|
|
270
|
+
(inserted[row - 1][column - 1] + substitution, _FROM_INSERT),
|
|
271
|
+
)
|
|
272
|
+
if config.mode == "local":
|
|
273
|
+
match_candidates = ((0.0, _STOP), *match_candidates)
|
|
274
|
+
match_previous = _choose(match_candidates)
|
|
275
|
+
match[row][column] = match_previous[0]
|
|
276
|
+
match_pointers[row][column] = match_previous[1]
|
|
277
|
+
deleted[row][column], delete_pointers[row][column] = _choose(
|
|
278
|
+
(
|
|
279
|
+
(match[row - 1][column] + gap_open, _FROM_MATCH),
|
|
280
|
+
(deleted[row - 1][column] + gap_extend, _FROM_DELETE),
|
|
281
|
+
(inserted[row - 1][column] + gap_open, _FROM_INSERT),
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
inserted[row][column], insert_pointers[row][column] = _choose(
|
|
285
|
+
(
|
|
286
|
+
(match[row][column - 1] + gap_open, _FROM_MATCH),
|
|
287
|
+
(deleted[row][column - 1] + gap_open, _FROM_DELETE),
|
|
288
|
+
(inserted[row][column - 1] + gap_extend, _FROM_INSERT),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
scores[row][column] = max(
|
|
292
|
+
match[row][column], deleted[row][column], inserted[row][column]
|
|
293
|
+
)
|
|
294
|
+
if config.mode == "local":
|
|
295
|
+
scores[row][column] = max(0.0, scores[row][column])
|
|
296
|
+
pointers = (match_pointers, delete_pointers, insert_pointers)
|
|
297
|
+
states = (
|
|
298
|
+
(_FROM_MATCH, match),
|
|
299
|
+
(_FROM_DELETE, deleted),
|
|
300
|
+
(_FROM_INSERT, inserted),
|
|
301
|
+
)
|
|
302
|
+
if config.mode == "local":
|
|
303
|
+
_, row, column, state = max(
|
|
304
|
+
(
|
|
305
|
+
(matrix[row][column], row, column, state)
|
|
306
|
+
for row in range(rows)
|
|
307
|
+
for column in range(columns)
|
|
308
|
+
for state, matrix in states
|
|
309
|
+
),
|
|
310
|
+
key=lambda item: item[0],
|
|
311
|
+
)
|
|
312
|
+
return scores, pointers, (row, column, state)
|
|
313
|
+
if config.mode == "semi_global":
|
|
314
|
+
_, row, column, state = max(
|
|
315
|
+
chain(
|
|
316
|
+
(
|
|
317
|
+
(matrix[-1][column], len(query), column, state)
|
|
318
|
+
for column in range(columns)
|
|
319
|
+
for state, matrix in states
|
|
320
|
+
),
|
|
321
|
+
(
|
|
322
|
+
(matrix[row][-1], row, len(target), state)
|
|
323
|
+
for row in range(rows - 1)
|
|
324
|
+
for state, matrix in states
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
key=lambda item: item[0],
|
|
328
|
+
)
|
|
329
|
+
return scores, pointers, (row, column, state)
|
|
330
|
+
state = _choose(tuple((matrix[-1][-1], state) for state, matrix in states))[1]
|
|
331
|
+
return scores, pointers, (len(query), len(target), state)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _trace_affine(
|
|
335
|
+
query: str,
|
|
336
|
+
target: str,
|
|
337
|
+
pointers: tuple[list[bytearray], list[bytearray], list[bytearray]],
|
|
338
|
+
endpoint: tuple[int, int, int],
|
|
339
|
+
mode: AlignmentMode,
|
|
340
|
+
) -> tuple[list[AlignmentColumn], list[str], list[str], int, int]:
|
|
341
|
+
row, column, state = endpoint
|
|
342
|
+
match_pointers, delete_pointers, insert_pointers = pointers
|
|
343
|
+
reverse_columns: list[AlignmentColumn] = []
|
|
344
|
+
reverse_query: list[str] = []
|
|
345
|
+
reverse_target: list[str] = []
|
|
346
|
+
while row > 0 or column > 0:
|
|
347
|
+
if mode == "semi_global" and (row == 0 or column == 0):
|
|
348
|
+
break
|
|
349
|
+
if state == _FROM_MATCH:
|
|
350
|
+
previous = match_pointers[row][column]
|
|
351
|
+
if previous == _STOP:
|
|
352
|
+
break
|
|
353
|
+
query_symbol, target_symbol = query[row - 1], target[column - 1]
|
|
354
|
+
operation: Literal["match", "mismatch", "insertion", "deletion"] = (
|
|
355
|
+
"match" if query_symbol == target_symbol else "mismatch"
|
|
356
|
+
)
|
|
357
|
+
reverse_columns.append(
|
|
358
|
+
AlignmentColumn(query_symbol, target_symbol, row - 1, column - 1, operation)
|
|
359
|
+
)
|
|
360
|
+
reverse_query.append(query_symbol)
|
|
361
|
+
reverse_target.append(target_symbol)
|
|
362
|
+
row -= 1
|
|
363
|
+
column -= 1
|
|
364
|
+
elif state == _FROM_DELETE:
|
|
365
|
+
previous = delete_pointers[row][column]
|
|
366
|
+
query_symbol = query[row - 1]
|
|
367
|
+
reverse_columns.append(AlignmentColumn(query_symbol, "-", row - 1, None, "deletion"))
|
|
368
|
+
reverse_query.append(query_symbol)
|
|
369
|
+
reverse_target.append("-")
|
|
370
|
+
row -= 1
|
|
371
|
+
elif state == _FROM_INSERT:
|
|
372
|
+
previous = insert_pointers[row][column]
|
|
373
|
+
target_symbol = target[column - 1]
|
|
374
|
+
reverse_columns.append(
|
|
375
|
+
AlignmentColumn("-", target_symbol, None, column - 1, "insertion")
|
|
376
|
+
)
|
|
377
|
+
reverse_query.append("-")
|
|
378
|
+
reverse_target.append(target_symbol)
|
|
379
|
+
column -= 1
|
|
380
|
+
else:
|
|
381
|
+
raise RuntimeError("Invalid affine-alignment traceback state.")
|
|
382
|
+
state = previous
|
|
383
|
+
return reverse_columns, reverse_query, reverse_target, row, column
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def align_pairwise(
|
|
387
|
+
query: SequenceInput,
|
|
388
|
+
target: SequenceInput,
|
|
389
|
+
*,
|
|
390
|
+
config: AlignmentConfig | None = None,
|
|
391
|
+
) -> AlignmentResult:
|
|
392
|
+
"""Align two sequences with literal-IUPAC equality and deterministic traceback."""
|
|
393
|
+
|
|
394
|
+
resolved = AlignmentConfig() if config is None else config
|
|
395
|
+
if not isinstance(resolved, AlignmentConfig):
|
|
396
|
+
raise ConfigurationError(
|
|
397
|
+
"config must be AlignmentConfig or None.", code="INVALID_ALIGNMENT_CONFIG"
|
|
398
|
+
)
|
|
399
|
+
query_sequence, query_id = _sequence_and_id(query, "query")
|
|
400
|
+
target_sequence, target_id = _sequence_and_id(target, "target")
|
|
401
|
+
query_text = query_sequence.symbols
|
|
402
|
+
target_text = target_sequence.symbols
|
|
403
|
+
if resolved.gap_open_score is None:
|
|
404
|
+
scores, linear_pointers, (row, column) = _fill(query_text, target_text, resolved)
|
|
405
|
+
query_end, target_end = row, column
|
|
406
|
+
reverse_columns: list[AlignmentColumn] = []
|
|
407
|
+
reverse_query: list[str] = []
|
|
408
|
+
reverse_target: list[str] = []
|
|
409
|
+
while row > 0 or column > 0:
|
|
410
|
+
if resolved.mode == "semi_global" and (row == 0 or column == 0):
|
|
411
|
+
break
|
|
412
|
+
pointer = linear_pointers[row][column]
|
|
413
|
+
if pointer == _STOP:
|
|
414
|
+
break
|
|
415
|
+
if pointer == _DIAGONAL:
|
|
416
|
+
query_symbol, target_symbol = query_text[row - 1], target_text[column - 1]
|
|
417
|
+
operation: Literal["match", "mismatch", "insertion", "deletion"] = (
|
|
418
|
+
"match" if query_symbol == target_symbol else "mismatch"
|
|
419
|
+
)
|
|
420
|
+
reverse_columns.append(
|
|
421
|
+
AlignmentColumn(query_symbol, target_symbol, row - 1, column - 1, operation)
|
|
422
|
+
)
|
|
423
|
+
reverse_query.append(query_symbol)
|
|
424
|
+
reverse_target.append(target_symbol)
|
|
425
|
+
row -= 1
|
|
426
|
+
column -= 1
|
|
427
|
+
elif pointer == _DELETE:
|
|
428
|
+
query_symbol = query_text[row - 1]
|
|
429
|
+
reverse_columns.append(
|
|
430
|
+
AlignmentColumn(query_symbol, "-", row - 1, None, "deletion")
|
|
431
|
+
)
|
|
432
|
+
reverse_query.append(query_symbol)
|
|
433
|
+
reverse_target.append("-")
|
|
434
|
+
row -= 1
|
|
435
|
+
elif pointer == _INSERT:
|
|
436
|
+
target_symbol = target_text[column - 1]
|
|
437
|
+
reverse_columns.append(
|
|
438
|
+
AlignmentColumn("-", target_symbol, None, column - 1, "insertion")
|
|
439
|
+
)
|
|
440
|
+
reverse_query.append("-")
|
|
441
|
+
reverse_target.append(target_symbol)
|
|
442
|
+
column -= 1
|
|
443
|
+
else:
|
|
444
|
+
raise RuntimeError("Invalid alignment traceback pointer.")
|
|
445
|
+
score = scores[query_end][target_end]
|
|
446
|
+
else:
|
|
447
|
+
scores, affine_pointers, endpoint = _fill_affine(query_text, target_text, resolved)
|
|
448
|
+
query_end, target_end, _ = endpoint
|
|
449
|
+
reverse_columns, reverse_query, reverse_target, row, column = _trace_affine(
|
|
450
|
+
query_text,
|
|
451
|
+
target_text,
|
|
452
|
+
affine_pointers,
|
|
453
|
+
endpoint,
|
|
454
|
+
resolved.mode,
|
|
455
|
+
)
|
|
456
|
+
score = scores[query_end][target_end]
|
|
457
|
+
columns = tuple(reversed(reverse_columns))
|
|
458
|
+
matches = sum(item.operation == "match" for item in columns)
|
|
459
|
+
mismatches = sum(item.operation == "mismatch" for item in columns)
|
|
460
|
+
insertions = sum(item.operation == "insertion" for item in columns)
|
|
461
|
+
deletions = sum(item.operation == "deletion" for item in columns)
|
|
462
|
+
comparable = matches + mismatches + insertions + deletions
|
|
463
|
+
query_consumed = matches + mismatches + deletions
|
|
464
|
+
target_consumed = matches + mismatches + insertions
|
|
465
|
+
return AlignmentResult(
|
|
466
|
+
name="pairwise_alignment",
|
|
467
|
+
method=resolved.mode,
|
|
468
|
+
algorithm_version=(
|
|
469
|
+
"affine-gap-dp-v1" if resolved.gap_open_score is not None else "linear-gap-dp-v1"
|
|
470
|
+
),
|
|
471
|
+
score=score,
|
|
472
|
+
aligned_query="".join(reversed(reverse_query)),
|
|
473
|
+
aligned_target="".join(reversed(reverse_target)),
|
|
474
|
+
query_id=query_id,
|
|
475
|
+
target_id=target_id,
|
|
476
|
+
query_start=row,
|
|
477
|
+
query_end=query_end,
|
|
478
|
+
target_start=column,
|
|
479
|
+
target_end=target_end,
|
|
480
|
+
matches=matches,
|
|
481
|
+
mismatches=mismatches,
|
|
482
|
+
insertions=insertions,
|
|
483
|
+
deletions=deletions,
|
|
484
|
+
identity=None if comparable == 0 else matches / comparable,
|
|
485
|
+
query_coverage=1.0 if not query_text else query_consumed / len(query_text),
|
|
486
|
+
target_coverage=1.0 if not target_text else target_consumed / len(target_text),
|
|
487
|
+
columns=columns,
|
|
488
|
+
parameters=FrozenDict(
|
|
489
|
+
{
|
|
490
|
+
"match_score": resolved.match_score,
|
|
491
|
+
"mismatch_score": resolved.mismatch_score,
|
|
492
|
+
"gap_score": resolved.gap_score,
|
|
493
|
+
"gap_open_score": resolved.gap_open_score,
|
|
494
|
+
"gap_extend_score": resolved.gap_extend_score,
|
|
495
|
+
"max_cells": resolved.max_cells,
|
|
496
|
+
"dp_cells": (len(query_text) + 1)
|
|
497
|
+
* (len(target_text) + 1)
|
|
498
|
+
* (3 if resolved.gap_open_score is not None else 1),
|
|
499
|
+
"iupac_matching": "literal",
|
|
500
|
+
"tie_break": "diagonal-delete-insert",
|
|
501
|
+
"end_gap_policy": {
|
|
502
|
+
"global": "penalized",
|
|
503
|
+
"local": "excluded-local-flanks",
|
|
504
|
+
"semi_global": "free-query-and-target-ends",
|
|
505
|
+
}[resolved.mode],
|
|
506
|
+
}
|
|
507
|
+
),
|
|
508
|
+
provenance=_alignment_provenance(resolved),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
__all__ = ["AlignmentConfig", "align_pairwise"]
|