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,142 @@
|
|
|
1
|
+
"""Immutable secondary-structure and ensemble result objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from dnakit.core import BackendInfo, Provenance
|
|
9
|
+
from dnakit.core._json import to_json_compatible
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _SerializableResult:
|
|
13
|
+
def to_dict(self) -> dict[str, Any]:
|
|
14
|
+
return cast(dict[str, Any], to_json_compatible(self))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class SecondaryBasePair(_SerializableResult):
|
|
19
|
+
first_global_index: int
|
|
20
|
+
second_global_index: int
|
|
21
|
+
first_strand_index: int
|
|
22
|
+
first_local_index: int
|
|
23
|
+
second_strand_index: int
|
|
24
|
+
second_local_index: int
|
|
25
|
+
bracket_type: str
|
|
26
|
+
inter_strand: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SecondaryStem(_SerializableResult):
|
|
31
|
+
base_pairs: tuple[SecondaryBasePair, ...]
|
|
32
|
+
length: int
|
|
33
|
+
inter_strand: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SecondaryStructureSummary(_SerializableResult):
|
|
38
|
+
strands_5to3: tuple[str, ...]
|
|
39
|
+
dot_bracket: str
|
|
40
|
+
base_pairs: tuple[SecondaryBasePair, ...]
|
|
41
|
+
stems: tuple[SecondaryStem, ...]
|
|
42
|
+
structure_type: str
|
|
43
|
+
base_pair_count: int
|
|
44
|
+
paired_base_fraction: float
|
|
45
|
+
stem_lengths: tuple[int, ...]
|
|
46
|
+
hairpin_count: int
|
|
47
|
+
hairpin_loop_lengths: tuple[int, ...]
|
|
48
|
+
max_contiguous_pair_count: int
|
|
49
|
+
three_prime_window: int
|
|
50
|
+
three_prime_dimer: bool
|
|
51
|
+
three_prime_dimer_max_contiguous_pairs: int
|
|
52
|
+
method: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class AccessibilityWindow(_SerializableResult):
|
|
57
|
+
start: int
|
|
58
|
+
end: int
|
|
59
|
+
mean_unpaired_probability: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class PairProbabilityResult(_SerializableResult):
|
|
64
|
+
strands_5to3: tuple[str, ...]
|
|
65
|
+
pair_probabilities: tuple[tuple[float, ...], ...]
|
|
66
|
+
pairing_probabilities_by_base: tuple[float, ...]
|
|
67
|
+
unpaired_probabilities_by_base: tuple[float, ...]
|
|
68
|
+
accessibility_window_size: int
|
|
69
|
+
accessibility_windows: tuple[AccessibilityWindow, ...]
|
|
70
|
+
most_accessible_window_start: int | None
|
|
71
|
+
method: str
|
|
72
|
+
applicability: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class PredictedSecondaryStructure(_SerializableResult):
|
|
77
|
+
summary: SecondaryStructureSummary
|
|
78
|
+
free_energy_kcal_per_mol: float
|
|
79
|
+
stack_free_energy_kcal_per_mol: float | None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class NupackComplexResult(_SerializableResult):
|
|
84
|
+
strands_5to3: tuple[str, ...]
|
|
85
|
+
material: str
|
|
86
|
+
temperature_celsius: float
|
|
87
|
+
monovalent_molar: float
|
|
88
|
+
magnesium_molar: float
|
|
89
|
+
ensemble: str
|
|
90
|
+
partition_function_log: float
|
|
91
|
+
ensemble_free_energy_kcal_per_mol: float
|
|
92
|
+
mfe_structures: tuple[PredictedSecondaryStructure, ...]
|
|
93
|
+
pair_probabilities: PairProbabilityResult
|
|
94
|
+
suboptimal_structures: tuple[PredictedSecondaryStructure, ...]
|
|
95
|
+
boltzmann_samples: tuple[str, ...]
|
|
96
|
+
ensemble_size: int
|
|
97
|
+
target_structure: SecondaryStructureSummary | None
|
|
98
|
+
target_structure_probability: float | None
|
|
99
|
+
target_ensemble_defect: float | None
|
|
100
|
+
method: str
|
|
101
|
+
backend: BackendInfo
|
|
102
|
+
provenance: Provenance
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class ComplexConcentration(_SerializableResult):
|
|
107
|
+
name: str
|
|
108
|
+
strand_names: tuple[str, ...]
|
|
109
|
+
concentration_molar: float
|
|
110
|
+
is_target: bool
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class NupackTubeResult(_SerializableResult):
|
|
115
|
+
strand_names: tuple[str, ...]
|
|
116
|
+
sequences_5to3: tuple[str, ...]
|
|
117
|
+
input_concentrations_molar: tuple[float, ...]
|
|
118
|
+
complex_concentrations: tuple[ComplexConcentration, ...]
|
|
119
|
+
target_complex_name: str
|
|
120
|
+
target_strand_names: tuple[str, ...]
|
|
121
|
+
target_complex_concentration_molar: float
|
|
122
|
+
complex_fraction_denominator_molar: float
|
|
123
|
+
target_complex_fraction: float
|
|
124
|
+
non_target_complex_fraction: float
|
|
125
|
+
fraction_bases_unpaired: float
|
|
126
|
+
max_complex_size: int
|
|
127
|
+
method: str
|
|
128
|
+
backend: BackendInfo
|
|
129
|
+
provenance: Provenance
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = [
|
|
133
|
+
"AccessibilityWindow",
|
|
134
|
+
"ComplexConcentration",
|
|
135
|
+
"NupackComplexResult",
|
|
136
|
+
"NupackTubeResult",
|
|
137
|
+
"PairProbabilityResult",
|
|
138
|
+
"PredictedSecondaryStructure",
|
|
139
|
+
"SecondaryBasePair",
|
|
140
|
+
"SecondaryStem",
|
|
141
|
+
"SecondaryStructureSummary",
|
|
142
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Deterministic native sequence and fingerprint similarity tools."""
|
|
2
|
+
|
|
3
|
+
from dnakit.similarity.approximate import (
|
|
4
|
+
DEFAULT_MAX_APPROXIMATE_CELLS,
|
|
5
|
+
DEFAULT_MAX_APPROXIMATE_MATCHES,
|
|
6
|
+
DEFAULT_MAX_APPROXIMATE_TARGETS,
|
|
7
|
+
approximate_search,
|
|
8
|
+
)
|
|
9
|
+
from dnakit.similarity.compare import compare, exact_similarity
|
|
10
|
+
from dnakit.similarity.dashing import (
|
|
11
|
+
DEFAULT_MAX_DASHING_CAPTURE_BYTES,
|
|
12
|
+
DEFAULT_MAX_DASHING_INPUT_BYTES,
|
|
13
|
+
DEFAULT_MAX_DASHING_ITEMS,
|
|
14
|
+
DEFAULT_MAX_DASHING_OUTPUT_BYTES,
|
|
15
|
+
DEFAULT_MAX_DASHING_SKETCH_MEMORY_BYTES,
|
|
16
|
+
DashingAdapter,
|
|
17
|
+
DashingInput,
|
|
18
|
+
DashingMode,
|
|
19
|
+
)
|
|
20
|
+
from dnakit.similarity.distance import (
|
|
21
|
+
DEFAULT_MAX_EDIT_CELLS,
|
|
22
|
+
edit_distance,
|
|
23
|
+
hamming_distance,
|
|
24
|
+
levenshtein_distance,
|
|
25
|
+
)
|
|
26
|
+
from dnakit.similarity.index import (
|
|
27
|
+
SketchIndex,
|
|
28
|
+
build_sketch_index,
|
|
29
|
+
load_sketch_index,
|
|
30
|
+
nearest_neighbors,
|
|
31
|
+
save_sketch_index,
|
|
32
|
+
)
|
|
33
|
+
from dnakit.similarity.matrix import (
|
|
34
|
+
DEFAULT_MAX_MATRIX_ITEMS,
|
|
35
|
+
pairwise_matrix,
|
|
36
|
+
similarity_matrix,
|
|
37
|
+
)
|
|
38
|
+
from dnakit.similarity.results import (
|
|
39
|
+
ApproximateMatch,
|
|
40
|
+
ApproximateSearchResult,
|
|
41
|
+
DashingJaccardMatrixResult,
|
|
42
|
+
DashingNeighborHit,
|
|
43
|
+
DashingNeighborRow,
|
|
44
|
+
DashingTopKResult,
|
|
45
|
+
DistanceResult,
|
|
46
|
+
EditStep,
|
|
47
|
+
Mismatch,
|
|
48
|
+
NearestNeighborHit,
|
|
49
|
+
NearestNeighborResult,
|
|
50
|
+
SearchResult,
|
|
51
|
+
SequenceMatch,
|
|
52
|
+
SimilarityMatrixResult,
|
|
53
|
+
SimilarityResult,
|
|
54
|
+
SketchSimilarityResult,
|
|
55
|
+
)
|
|
56
|
+
from dnakit.similarity.search import (
|
|
57
|
+
DEFAULT_MAX_SEARCH_MATCHES,
|
|
58
|
+
DEFAULT_MAX_SEARCH_TARGETS,
|
|
59
|
+
exact_search,
|
|
60
|
+
find_subsequence,
|
|
61
|
+
reverse_complement_search,
|
|
62
|
+
subsequence_search,
|
|
63
|
+
)
|
|
64
|
+
from dnakit.similarity.sketch import SketchMetric, sketch_similarity
|
|
65
|
+
from dnakit.similarity.vector import (
|
|
66
|
+
fingerprint_similarity,
|
|
67
|
+
kmer_similarity,
|
|
68
|
+
kmer_vector_similarity,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"DEFAULT_MAX_APPROXIMATE_CELLS",
|
|
73
|
+
"DEFAULT_MAX_APPROXIMATE_MATCHES",
|
|
74
|
+
"DEFAULT_MAX_APPROXIMATE_TARGETS",
|
|
75
|
+
"DEFAULT_MAX_DASHING_CAPTURE_BYTES",
|
|
76
|
+
"DEFAULT_MAX_DASHING_INPUT_BYTES",
|
|
77
|
+
"DEFAULT_MAX_DASHING_ITEMS",
|
|
78
|
+
"DEFAULT_MAX_DASHING_OUTPUT_BYTES",
|
|
79
|
+
"DEFAULT_MAX_DASHING_SKETCH_MEMORY_BYTES",
|
|
80
|
+
"DEFAULT_MAX_EDIT_CELLS",
|
|
81
|
+
"DEFAULT_MAX_MATRIX_ITEMS",
|
|
82
|
+
"DEFAULT_MAX_SEARCH_MATCHES",
|
|
83
|
+
"DEFAULT_MAX_SEARCH_TARGETS",
|
|
84
|
+
"ApproximateMatch",
|
|
85
|
+
"ApproximateSearchResult",
|
|
86
|
+
"DashingAdapter",
|
|
87
|
+
"DashingInput",
|
|
88
|
+
"DashingJaccardMatrixResult",
|
|
89
|
+
"DashingMode",
|
|
90
|
+
"DashingNeighborHit",
|
|
91
|
+
"DashingNeighborRow",
|
|
92
|
+
"DashingTopKResult",
|
|
93
|
+
"DistanceResult",
|
|
94
|
+
"EditStep",
|
|
95
|
+
"Mismatch",
|
|
96
|
+
"NearestNeighborHit",
|
|
97
|
+
"NearestNeighborResult",
|
|
98
|
+
"SearchResult",
|
|
99
|
+
"SequenceMatch",
|
|
100
|
+
"SimilarityMatrixResult",
|
|
101
|
+
"SimilarityResult",
|
|
102
|
+
"SketchIndex",
|
|
103
|
+
"SketchMetric",
|
|
104
|
+
"SketchSimilarityResult",
|
|
105
|
+
"approximate_search",
|
|
106
|
+
"build_sketch_index",
|
|
107
|
+
"compare",
|
|
108
|
+
"edit_distance",
|
|
109
|
+
"exact_search",
|
|
110
|
+
"exact_similarity",
|
|
111
|
+
"find_subsequence",
|
|
112
|
+
"fingerprint_similarity",
|
|
113
|
+
"hamming_distance",
|
|
114
|
+
"kmer_similarity",
|
|
115
|
+
"kmer_vector_similarity",
|
|
116
|
+
"levenshtein_distance",
|
|
117
|
+
"load_sketch_index",
|
|
118
|
+
"nearest_neighbors",
|
|
119
|
+
"pairwise_matrix",
|
|
120
|
+
"reverse_complement_search",
|
|
121
|
+
"save_sketch_index",
|
|
122
|
+
"similarity_matrix",
|
|
123
|
+
"sketch_similarity",
|
|
124
|
+
"subsequence_search",
|
|
125
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Shared input and policy helpers for native similarity functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from dnakit.core import DNA, DNARecord, DNASequence, DNASet, Gap
|
|
10
|
+
from dnakit.core.facade import resolve_single_dna
|
|
11
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
12
|
+
|
|
13
|
+
SequenceInput: TypeAlias = DNA | DNASequence | DNARecord
|
|
14
|
+
NumericVector: TypeAlias = Sequence[int | float]
|
|
15
|
+
NamedVector: TypeAlias = Mapping[str, int | float]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sequence_and_id(value: SequenceInput, *, role: str) -> tuple[DNASequence, str | None]:
|
|
19
|
+
"""Resolve a sequence input and reject untyped values consistently."""
|
|
20
|
+
|
|
21
|
+
if isinstance(value, (DNA, DNARecord, DNASequence)):
|
|
22
|
+
sequence, record = resolve_single_dna(value)
|
|
23
|
+
sequence_id = None if record is None else record.id
|
|
24
|
+
else:
|
|
25
|
+
raise ConfigurationError(
|
|
26
|
+
f"{role} must be DNASequence or DNARecord.",
|
|
27
|
+
code="INVALID_SIMILARITY_SEQUENCE",
|
|
28
|
+
context={"role": role, "input_type": type(value).__name__},
|
|
29
|
+
)
|
|
30
|
+
reject_gaps(sequence, role=role)
|
|
31
|
+
return sequence, sequence_id
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def reject_gaps(sequence: DNASequence, *, role: str) -> None:
|
|
35
|
+
"""Reject every explicit Gap instead of silently omitting it."""
|
|
36
|
+
|
|
37
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
38
|
+
if gaps:
|
|
39
|
+
raise UnsupportedGapOperationError(
|
|
40
|
+
"Native MVP similarity does not accept explicit Gap objects.",
|
|
41
|
+
code="SIMILARITY_GAP_NOT_ALLOWED",
|
|
42
|
+
context={
|
|
43
|
+
"role": role,
|
|
44
|
+
"gap_count": len(gaps),
|
|
45
|
+
"unknown_gap_count": sum(gap.length is None for gap in gaps),
|
|
46
|
+
},
|
|
47
|
+
hint="Compare gap-free fragments explicitly; gaps are never silently omitted.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def materialize_targets(
|
|
52
|
+
targets: object,
|
|
53
|
+
*,
|
|
54
|
+
max_targets: int,
|
|
55
|
+
) -> tuple[SequenceInput, ...]:
|
|
56
|
+
"""Materialize at most ``max_targets`` targets without exhausting iterators."""
|
|
57
|
+
|
|
58
|
+
resolved: tuple[SequenceInput, ...]
|
|
59
|
+
if isinstance(targets, DNA):
|
|
60
|
+
resolved = targets.records[: max_targets + 1]
|
|
61
|
+
elif isinstance(targets, (DNASequence, DNARecord)):
|
|
62
|
+
resolved = (targets,)
|
|
63
|
+
elif isinstance(targets, DNASet):
|
|
64
|
+
resolved = targets.records[: max_targets + 1]
|
|
65
|
+
elif isinstance(targets, (str, bytes)):
|
|
66
|
+
raise ConfigurationError(
|
|
67
|
+
"Search targets must be DNASequence/DNARecord objects, not raw text.",
|
|
68
|
+
code="INVALID_SEARCH_TARGETS",
|
|
69
|
+
)
|
|
70
|
+
elif not isinstance(targets, Iterable):
|
|
71
|
+
raise ConfigurationError(
|
|
72
|
+
"Search targets must be one sequence, DNASet, or iterable of sequences.",
|
|
73
|
+
code="INVALID_SEARCH_TARGETS",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
collected: list[SequenceInput] = []
|
|
77
|
+
for index, item in enumerate(targets):
|
|
78
|
+
if not isinstance(item, (DNA, DNASequence, DNARecord)):
|
|
79
|
+
raise ConfigurationError(
|
|
80
|
+
"Every search target must be DNASequence or DNARecord.",
|
|
81
|
+
code="INVALID_SEARCH_TARGET",
|
|
82
|
+
context={"target_index": index, "type": type(item).__name__},
|
|
83
|
+
)
|
|
84
|
+
if index >= max_targets:
|
|
85
|
+
raise ConfigurationError(
|
|
86
|
+
"Search target input exceeds max_targets.",
|
|
87
|
+
code="SEARCH_TARGET_LIMIT_EXCEEDED",
|
|
88
|
+
context={
|
|
89
|
+
"target_count": max_targets + 1,
|
|
90
|
+
"target_count_is_lower_bound": True,
|
|
91
|
+
"max_targets": max_targets,
|
|
92
|
+
},
|
|
93
|
+
hint="Reduce the target input or increase max_targets explicitly.",
|
|
94
|
+
)
|
|
95
|
+
if isinstance(item, DNA):
|
|
96
|
+
if not item.is_single:
|
|
97
|
+
raise ConfigurationError(
|
|
98
|
+
"A nested DNA search target must contain exactly one record.",
|
|
99
|
+
code="INVALID_SEARCH_TARGET",
|
|
100
|
+
context={"target_index": index, "record_count": len(item)},
|
|
101
|
+
)
|
|
102
|
+
collected.append(item.record)
|
|
103
|
+
else:
|
|
104
|
+
collected.append(item)
|
|
105
|
+
resolved = tuple(collected)
|
|
106
|
+
if len(resolved) > max_targets:
|
|
107
|
+
raise ConfigurationError(
|
|
108
|
+
"Search target input exceeds max_targets.",
|
|
109
|
+
code="SEARCH_TARGET_LIMIT_EXCEEDED",
|
|
110
|
+
context={
|
|
111
|
+
"target_count": max_targets + 1,
|
|
112
|
+
"target_count_is_lower_bound": True,
|
|
113
|
+
"max_targets": max_targets,
|
|
114
|
+
},
|
|
115
|
+
hint="Reduce the target input or increase max_targets explicitly.",
|
|
116
|
+
)
|
|
117
|
+
if any(not isinstance(item, (DNASequence, DNARecord)) for item in resolved):
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
"Every search target must be DNASequence or DNARecord.",
|
|
120
|
+
code="INVALID_SEARCH_TARGET",
|
|
121
|
+
)
|
|
122
|
+
return resolved
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_bool(value: bool, name: str) -> None:
|
|
126
|
+
if not isinstance(value, bool):
|
|
127
|
+
raise ConfigurationError(f"{name} must be boolean.", context={name: value})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_positive_int(value: int, name: str) -> None:
|
|
131
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
132
|
+
raise ConfigurationError(
|
|
133
|
+
f"{name} must be a positive integer.",
|
|
134
|
+
context={name: value},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_non_negative_number(value: int | float, name: str) -> float:
|
|
139
|
+
if (
|
|
140
|
+
isinstance(value, bool)
|
|
141
|
+
or not isinstance(value, (int, float))
|
|
142
|
+
or not math.isfinite(value)
|
|
143
|
+
or value < 0
|
|
144
|
+
):
|
|
145
|
+
raise ConfigurationError(
|
|
146
|
+
f"{name} must be a finite non-negative number.",
|
|
147
|
+
context={name: value},
|
|
148
|
+
)
|
|
149
|
+
return float(value)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = ["NamedVector", "NumericVector", "SequenceInput"]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Bounded approximate substring matching by dynamic programming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from dnakit.core import DNA, DNARecord, DNASequence, Strand, Topology
|
|
9
|
+
from dnakit.core._json import FrozenDict
|
|
10
|
+
from dnakit.core.facade import resolve_single_dna
|
|
11
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
12
|
+
|
|
13
|
+
from ._shared import SequenceInput, materialize_targets, validate_bool
|
|
14
|
+
from .results import ApproximateMatch, ApproximateSearchResult
|
|
15
|
+
|
|
16
|
+
DEFAULT_MAX_APPROXIMATE_CELLS = 5_000_000
|
|
17
|
+
DEFAULT_MAX_APPROXIMATE_MATCHES = 100_000
|
|
18
|
+
DEFAULT_MAX_APPROXIMATE_TARGETS = 100_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _finite_non_negative(value: object, name: str) -> float:
|
|
22
|
+
if (
|
|
23
|
+
isinstance(value, bool)
|
|
24
|
+
or not isinstance(value, (int, float))
|
|
25
|
+
or not math.isfinite(value)
|
|
26
|
+
or value < 0
|
|
27
|
+
):
|
|
28
|
+
raise ConfigurationError(
|
|
29
|
+
f"{name} must be a finite non-negative number.",
|
|
30
|
+
code="INVALID_APPROXIMATE_MATCH_COST",
|
|
31
|
+
context={"field": name, "value": value},
|
|
32
|
+
)
|
|
33
|
+
return float(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _positive_int(value: object, name: str) -> int:
|
|
37
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
38
|
+
raise ConfigurationError(
|
|
39
|
+
f"{name} must be a positive integer.",
|
|
40
|
+
code="INVALID_APPROXIMATE_MATCH_LIMIT",
|
|
41
|
+
)
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sequence_and_id_without_gap_policy(
|
|
46
|
+
value: SequenceInput, *, role: str
|
|
47
|
+
) -> tuple[DNASequence, str | None]:
|
|
48
|
+
"""Resolve approximate-search inputs before applying its domain-specific Gap error."""
|
|
49
|
+
|
|
50
|
+
if isinstance(value, (DNA, DNARecord, DNASequence)):
|
|
51
|
+
sequence, record = resolve_single_dna(value)
|
|
52
|
+
return sequence, None if record is None else record.id
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
f"{role} must be DNASequence or DNARecord.",
|
|
55
|
+
code="INVALID_SIMILARITY_SEQUENCE",
|
|
56
|
+
context={"role": role, "input_type": type(value).__name__},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _scan_one(
|
|
61
|
+
query: str,
|
|
62
|
+
target: str,
|
|
63
|
+
*,
|
|
64
|
+
max_distance: float,
|
|
65
|
+
substitution_cost: float,
|
|
66
|
+
insertion_cost: float,
|
|
67
|
+
deletion_cost: float,
|
|
68
|
+
target_index: int,
|
|
69
|
+
target_id: str | None,
|
|
70
|
+
strand: Strand,
|
|
71
|
+
remaining_matches: int,
|
|
72
|
+
) -> tuple[ApproximateMatch, ...]:
|
|
73
|
+
columns = len(target) + 1
|
|
74
|
+
previous = [0.0] * columns
|
|
75
|
+
starts_previous = list(range(columns))
|
|
76
|
+
for query_index, query_symbol in enumerate(query, start=1):
|
|
77
|
+
current = [query_index * deletion_cost] + [0.0] * len(target)
|
|
78
|
+
starts_current = [0] * columns
|
|
79
|
+
for target_index_1, target_symbol in enumerate(target, start=1):
|
|
80
|
+
candidates = (
|
|
81
|
+
(
|
|
82
|
+
previous[target_index_1 - 1]
|
|
83
|
+
+ (0.0 if query_symbol == target_symbol else substitution_cost),
|
|
84
|
+
starts_previous[target_index_1 - 1],
|
|
85
|
+
0,
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
previous[target_index_1] + deletion_cost,
|
|
89
|
+
starts_previous[target_index_1],
|
|
90
|
+
1,
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
current[target_index_1 - 1] + insertion_cost,
|
|
94
|
+
starts_current[target_index_1 - 1],
|
|
95
|
+
2,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
score, start, _ = min(candidates, key=lambda item: (item[0], item[2], item[1]))
|
|
99
|
+
current[target_index_1] = score
|
|
100
|
+
starts_current[target_index_1] = start
|
|
101
|
+
previous, current = current, previous
|
|
102
|
+
starts_previous, starts_current = starts_current, starts_previous
|
|
103
|
+
matches: list[ApproximateMatch] = []
|
|
104
|
+
seen: set[tuple[int, int, float]] = set()
|
|
105
|
+
for end, distance in enumerate(previous[1:], start=1):
|
|
106
|
+
start = starts_previous[end]
|
|
107
|
+
key = (start, end, distance)
|
|
108
|
+
if distance <= max_distance and key not in seen:
|
|
109
|
+
if len(matches) >= remaining_matches:
|
|
110
|
+
raise ConfigurationError(
|
|
111
|
+
"Approximate search exceeds max_matches.",
|
|
112
|
+
code="APPROXIMATE_MATCH_LIMIT_EXCEEDED",
|
|
113
|
+
context={"match_count_lower_bound": remaining_matches + 1},
|
|
114
|
+
)
|
|
115
|
+
seen.add(key)
|
|
116
|
+
matches.append(
|
|
117
|
+
ApproximateMatch(
|
|
118
|
+
target_index=target_index,
|
|
119
|
+
target_id=target_id,
|
|
120
|
+
start=start,
|
|
121
|
+
end=end,
|
|
122
|
+
strand=strand,
|
|
123
|
+
distance=distance,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return tuple(matches)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def approximate_search(
|
|
130
|
+
query: SequenceInput,
|
|
131
|
+
targets: SequenceInput | Iterable[SequenceInput],
|
|
132
|
+
*,
|
|
133
|
+
max_distance: int | float,
|
|
134
|
+
substitution_cost: int | float = 1.0,
|
|
135
|
+
insertion_cost: int | float = 1.0,
|
|
136
|
+
deletion_cost: int | float = 1.0,
|
|
137
|
+
reverse_complement: bool = False,
|
|
138
|
+
max_targets: int = DEFAULT_MAX_APPROXIMATE_TARGETS,
|
|
139
|
+
max_matches: int = DEFAULT_MAX_APPROXIMATE_MATCHES,
|
|
140
|
+
max_cells: int = DEFAULT_MAX_APPROXIMATE_CELLS,
|
|
141
|
+
) -> ApproximateSearchResult:
|
|
142
|
+
"""Find bounded-cost query alignments ending at each target coordinate."""
|
|
143
|
+
|
|
144
|
+
maximum = _finite_non_negative(max_distance, "max_distance")
|
|
145
|
+
substitution = _finite_non_negative(substitution_cost, "substitution_cost")
|
|
146
|
+
insertion = _finite_non_negative(insertion_cost, "insertion_cost")
|
|
147
|
+
deletion = _finite_non_negative(deletion_cost, "deletion_cost")
|
|
148
|
+
if substitution == insertion == deletion == 0.0:
|
|
149
|
+
raise ConfigurationError(
|
|
150
|
+
"At least one edit cost must be positive.", code="DEGENERATE_APPROXIMATE_COSTS"
|
|
151
|
+
)
|
|
152
|
+
validate_bool(reverse_complement, "reverse_complement")
|
|
153
|
+
for name, value in (
|
|
154
|
+
("max_targets", max_targets),
|
|
155
|
+
("max_matches", max_matches),
|
|
156
|
+
("max_cells", max_cells),
|
|
157
|
+
):
|
|
158
|
+
_positive_int(value, name)
|
|
159
|
+
query_sequence, query_id = _sequence_and_id_without_gap_policy(query, role="query")
|
|
160
|
+
if query_sequence.topology is Topology.CIRCULAR:
|
|
161
|
+
raise ConfigurationError(
|
|
162
|
+
"Approximate query matching requires a linear query.",
|
|
163
|
+
code="APPROXIMATE_CIRCULAR_UNSUPPORTED",
|
|
164
|
+
)
|
|
165
|
+
if query_sequence.is_gapped:
|
|
166
|
+
raise UnsupportedGapOperationError(
|
|
167
|
+
"Approximate matching cannot silently omit explicit query Gap objects.",
|
|
168
|
+
code="APPROXIMATE_GAP_NOT_ALLOWED",
|
|
169
|
+
context={"role": "query"},
|
|
170
|
+
)
|
|
171
|
+
if query_sequence.symbol_length == 0:
|
|
172
|
+
raise ConfigurationError(
|
|
173
|
+
"Approximate matching does not accept an empty query.",
|
|
174
|
+
code="EMPTY_APPROXIMATE_QUERY",
|
|
175
|
+
)
|
|
176
|
+
materialized = materialize_targets(targets, max_targets=max_targets)
|
|
177
|
+
resolved_targets: list[tuple[DNASequence, str | None]] = []
|
|
178
|
+
total_cells = 0
|
|
179
|
+
for target in materialized:
|
|
180
|
+
target_sequence, target_id = _sequence_and_id_without_gap_policy(target, role="target")
|
|
181
|
+
if target_sequence.topology is Topology.CIRCULAR:
|
|
182
|
+
raise ConfigurationError(
|
|
183
|
+
"Approximate matching currently requires linear targets.",
|
|
184
|
+
code="APPROXIMATE_CIRCULAR_UNSUPPORTED",
|
|
185
|
+
)
|
|
186
|
+
if target_sequence.is_gapped:
|
|
187
|
+
raise UnsupportedGapOperationError(
|
|
188
|
+
"Approximate matching cannot silently omit explicit target Gap objects.",
|
|
189
|
+
code="APPROXIMATE_GAP_NOT_ALLOWED",
|
|
190
|
+
context={"role": "target"},
|
|
191
|
+
)
|
|
192
|
+
cells = (query_sequence.symbol_length + 1) * (target_sequence.symbol_length + 1)
|
|
193
|
+
total_cells += cells * (2 if reverse_complement else 1)
|
|
194
|
+
if total_cells > max_cells:
|
|
195
|
+
raise ConfigurationError(
|
|
196
|
+
"Approximate search exceeds max_cells.",
|
|
197
|
+
code="APPROXIMATE_MATCH_CELL_LIMIT",
|
|
198
|
+
context={"required_cells_lower_bound": total_cells, "max_cells": max_cells},
|
|
199
|
+
)
|
|
200
|
+
resolved_targets.append((target_sequence, target_id))
|
|
201
|
+
strands = [(query_sequence.symbols, Strand.FORWARD)]
|
|
202
|
+
if reverse_complement:
|
|
203
|
+
reverse = query_sequence.reverse_complement().symbols
|
|
204
|
+
if reverse != query_sequence.symbols:
|
|
205
|
+
strands.append((reverse, Strand.REVERSE))
|
|
206
|
+
matches: list[ApproximateMatch] = []
|
|
207
|
+
for target_index, (target_sequence, target_id) in enumerate(resolved_targets):
|
|
208
|
+
for query_symbols, strand in strands:
|
|
209
|
+
for match in _scan_one(
|
|
210
|
+
query_symbols,
|
|
211
|
+
target_sequence.symbols,
|
|
212
|
+
max_distance=maximum,
|
|
213
|
+
substitution_cost=substitution,
|
|
214
|
+
insertion_cost=insertion,
|
|
215
|
+
deletion_cost=deletion,
|
|
216
|
+
target_index=target_index,
|
|
217
|
+
target_id=target_id,
|
|
218
|
+
strand=strand,
|
|
219
|
+
remaining_matches=max_matches - len(matches),
|
|
220
|
+
):
|
|
221
|
+
matches.append(match)
|
|
222
|
+
ordered = tuple(
|
|
223
|
+
sorted(
|
|
224
|
+
matches, key=lambda item: (item.target_index, item.start, item.end, item.strand.value)
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return ApproximateSearchResult(
|
|
228
|
+
name="approximate_search",
|
|
229
|
+
method="semi-global-weighted-edit-distance",
|
|
230
|
+
algorithm_version="dnakit-approximate-search-v1",
|
|
231
|
+
query_id=query_id,
|
|
232
|
+
query_length=query_sequence.symbol_length,
|
|
233
|
+
target_count=len(resolved_targets),
|
|
234
|
+
matches=ordered,
|
|
235
|
+
max_distance=maximum,
|
|
236
|
+
reverse_complement=reverse_complement,
|
|
237
|
+
max_targets=max_targets,
|
|
238
|
+
max_matches=max_matches,
|
|
239
|
+
max_cells=max_cells,
|
|
240
|
+
dp_cells=total_cells,
|
|
241
|
+
parameters=FrozenDict(
|
|
242
|
+
{
|
|
243
|
+
"substitution_cost": substitution,
|
|
244
|
+
"insertion_cost": insertion,
|
|
245
|
+
"deletion_cost": deletion,
|
|
246
|
+
"iupac_matching": "literal",
|
|
247
|
+
"coordinate_system": "0-based-half-open",
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"DEFAULT_MAX_APPROXIMATE_CELLS",
|
|
255
|
+
"DEFAULT_MAX_APPROXIMATE_MATCHES",
|
|
256
|
+
"DEFAULT_MAX_APPROXIMATE_TARGETS",
|
|
257
|
+
"approximate_search",
|
|
258
|
+
]
|