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,100 @@
|
|
|
1
|
+
"""Dataset deduplication, clustering, splitting, leakage, and quality evaluation."""
|
|
2
|
+
|
|
3
|
+
from dnakit.datasets.advanced_deduplicate import deduplicate_approximate, deduplicate_iupac
|
|
4
|
+
from dnakit.datasets.advanced_split import joint_split, temporal_split
|
|
5
|
+
from dnakit.datasets.clustering import (
|
|
6
|
+
cluster_sequences,
|
|
7
|
+
hierarchical_cluster,
|
|
8
|
+
select_representatives,
|
|
9
|
+
)
|
|
10
|
+
from dnakit.datasets.config import (
|
|
11
|
+
ClusterConfig,
|
|
12
|
+
DeduplicationConfig,
|
|
13
|
+
HierarchicalClusteringConfig,
|
|
14
|
+
IUPACDeduplicationConfig,
|
|
15
|
+
JointSplitConfig,
|
|
16
|
+
LeakageConfig,
|
|
17
|
+
NeuralClusteringConfig,
|
|
18
|
+
SplitConfig,
|
|
19
|
+
TemporalSplitConfig,
|
|
20
|
+
)
|
|
21
|
+
from dnakit.datasets.deduplicate import deduplicate
|
|
22
|
+
from dnakit.datasets.evaluation import detect_leakage, evaluate_split_quality
|
|
23
|
+
from dnakit.datasets.filter import (
|
|
24
|
+
MetadataExclusionField,
|
|
25
|
+
MetadataValues,
|
|
26
|
+
exclude_by_metadata,
|
|
27
|
+
exclude_chromosomes,
|
|
28
|
+
exclude_species,
|
|
29
|
+
)
|
|
30
|
+
from dnakit.datasets.neural_clustering import neural_cluster_sequences
|
|
31
|
+
from dnakit.datasets.results import (
|
|
32
|
+
Cluster,
|
|
33
|
+
ClusteringResult,
|
|
34
|
+
DedupGroup,
|
|
35
|
+
DeduplicationResult,
|
|
36
|
+
HierarchicalClusteringResult,
|
|
37
|
+
IUPACDeduplicationResult,
|
|
38
|
+
IUPACDuplicateGroup,
|
|
39
|
+
IUPACPairRelation,
|
|
40
|
+
JointSplitResult,
|
|
41
|
+
LeakageEvent,
|
|
42
|
+
LeakageReport,
|
|
43
|
+
LinkageStep,
|
|
44
|
+
NeuralClusteringResult,
|
|
45
|
+
RepresentativeSelectionResult,
|
|
46
|
+
SplitAssignment,
|
|
47
|
+
SplitQualityResult,
|
|
48
|
+
SplitResult,
|
|
49
|
+
SplitSubset,
|
|
50
|
+
TemporalSplitResult,
|
|
51
|
+
)
|
|
52
|
+
from dnakit.datasets.split import split
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"Cluster",
|
|
56
|
+
"ClusterConfig",
|
|
57
|
+
"ClusteringResult",
|
|
58
|
+
"DedupGroup",
|
|
59
|
+
"DeduplicationConfig",
|
|
60
|
+
"DeduplicationResult",
|
|
61
|
+
"HierarchicalClusteringConfig",
|
|
62
|
+
"HierarchicalClusteringResult",
|
|
63
|
+
"IUPACDeduplicationConfig",
|
|
64
|
+
"IUPACDeduplicationResult",
|
|
65
|
+
"IUPACDuplicateGroup",
|
|
66
|
+
"IUPACPairRelation",
|
|
67
|
+
"JointSplitConfig",
|
|
68
|
+
"JointSplitResult",
|
|
69
|
+
"LeakageConfig",
|
|
70
|
+
"LeakageEvent",
|
|
71
|
+
"LeakageReport",
|
|
72
|
+
"LinkageStep",
|
|
73
|
+
"MetadataExclusionField",
|
|
74
|
+
"MetadataValues",
|
|
75
|
+
"NeuralClusteringConfig",
|
|
76
|
+
"NeuralClusteringResult",
|
|
77
|
+
"RepresentativeSelectionResult",
|
|
78
|
+
"SplitAssignment",
|
|
79
|
+
"SplitConfig",
|
|
80
|
+
"SplitQualityResult",
|
|
81
|
+
"SplitResult",
|
|
82
|
+
"SplitSubset",
|
|
83
|
+
"TemporalSplitConfig",
|
|
84
|
+
"TemporalSplitResult",
|
|
85
|
+
"cluster_sequences",
|
|
86
|
+
"deduplicate",
|
|
87
|
+
"deduplicate_approximate",
|
|
88
|
+
"deduplicate_iupac",
|
|
89
|
+
"detect_leakage",
|
|
90
|
+
"evaluate_split_quality",
|
|
91
|
+
"exclude_by_metadata",
|
|
92
|
+
"exclude_chromosomes",
|
|
93
|
+
"exclude_species",
|
|
94
|
+
"hierarchical_cluster",
|
|
95
|
+
"joint_split",
|
|
96
|
+
"neural_cluster_sequences",
|
|
97
|
+
"select_representatives",
|
|
98
|
+
"split",
|
|
99
|
+
"temporal_split",
|
|
100
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Shared bounded pairwise primitives for advanced dataset algorithms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from dnakit.alignment import AlignmentConfig, align_pairwise
|
|
9
|
+
from dnakit.core import DNARecord, Gap, Topology
|
|
10
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
11
|
+
from dnakit.fingerprints import kmer_fingerprint
|
|
12
|
+
from dnakit.similarity import edit_distance, fingerprint_similarity, kmer_similarity
|
|
13
|
+
|
|
14
|
+
from .config import ClusterMethod
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnionFind:
|
|
18
|
+
def __init__(self, size: int) -> None:
|
|
19
|
+
self.parent = list(range(size))
|
|
20
|
+
|
|
21
|
+
def find(self, value: int) -> int:
|
|
22
|
+
root = value
|
|
23
|
+
while self.parent[root] != root:
|
|
24
|
+
root = self.parent[root]
|
|
25
|
+
while self.parent[value] != value:
|
|
26
|
+
parent = self.parent[value]
|
|
27
|
+
self.parent[value] = root
|
|
28
|
+
value = parent
|
|
29
|
+
return root
|
|
30
|
+
|
|
31
|
+
def union(self, left: int, right: int) -> None:
|
|
32
|
+
left_root, right_root = self.find(left), self.find(right)
|
|
33
|
+
if left_root != right_root:
|
|
34
|
+
self.parent[right_root] = left_root
|
|
35
|
+
|
|
36
|
+
def groups(self) -> tuple[tuple[int, ...], ...]:
|
|
37
|
+
grouped: dict[int, list[int]] = {}
|
|
38
|
+
for index in range(len(self.parent)):
|
|
39
|
+
grouped.setdefault(self.find(index), []).append(index)
|
|
40
|
+
return tuple(tuple(values) for values in grouped.values())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_max_records(value: int, maximum: int) -> None:
|
|
44
|
+
if value > maximum:
|
|
45
|
+
raise ConfigurationError(
|
|
46
|
+
"Advanced dataset input exceeds max_records.",
|
|
47
|
+
code="ADVANCED_DATASET_SIZE_LIMIT",
|
|
48
|
+
context={"record_count": value, "max_records": maximum},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def materialize_limited(
|
|
53
|
+
records: Iterable[DNARecord],
|
|
54
|
+
*,
|
|
55
|
+
max_records: int,
|
|
56
|
+
) -> tuple[DNARecord, ...]:
|
|
57
|
+
try:
|
|
58
|
+
iterator = iter(records)
|
|
59
|
+
except TypeError as exc:
|
|
60
|
+
raise ConfigurationError(
|
|
61
|
+
"records must be iterable.", code="INVALID_DATASET_RECORDS"
|
|
62
|
+
) from exc
|
|
63
|
+
values: list[DNARecord] = []
|
|
64
|
+
for index, record in enumerate(iterator):
|
|
65
|
+
if not isinstance(record, DNARecord):
|
|
66
|
+
raise ConfigurationError(
|
|
67
|
+
"Every dataset item must be a DNARecord.",
|
|
68
|
+
code="INVALID_DATASET_RECORD",
|
|
69
|
+
context={"input_index": index},
|
|
70
|
+
)
|
|
71
|
+
values.append(record)
|
|
72
|
+
if len(values) > max_records:
|
|
73
|
+
validate_max_records(len(values), max_records)
|
|
74
|
+
return tuple(values)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_pairwise_sequences(records: tuple[DNARecord, ...], *, operation: str) -> None:
|
|
78
|
+
for index, record in enumerate(records):
|
|
79
|
+
if record.sequence.topology is Topology.CIRCULAR:
|
|
80
|
+
raise ConfigurationError(
|
|
81
|
+
f"{operation} requires linear sequences.",
|
|
82
|
+
code="CIRCULAR_ADVANCED_DATASET_UNSUPPORTED",
|
|
83
|
+
context={"input_index": index, "record_id": record.id},
|
|
84
|
+
)
|
|
85
|
+
if any(isinstance(part, Gap) for part in record.sequence.parts):
|
|
86
|
+
raise UnsupportedGapOperationError(
|
|
87
|
+
f"{operation} does not silently omit or bridge Gap objects.",
|
|
88
|
+
code="ADVANCED_DATASET_GAP_UNSUPPORTED",
|
|
89
|
+
context={"input_index": index, "record_id": record.id},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def pair_count(size: int) -> int:
|
|
94
|
+
return size * (size - 1) // 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ensure_pair_limit(size: int, limit: int) -> int:
|
|
98
|
+
count = pair_count(size)
|
|
99
|
+
if count > limit:
|
|
100
|
+
raise ConfigurationError(
|
|
101
|
+
"Pairwise workload exceeds max_pairwise_comparisons.",
|
|
102
|
+
code="ADVANCED_PAIRWISE_LIMIT",
|
|
103
|
+
context={"record_count": size, "pairwise_comparisons": count, "limit": limit},
|
|
104
|
+
)
|
|
105
|
+
return count
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def quality_score(record: DNARecord) -> float:
|
|
109
|
+
values = record.letter_annotations.get("phred_quality")
|
|
110
|
+
return float("-inf") if not values else math.fsum(values) / len(values)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def pair_similarity(
|
|
114
|
+
left: DNARecord,
|
|
115
|
+
right: DNARecord,
|
|
116
|
+
*,
|
|
117
|
+
method: ClusterMethod,
|
|
118
|
+
k: int,
|
|
119
|
+
canonical: bool,
|
|
120
|
+
max_alignment_cells: int,
|
|
121
|
+
) -> float:
|
|
122
|
+
if method in {"kmer", "fingerprint"} and (
|
|
123
|
+
left.sequence.symbol_length < k or right.sequence.symbol_length < k
|
|
124
|
+
):
|
|
125
|
+
raise ConfigurationError(
|
|
126
|
+
"k exceeds at least one sequence length in a pairwise dataset comparison.",
|
|
127
|
+
code="DATASET_K_EXCEEDS_SEQUENCE_LENGTH",
|
|
128
|
+
context={
|
|
129
|
+
"k": k,
|
|
130
|
+
"left_length": left.sequence.symbol_length,
|
|
131
|
+
"right_length": right.sequence.symbol_length,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
if method == "identity":
|
|
135
|
+
alignment_result = align_pairwise(
|
|
136
|
+
left,
|
|
137
|
+
right,
|
|
138
|
+
config=AlignmentConfig(mode="global", max_cells=max_alignment_cells),
|
|
139
|
+
)
|
|
140
|
+
return 1.0 if alignment_result.identity is None else alignment_result.identity
|
|
141
|
+
if method == "edit":
|
|
142
|
+
distance_result = edit_distance(
|
|
143
|
+
left,
|
|
144
|
+
right,
|
|
145
|
+
max_cells=max_alignment_cells,
|
|
146
|
+
)
|
|
147
|
+
denominator = max(left.sequence.symbol_length, right.sequence.symbol_length)
|
|
148
|
+
return 1.0 if denominator == 0 else 1.0 - distance_result.distance / denominator
|
|
149
|
+
if method == "kmer":
|
|
150
|
+
return kmer_similarity(
|
|
151
|
+
left,
|
|
152
|
+
right,
|
|
153
|
+
k=k,
|
|
154
|
+
metric="jaccard",
|
|
155
|
+
canonical=canonical,
|
|
156
|
+
).value
|
|
157
|
+
left_fingerprint = kmer_fingerprint(left, k=k, canonical=canonical, mode="binary")
|
|
158
|
+
right_fingerprint = kmer_fingerprint(right, k=k, canonical=canonical, mode="binary")
|
|
159
|
+
return fingerprint_similarity(left_fingerprint, right_fingerprint, metric="tanimoto").value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def similarity_matrix(
|
|
163
|
+
records: tuple[DNARecord, ...],
|
|
164
|
+
*,
|
|
165
|
+
method: ClusterMethod,
|
|
166
|
+
k: int,
|
|
167
|
+
canonical: bool,
|
|
168
|
+
max_alignment_cells: int,
|
|
169
|
+
) -> tuple[tuple[float, ...], ...]:
|
|
170
|
+
rows = [
|
|
171
|
+
[1.0 if left == right else 0.0 for right in range(len(records))]
|
|
172
|
+
for left in range(len(records))
|
|
173
|
+
]
|
|
174
|
+
for left in range(len(records)):
|
|
175
|
+
for right in range(left + 1, len(records)):
|
|
176
|
+
value = pair_similarity(
|
|
177
|
+
records[left],
|
|
178
|
+
records[right],
|
|
179
|
+
method=method,
|
|
180
|
+
k=k,
|
|
181
|
+
canonical=canonical,
|
|
182
|
+
max_alignment_cells=max_alignment_cells,
|
|
183
|
+
)
|
|
184
|
+
rows[left][right] = rows[right][left] = value
|
|
185
|
+
return tuple(tuple(row) for row in rows)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = [
|
|
189
|
+
"UnionFind",
|
|
190
|
+
"ensure_pair_limit",
|
|
191
|
+
"materialize_limited",
|
|
192
|
+
"pair_count",
|
|
193
|
+
"pair_similarity",
|
|
194
|
+
"quality_score",
|
|
195
|
+
"similarity_matrix",
|
|
196
|
+
"validate_max_records",
|
|
197
|
+
"validate_pairwise_sequences",
|
|
198
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Type-preserving keys for immutable JSON metadata values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
|
|
7
|
+
from dnakit.core._json import FrozenDict, JSONValue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def metadata_value_key(value: JSONValue) -> Hashable:
|
|
11
|
+
"""Return a recursive key that does not apply Python numeric coercion rules."""
|
|
12
|
+
|
|
13
|
+
if value is None:
|
|
14
|
+
return ("null",)
|
|
15
|
+
if isinstance(value, bool):
|
|
16
|
+
return ("bool", value)
|
|
17
|
+
if isinstance(value, int):
|
|
18
|
+
return ("int", value)
|
|
19
|
+
if isinstance(value, float):
|
|
20
|
+
return ("float", value)
|
|
21
|
+
if isinstance(value, str):
|
|
22
|
+
return ("string", value)
|
|
23
|
+
if isinstance(value, tuple):
|
|
24
|
+
return ("array", tuple(metadata_value_key(item) for item in value))
|
|
25
|
+
if isinstance(value, FrozenDict):
|
|
26
|
+
return (
|
|
27
|
+
"object",
|
|
28
|
+
tuple((key, metadata_value_key(item)) for key, item in sorted(value.items())),
|
|
29
|
+
)
|
|
30
|
+
raise TypeError(f"Unsupported frozen JSON metadata type: {type(value).__name__}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["metadata_value_key"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""IUPAC-compatible and approximate deduplication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from dnakit.core import DNARecord, DNASet, Gap, Topology
|
|
9
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
10
|
+
|
|
11
|
+
from ._advanced_shared import ensure_pair_limit, materialize_limited, quality_score
|
|
12
|
+
from .clustering import cluster_sequences
|
|
13
|
+
from .config import ClusterConfig, IUPACDeduplicationConfig
|
|
14
|
+
from .results import (
|
|
15
|
+
ClusteringResult,
|
|
16
|
+
IUPACDeduplicationResult,
|
|
17
|
+
IUPACDuplicateGroup,
|
|
18
|
+
IUPACPairRelation,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_IUPAC = {
|
|
22
|
+
"A": frozenset("A"),
|
|
23
|
+
"C": frozenset("C"),
|
|
24
|
+
"G": frozenset("G"),
|
|
25
|
+
"T": frozenset("T"),
|
|
26
|
+
"R": frozenset("AG"),
|
|
27
|
+
"Y": frozenset("CT"),
|
|
28
|
+
"S": frozenset("GC"),
|
|
29
|
+
"W": frozenset("AT"),
|
|
30
|
+
"K": frozenset("GT"),
|
|
31
|
+
"M": frozenset("AC"),
|
|
32
|
+
"B": frozenset("CGT"),
|
|
33
|
+
"D": frozenset("AGT"),
|
|
34
|
+
"H": frozenset("ACT"),
|
|
35
|
+
"V": frozenset("ACG"),
|
|
36
|
+
"N": frozenset("ACGT"),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _compatible(left: str, right: str) -> bool:
|
|
41
|
+
return len(left) == len(right) and all(
|
|
42
|
+
bool(_IUPAC[left_symbol] & _IUPAC[right_symbol])
|
|
43
|
+
for left_symbol, right_symbol in zip(left, right, strict=True)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _representative(
|
|
48
|
+
members: tuple[int, ...],
|
|
49
|
+
records: tuple[DNARecord, ...],
|
|
50
|
+
policy: str,
|
|
51
|
+
) -> int:
|
|
52
|
+
if policy == "first":
|
|
53
|
+
return members[0]
|
|
54
|
+
if policy == "shortest":
|
|
55
|
+
return min(members, key=lambda index: (records[index].sequence.symbol_length, index))
|
|
56
|
+
if policy == "longest":
|
|
57
|
+
return max(members, key=lambda index: (records[index].sequence.symbol_length, -index))
|
|
58
|
+
return max(members, key=lambda index: (quality_score(records[index]), -index))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def deduplicate_iupac(
|
|
62
|
+
records: Iterable[DNARecord],
|
|
63
|
+
*,
|
|
64
|
+
config: IUPACDeduplicationConfig | None = None,
|
|
65
|
+
) -> IUPACDeduplicationResult:
|
|
66
|
+
"""Greedily build stable complete-link groups of pairwise-compatible IUPAC strings."""
|
|
67
|
+
|
|
68
|
+
resolved = IUPACDeduplicationConfig() if config is None else config
|
|
69
|
+
if not isinstance(resolved, IUPACDeduplicationConfig):
|
|
70
|
+
raise ConfigurationError(
|
|
71
|
+
"config must be IUPACDeduplicationConfig or None.",
|
|
72
|
+
code="INVALID_IUPAC_DEDUPLICATION_CONFIG",
|
|
73
|
+
)
|
|
74
|
+
materialized = materialize_limited(records, max_records=resolved.max_records)
|
|
75
|
+
comparisons = ensure_pair_limit(len(materialized), resolved.max_pairwise_comparisons)
|
|
76
|
+
for index, record in enumerate(materialized):
|
|
77
|
+
if record.sequence.topology is not Topology.LINEAR:
|
|
78
|
+
raise ConfigurationError(
|
|
79
|
+
"IUPAC-aware deduplication currently requires linear sequences.",
|
|
80
|
+
code="IUPAC_DEDUPLICATION_LINEAR_REQUIRED",
|
|
81
|
+
context={"input_index": index},
|
|
82
|
+
)
|
|
83
|
+
if any(isinstance(part, Gap) for part in record.sequence.parts):
|
|
84
|
+
raise UnsupportedGapOperationError(
|
|
85
|
+
"IUPAC-aware deduplication does not bridge Gap objects.",
|
|
86
|
+
code="IUPAC_DEDUPLICATION_GAP_UNSUPPORTED",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
compatibility = [bytearray(len(materialized) - left - 1) for left in range(len(materialized))]
|
|
90
|
+
identical_pairs = 0
|
|
91
|
+
compatible_pairs = 0
|
|
92
|
+
conflict_pairs = 0
|
|
93
|
+
pair_relations: list[IUPACPairRelation] = []
|
|
94
|
+
for left in range(len(materialized)):
|
|
95
|
+
left_symbols = materialized[left].sequence.symbols
|
|
96
|
+
for right in range(left + 1, len(materialized)):
|
|
97
|
+
right_symbols = materialized[right].sequence.symbols
|
|
98
|
+
is_compatible = _compatible(left_symbols, right_symbols)
|
|
99
|
+
compatibility[left][right - left - 1] = is_compatible
|
|
100
|
+
relation: Literal["identical", "compatible", "conflict"]
|
|
101
|
+
if left_symbols == right_symbols:
|
|
102
|
+
identical_pairs += 1
|
|
103
|
+
relation = "identical"
|
|
104
|
+
elif is_compatible:
|
|
105
|
+
compatible_pairs += 1
|
|
106
|
+
relation = "compatible"
|
|
107
|
+
else:
|
|
108
|
+
conflict_pairs += 1
|
|
109
|
+
relation = "conflict"
|
|
110
|
+
pair_relations.append(
|
|
111
|
+
IUPACPairRelation(
|
|
112
|
+
left,
|
|
113
|
+
right,
|
|
114
|
+
materialized[left].id,
|
|
115
|
+
materialized[right].id,
|
|
116
|
+
relation,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def pair_is_compatible(left: int, right: int) -> bool:
|
|
121
|
+
if left > right:
|
|
122
|
+
left, right = right, left
|
|
123
|
+
return bool(compatibility[left][right - left - 1])
|
|
124
|
+
|
|
125
|
+
groups: list[list[int]] = []
|
|
126
|
+
for index in range(len(materialized)):
|
|
127
|
+
for group in groups:
|
|
128
|
+
if all(pair_is_compatible(index, member) for member in group):
|
|
129
|
+
group.append(index)
|
|
130
|
+
break
|
|
131
|
+
else:
|
|
132
|
+
groups.append([index])
|
|
133
|
+
|
|
134
|
+
audit: list[IUPACDuplicateGroup] = []
|
|
135
|
+
selected: list[DNARecord] = []
|
|
136
|
+
for group_index, raw_members in enumerate(groups):
|
|
137
|
+
members = tuple(raw_members)
|
|
138
|
+
representative = _representative(members, materialized, resolved.representative_policy)
|
|
139
|
+
selected.append(materialized[representative])
|
|
140
|
+
symbols = {materialized[index].sequence.symbols for index in members}
|
|
141
|
+
audit.append(
|
|
142
|
+
IUPACDuplicateGroup(
|
|
143
|
+
group_index,
|
|
144
|
+
materialized[representative].id,
|
|
145
|
+
tuple(materialized[index].id for index in members),
|
|
146
|
+
(
|
|
147
|
+
"singleton"
|
|
148
|
+
if len(members) == 1
|
|
149
|
+
else "identical"
|
|
150
|
+
if len(symbols) == 1
|
|
151
|
+
else "compatible"
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return IUPACDeduplicationResult(
|
|
156
|
+
DNASet(selected),
|
|
157
|
+
tuple(audit),
|
|
158
|
+
tuple(pair_relations),
|
|
159
|
+
resolved.representative_policy,
|
|
160
|
+
"position-compatible iff IUPAC base-set intersection is non-empty",
|
|
161
|
+
"stable-greedy-complete-link",
|
|
162
|
+
len(materialized),
|
|
163
|
+
len(selected),
|
|
164
|
+
comparisons,
|
|
165
|
+
identical_pairs,
|
|
166
|
+
compatible_pairs,
|
|
167
|
+
conflict_pairs,
|
|
168
|
+
resolved.max_records,
|
|
169
|
+
resolved.max_pairwise_comparisons,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def deduplicate_approximate(
|
|
174
|
+
records: Iterable[DNARecord],
|
|
175
|
+
*,
|
|
176
|
+
config: ClusterConfig,
|
|
177
|
+
) -> ClusteringResult:
|
|
178
|
+
"""Return threshold-graph components and representatives as near-duplicate groups."""
|
|
179
|
+
|
|
180
|
+
return cluster_sequences(records, config=config)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
__all__ = ["deduplicate_approximate", "deduplicate_iupac"]
|