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,662 @@
|
|
|
1
|
+
"""Exact k-mer and numeric fingerprint comparisons."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from typing import Literal, TypeAlias, TypeGuard
|
|
9
|
+
|
|
10
|
+
from dnakit.core._json import FrozenDict
|
|
11
|
+
from dnakit.exceptions import ConfigurationError
|
|
12
|
+
from dnakit.fingerprints import BitFingerprintResult, FingerprintResult
|
|
13
|
+
from dnakit.similarity._shared import (
|
|
14
|
+
NamedVector,
|
|
15
|
+
NumericVector,
|
|
16
|
+
SequenceInput,
|
|
17
|
+
sequence_and_id,
|
|
18
|
+
validate_bool,
|
|
19
|
+
validate_positive_int,
|
|
20
|
+
)
|
|
21
|
+
from dnakit.similarity.results import SimilarityResult
|
|
22
|
+
|
|
23
|
+
VectorMetric: TypeAlias = Literal["tanimoto", "jaccard", "cosine", "euclidean", "manhattan"]
|
|
24
|
+
KmerMetric: TypeAlias = Literal["jaccard", "containment", "cosine"]
|
|
25
|
+
KmerMode: TypeAlias = Literal["set", "count"]
|
|
26
|
+
ZeroVectorPolicy: TypeAlias = Literal["identity", "error"]
|
|
27
|
+
FingerprintVectorResult: TypeAlias = FingerprintResult | BitFingerprintResult
|
|
28
|
+
VectorInput: TypeAlias = FingerprintVectorResult | NumericVector | NamedVector
|
|
29
|
+
WeightInput: TypeAlias = NumericVector | NamedVector
|
|
30
|
+
KmerVectorInput: TypeAlias = FingerprintResult | NamedVector
|
|
31
|
+
|
|
32
|
+
_IUPAC_COMPLEMENT = str.maketrans(
|
|
33
|
+
{
|
|
34
|
+
"A": "T",
|
|
35
|
+
"C": "G",
|
|
36
|
+
"G": "C",
|
|
37
|
+
"T": "A",
|
|
38
|
+
"R": "Y",
|
|
39
|
+
"Y": "R",
|
|
40
|
+
"S": "S",
|
|
41
|
+
"W": "W",
|
|
42
|
+
"K": "M",
|
|
43
|
+
"M": "K",
|
|
44
|
+
"B": "V",
|
|
45
|
+
"D": "H",
|
|
46
|
+
"H": "D",
|
|
47
|
+
"V": "B",
|
|
48
|
+
"N": "N",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _validate_zero_policy(value: ZeroVectorPolicy) -> ZeroVectorPolicy:
|
|
54
|
+
if value not in ("identity", "error"):
|
|
55
|
+
raise ConfigurationError(
|
|
56
|
+
"zero_vector_policy must be 'identity' or 'error'.",
|
|
57
|
+
code="INVALID_ZERO_VECTOR_POLICY",
|
|
58
|
+
context={"zero_vector_policy": value},
|
|
59
|
+
)
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _finite_number(value: object, *, role: str, index: int | str) -> float:
|
|
64
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
65
|
+
raise ConfigurationError(
|
|
66
|
+
"Vector values must be finite numbers.",
|
|
67
|
+
code="INVALID_VECTOR_VALUE",
|
|
68
|
+
context={"role": role, "index": index, "value": value},
|
|
69
|
+
)
|
|
70
|
+
return float(value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_fingerprint_result(value: object) -> TypeGuard[FingerprintVectorResult]:
|
|
74
|
+
return isinstance(value, (FingerprintResult, BitFingerprintResult))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _fingerprint_values(value: FingerprintVectorResult, *, role: str) -> tuple[float, ...]:
|
|
78
|
+
return tuple(
|
|
79
|
+
_finite_number(item, role=role, index=index)
|
|
80
|
+
for index, item in enumerate(value.dense_values())
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _mapping_values(
|
|
85
|
+
left: Mapping[str, int | float],
|
|
86
|
+
right: Mapping[str, int | float],
|
|
87
|
+
) -> tuple[tuple[float, ...], tuple[float, ...], tuple[str, ...]]:
|
|
88
|
+
if any(not isinstance(key, str) or not key for key in (*left.keys(), *right.keys())):
|
|
89
|
+
raise ConfigurationError(
|
|
90
|
+
"Named-vector feature names must be non-empty strings.",
|
|
91
|
+
code="INVALID_VECTOR_FEATURE",
|
|
92
|
+
)
|
|
93
|
+
features = tuple(sorted(set(left) | set(right)))
|
|
94
|
+
left_values = tuple(
|
|
95
|
+
_finite_number(left.get(feature, 0), role="left", index=feature) for feature in features
|
|
96
|
+
)
|
|
97
|
+
right_values = tuple(
|
|
98
|
+
_finite_number(right.get(feature, 0), role="right", index=feature) for feature in features
|
|
99
|
+
)
|
|
100
|
+
return left_values, right_values, features
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _sequence_values(value: object, *, role: str) -> tuple[float, ...]:
|
|
104
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
105
|
+
raise ConfigurationError(
|
|
106
|
+
"Numeric vectors must be non-text sequences of finite numbers.",
|
|
107
|
+
code="INVALID_VECTOR_INPUT",
|
|
108
|
+
context={"role": role, "input_type": type(value).__name__},
|
|
109
|
+
)
|
|
110
|
+
return tuple(_finite_number(item, role=role, index=index) for index, item in enumerate(value))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _align_vectors(
|
|
114
|
+
left: VectorInput,
|
|
115
|
+
right: VectorInput,
|
|
116
|
+
) -> tuple[tuple[float, ...], tuple[float, ...], tuple[str, ...] | None, str | None, str | None]:
|
|
117
|
+
if _is_fingerprint_result(left) and _is_fingerprint_result(right):
|
|
118
|
+
left_parameters = left.parameters if isinstance(left, BitFingerprintResult) else None
|
|
119
|
+
right_parameters = right.parameters if isinstance(right, BitFingerprintResult) else None
|
|
120
|
+
if (
|
|
121
|
+
left.schema_version != right.schema_version
|
|
122
|
+
or left.feature_names != right.feature_names
|
|
123
|
+
or left_parameters != right_parameters
|
|
124
|
+
):
|
|
125
|
+
raise ConfigurationError(
|
|
126
|
+
"Fingerprint schemas must match exactly before comparison.",
|
|
127
|
+
code="FINGERPRINT_SCHEMA_MISMATCH",
|
|
128
|
+
context={
|
|
129
|
+
"left_schema": left.schema_version,
|
|
130
|
+
"right_schema": right.schema_version,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
return (
|
|
134
|
+
_fingerprint_values(left, role="left"),
|
|
135
|
+
_fingerprint_values(right, role="right"),
|
|
136
|
+
left.feature_names,
|
|
137
|
+
left.sequence_id,
|
|
138
|
+
right.sequence_id,
|
|
139
|
+
)
|
|
140
|
+
if _is_fingerprint_result(left) or _is_fingerprint_result(right):
|
|
141
|
+
raise ConfigurationError(
|
|
142
|
+
"A versioned fingerprint cannot be mixed with an unversioned vector.",
|
|
143
|
+
code="MIXED_VECTOR_TYPES",
|
|
144
|
+
)
|
|
145
|
+
if isinstance(left, Mapping) and isinstance(right, Mapping):
|
|
146
|
+
left_values, right_values, features = _mapping_values(left, right)
|
|
147
|
+
return left_values, right_values, features, None, None
|
|
148
|
+
if isinstance(left, Mapping) or isinstance(right, Mapping):
|
|
149
|
+
raise ConfigurationError(
|
|
150
|
+
"Named and positional vectors cannot be mixed.",
|
|
151
|
+
code="MIXED_VECTOR_TYPES",
|
|
152
|
+
)
|
|
153
|
+
left_values = _sequence_values(left, role="left")
|
|
154
|
+
right_values = _sequence_values(right, role="right")
|
|
155
|
+
if len(left_values) != len(right_values):
|
|
156
|
+
raise ConfigurationError(
|
|
157
|
+
"Positional vectors must have equal dimensions.",
|
|
158
|
+
code="VECTOR_DIMENSION_MISMATCH",
|
|
159
|
+
context={"left_dimension": len(left_values), "right_dimension": len(right_values)},
|
|
160
|
+
)
|
|
161
|
+
return left_values, right_values, None, None, None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _resolve_weights(
|
|
165
|
+
weights: WeightInput | None,
|
|
166
|
+
*,
|
|
167
|
+
dimension: int,
|
|
168
|
+
features: tuple[str, ...] | None,
|
|
169
|
+
) -> tuple[float, ...]:
|
|
170
|
+
if weights is None:
|
|
171
|
+
return (1.0,) * dimension
|
|
172
|
+
if isinstance(weights, Mapping):
|
|
173
|
+
if features is None:
|
|
174
|
+
raise ConfigurationError(
|
|
175
|
+
"Mapping weights require named vector features.",
|
|
176
|
+
code="INVALID_VECTOR_WEIGHTS",
|
|
177
|
+
)
|
|
178
|
+
if any(not isinstance(feature, str) or not feature for feature in weights):
|
|
179
|
+
raise ConfigurationError(
|
|
180
|
+
"Weight feature names must be non-empty strings.",
|
|
181
|
+
code="INVALID_VECTOR_WEIGHTS",
|
|
182
|
+
)
|
|
183
|
+
unknown = set(weights) - set(features)
|
|
184
|
+
if unknown:
|
|
185
|
+
raise ConfigurationError(
|
|
186
|
+
"Weights contain unknown feature names.",
|
|
187
|
+
code="UNKNOWN_WEIGHT_FEATURE",
|
|
188
|
+
context={"features": sorted(unknown)},
|
|
189
|
+
)
|
|
190
|
+
resolved = tuple(
|
|
191
|
+
_finite_number(weights.get(feature, 1), role="weight", index=feature)
|
|
192
|
+
for feature in features
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
resolved = _sequence_values(weights, role="weight")
|
|
196
|
+
if len(resolved) != dimension:
|
|
197
|
+
raise ConfigurationError(
|
|
198
|
+
"Weight dimension must match vector dimension.",
|
|
199
|
+
code="WEIGHT_DIMENSION_MISMATCH",
|
|
200
|
+
context={"weight_dimension": len(resolved), "dimension": dimension},
|
|
201
|
+
)
|
|
202
|
+
if any(weight < 0 for weight in resolved):
|
|
203
|
+
raise ConfigurationError(
|
|
204
|
+
"Feature weights must be non-negative.",
|
|
205
|
+
code="NEGATIVE_VECTOR_WEIGHT",
|
|
206
|
+
)
|
|
207
|
+
return resolved
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _resolved_weight_parameter(
|
|
211
|
+
weights: WeightInput | None,
|
|
212
|
+
*,
|
|
213
|
+
resolved: tuple[float, ...],
|
|
214
|
+
features: tuple[str, ...] | None,
|
|
215
|
+
) -> tuple[float, ...] | FrozenDict | None:
|
|
216
|
+
"""Return the exact applied weights in a deterministic audit form."""
|
|
217
|
+
|
|
218
|
+
if weights is None:
|
|
219
|
+
return None
|
|
220
|
+
if isinstance(weights, Mapping):
|
|
221
|
+
if features is None: # Guarded by ``_resolve_weights``.
|
|
222
|
+
raise ConfigurationError(
|
|
223
|
+
"Mapping weights require named vector features.",
|
|
224
|
+
code="INVALID_VECTOR_WEIGHTS",
|
|
225
|
+
)
|
|
226
|
+
return FrozenDict(
|
|
227
|
+
{feature: weight for feature, weight in zip(features, resolved, strict=True)}
|
|
228
|
+
)
|
|
229
|
+
return resolved
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _undefined_zero_similarity(
|
|
233
|
+
*,
|
|
234
|
+
left_zero: bool,
|
|
235
|
+
right_zero: bool,
|
|
236
|
+
policy: ZeroVectorPolicy,
|
|
237
|
+
metric: str,
|
|
238
|
+
) -> float:
|
|
239
|
+
if policy == "error":
|
|
240
|
+
raise ConfigurationError(
|
|
241
|
+
f"{metric} is undefined for an effective zero vector.",
|
|
242
|
+
code="ZERO_VECTOR_UNDEFINED",
|
|
243
|
+
context={"left_zero": left_zero, "right_zero": right_zero, "metric": metric},
|
|
244
|
+
)
|
|
245
|
+
return 1.0 if left_zero and right_zero else 0.0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _compute_vector_metric(
|
|
249
|
+
left: tuple[float, ...],
|
|
250
|
+
right: tuple[float, ...],
|
|
251
|
+
weights: tuple[float, ...],
|
|
252
|
+
*,
|
|
253
|
+
metric: VectorMetric,
|
|
254
|
+
zero_policy: ZeroVectorPolicy,
|
|
255
|
+
) -> tuple[float, Literal["similarity", "distance"], FrozenDict]:
|
|
256
|
+
if metric in ("tanimoto", "jaccard") and any(value < 0 for value in (*left, *right)):
|
|
257
|
+
raise ConfigurationError(
|
|
258
|
+
f"{metric} requires non-negative vector values.",
|
|
259
|
+
code="NEGATIVE_VECTOR_NOT_ALLOWED",
|
|
260
|
+
context={"metric": metric},
|
|
261
|
+
)
|
|
262
|
+
if metric == "jaccard":
|
|
263
|
+
intersection = math.fsum(
|
|
264
|
+
weight
|
|
265
|
+
for left_value, right_value, weight in zip(left, right, weights, strict=True)
|
|
266
|
+
if left_value > 0 and right_value > 0
|
|
267
|
+
)
|
|
268
|
+
union = math.fsum(
|
|
269
|
+
weight
|
|
270
|
+
for left_value, right_value, weight in zip(left, right, weights, strict=True)
|
|
271
|
+
if left_value > 0 or right_value > 0
|
|
272
|
+
)
|
|
273
|
+
if union == 0:
|
|
274
|
+
value = _undefined_zero_similarity(
|
|
275
|
+
left_zero=True,
|
|
276
|
+
right_zero=True,
|
|
277
|
+
policy=zero_policy,
|
|
278
|
+
metric=metric,
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
value = intersection / union
|
|
282
|
+
return (
|
|
283
|
+
value,
|
|
284
|
+
"similarity",
|
|
285
|
+
FrozenDict({"intersection_weight": intersection, "union_weight": union}),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
dot = math.fsum(
|
|
289
|
+
weight * left_value * right_value
|
|
290
|
+
for left_value, right_value, weight in zip(left, right, weights, strict=True)
|
|
291
|
+
)
|
|
292
|
+
left_squared = math.fsum(
|
|
293
|
+
weight * value * value for value, weight in zip(left, weights, strict=True)
|
|
294
|
+
)
|
|
295
|
+
right_squared = math.fsum(
|
|
296
|
+
weight * value * value for value, weight in zip(right, weights, strict=True)
|
|
297
|
+
)
|
|
298
|
+
if metric == "tanimoto":
|
|
299
|
+
denominator = math.fsum((left_squared, right_squared, -dot))
|
|
300
|
+
if math.isclose(denominator, 0.0):
|
|
301
|
+
value = _undefined_zero_similarity(
|
|
302
|
+
left_zero=math.isclose(left_squared, 0.0),
|
|
303
|
+
right_zero=math.isclose(right_squared, 0.0),
|
|
304
|
+
policy=zero_policy,
|
|
305
|
+
metric=metric,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
value = dot / denominator
|
|
309
|
+
return (
|
|
310
|
+
value,
|
|
311
|
+
"similarity",
|
|
312
|
+
FrozenDict(
|
|
313
|
+
{
|
|
314
|
+
"dot_product": dot,
|
|
315
|
+
"left_squared_norm": left_squared,
|
|
316
|
+
"right_squared_norm": right_squared,
|
|
317
|
+
"denominator": denominator,
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
if metric == "cosine":
|
|
322
|
+
denominator = math.sqrt(left_squared * right_squared)
|
|
323
|
+
if math.isclose(denominator, 0.0):
|
|
324
|
+
value = _undefined_zero_similarity(
|
|
325
|
+
left_zero=math.isclose(left_squared, 0.0),
|
|
326
|
+
right_zero=math.isclose(right_squared, 0.0),
|
|
327
|
+
policy=zero_policy,
|
|
328
|
+
metric=metric,
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
value = dot / denominator
|
|
332
|
+
value = min(1.0, max(-1.0, value))
|
|
333
|
+
return (
|
|
334
|
+
value,
|
|
335
|
+
"similarity",
|
|
336
|
+
FrozenDict(
|
|
337
|
+
{
|
|
338
|
+
"dot_product": dot,
|
|
339
|
+
"left_squared_norm": left_squared,
|
|
340
|
+
"right_squared_norm": right_squared,
|
|
341
|
+
"denominator": denominator,
|
|
342
|
+
}
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
if metric == "euclidean":
|
|
346
|
+
squared_distance = math.fsum(
|
|
347
|
+
weight * (left_value - right_value) ** 2
|
|
348
|
+
for left_value, right_value, weight in zip(left, right, weights, strict=True)
|
|
349
|
+
)
|
|
350
|
+
return (
|
|
351
|
+
math.sqrt(squared_distance),
|
|
352
|
+
"distance",
|
|
353
|
+
FrozenDict({"squared_distance": squared_distance}),
|
|
354
|
+
)
|
|
355
|
+
if metric == "manhattan":
|
|
356
|
+
distance = math.fsum(
|
|
357
|
+
weight * abs(left_value - right_value)
|
|
358
|
+
for left_value, right_value, weight in zip(left, right, weights, strict=True)
|
|
359
|
+
)
|
|
360
|
+
return distance, "distance", FrozenDict({"absolute_distance": distance})
|
|
361
|
+
raise ConfigurationError(
|
|
362
|
+
"Unknown fingerprint similarity metric.",
|
|
363
|
+
code="INVALID_VECTOR_METRIC",
|
|
364
|
+
context={"metric": metric},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def fingerprint_similarity(
|
|
369
|
+
left: VectorInput,
|
|
370
|
+
right: VectorInput,
|
|
371
|
+
*,
|
|
372
|
+
metric: VectorMetric = "tanimoto",
|
|
373
|
+
weights: WeightInput | None = None,
|
|
374
|
+
zero_vector_policy: ZeroVectorPolicy = "identity",
|
|
375
|
+
) -> SimilarityResult:
|
|
376
|
+
"""Compare versioned fingerprints or homogeneous numeric vectors.
|
|
377
|
+
|
|
378
|
+
``jaccard`` compares non-zero feature presence. ``tanimoto`` uses the
|
|
379
|
+
generalized non-negative dot-product formula. ``cosine`` allows signed
|
|
380
|
+
vectors. Euclidean and Manhattan methods return distances. Under the
|
|
381
|
+
default zero policy, two effective zero vectors have similarity 1 while
|
|
382
|
+
exactly one zero vector has similarity 0. A ``FingerprintResult`` is
|
|
383
|
+
compared as its already-materialized vector; its recorded upstream Gap
|
|
384
|
+
traversal policy is not reinterpreted here.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
if metric not in ("tanimoto", "jaccard", "cosine", "euclidean", "manhattan"):
|
|
388
|
+
raise ConfigurationError(
|
|
389
|
+
"Unknown fingerprint similarity metric.",
|
|
390
|
+
code="INVALID_VECTOR_METRIC",
|
|
391
|
+
context={"metric": metric},
|
|
392
|
+
)
|
|
393
|
+
zero_policy = _validate_zero_policy(zero_vector_policy)
|
|
394
|
+
left_values, right_values, features, left_id, right_id = _align_vectors(left, right)
|
|
395
|
+
resolved_weights = _resolve_weights(
|
|
396
|
+
weights,
|
|
397
|
+
dimension=len(left_values),
|
|
398
|
+
features=features,
|
|
399
|
+
)
|
|
400
|
+
audited_weights = _resolved_weight_parameter(
|
|
401
|
+
weights,
|
|
402
|
+
resolved=resolved_weights,
|
|
403
|
+
features=features,
|
|
404
|
+
)
|
|
405
|
+
value, value_kind, components = _compute_vector_metric(
|
|
406
|
+
left_values,
|
|
407
|
+
right_values,
|
|
408
|
+
resolved_weights,
|
|
409
|
+
metric=metric,
|
|
410
|
+
zero_policy=zero_policy,
|
|
411
|
+
)
|
|
412
|
+
return SimilarityResult(
|
|
413
|
+
name="fingerprint_similarity",
|
|
414
|
+
method=metric,
|
|
415
|
+
value=value,
|
|
416
|
+
value_kind=value_kind,
|
|
417
|
+
left_id=left_id,
|
|
418
|
+
right_id=right_id,
|
|
419
|
+
left_dimension=len(left_values),
|
|
420
|
+
right_dimension=len(right_values),
|
|
421
|
+
parameters=FrozenDict(
|
|
422
|
+
{
|
|
423
|
+
"weighted": weights is not None,
|
|
424
|
+
"weights": audited_weights,
|
|
425
|
+
"feature_schema": features,
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
components=components,
|
|
429
|
+
zero_vector_policy=zero_policy if value_kind == "similarity" else None,
|
|
430
|
+
iupac_matching=None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _canonical_word(word: str) -> str:
|
|
435
|
+
reverse_complement = word.translate(_IUPAC_COMPLEMENT)[::-1]
|
|
436
|
+
return min(word, reverse_complement)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _kmer_counts(
|
|
440
|
+
text: str,
|
|
441
|
+
*,
|
|
442
|
+
k: int,
|
|
443
|
+
overlapping: bool,
|
|
444
|
+
canonical: bool,
|
|
445
|
+
) -> Counter[str]:
|
|
446
|
+
step = 1 if overlapping else k
|
|
447
|
+
words = (text[start : start + k] for start in range(0, len(text) - k + 1, step))
|
|
448
|
+
return Counter(_canonical_word(word) if canonical else word for word in words)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _kmer_components(
|
|
452
|
+
left: Mapping[str, int | float],
|
|
453
|
+
right: Mapping[str, int | float],
|
|
454
|
+
*,
|
|
455
|
+
mode: KmerMode,
|
|
456
|
+
metric: KmerMetric,
|
|
457
|
+
zero_policy: ZeroVectorPolicy,
|
|
458
|
+
) -> tuple[float, FrozenDict]:
|
|
459
|
+
features = tuple(sorted(set(left) | set(right)))
|
|
460
|
+
if mode == "set":
|
|
461
|
+
left_values = {feature: float(left[feature] > 0) for feature in features}
|
|
462
|
+
right_values = {feature: float(right[feature] > 0) for feature in features}
|
|
463
|
+
else:
|
|
464
|
+
left_values = {feature: float(left[feature]) for feature in features}
|
|
465
|
+
right_values = {feature: float(right[feature]) for feature in features}
|
|
466
|
+
shared = math.fsum(min(left_values[feature], right_values[feature]) for feature in features)
|
|
467
|
+
left_total = math.fsum(left_values[feature] for feature in features)
|
|
468
|
+
right_total = math.fsum(right_values[feature] for feature in features)
|
|
469
|
+
union = math.fsum(max(left_values[feature], right_values[feature]) for feature in features)
|
|
470
|
+
if metric == "jaccard":
|
|
471
|
+
if union == 0:
|
|
472
|
+
value = _undefined_zero_similarity(
|
|
473
|
+
left_zero=True,
|
|
474
|
+
right_zero=True,
|
|
475
|
+
policy=zero_policy,
|
|
476
|
+
metric="kmer_jaccard",
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
value = shared / union
|
|
480
|
+
denominator = union
|
|
481
|
+
elif metric == "containment":
|
|
482
|
+
if left_total == 0:
|
|
483
|
+
value = _undefined_zero_similarity(
|
|
484
|
+
left_zero=True,
|
|
485
|
+
right_zero=right_total == 0,
|
|
486
|
+
policy=zero_policy,
|
|
487
|
+
metric="kmer_containment",
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
value = shared / left_total
|
|
491
|
+
denominator = left_total
|
|
492
|
+
elif metric == "cosine":
|
|
493
|
+
dot = math.fsum(left_values[feature] * right_values[feature] for feature in features)
|
|
494
|
+
left_squared = math.fsum(left_values[feature] ** 2 for feature in features)
|
|
495
|
+
right_squared = math.fsum(right_values[feature] ** 2 for feature in features)
|
|
496
|
+
denominator = math.sqrt(left_squared * right_squared)
|
|
497
|
+
if denominator == 0:
|
|
498
|
+
value = _undefined_zero_similarity(
|
|
499
|
+
left_zero=left_squared == 0,
|
|
500
|
+
right_zero=right_squared == 0,
|
|
501
|
+
policy=zero_policy,
|
|
502
|
+
metric="kmer_cosine",
|
|
503
|
+
)
|
|
504
|
+
else:
|
|
505
|
+
value = dot / denominator
|
|
506
|
+
else:
|
|
507
|
+
raise ConfigurationError(
|
|
508
|
+
"Unknown k-mer similarity metric.",
|
|
509
|
+
code="INVALID_KMER_METRIC",
|
|
510
|
+
context={"metric": metric},
|
|
511
|
+
)
|
|
512
|
+
return value, FrozenDict(
|
|
513
|
+
{
|
|
514
|
+
"shared_weight": shared,
|
|
515
|
+
"left_weight": left_total,
|
|
516
|
+
"right_weight": right_total,
|
|
517
|
+
"denominator": denominator,
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def kmer_vector_similarity(
|
|
523
|
+
left: KmerVectorInput,
|
|
524
|
+
right: KmerVectorInput,
|
|
525
|
+
*,
|
|
526
|
+
metric: KmerMetric = "jaccard",
|
|
527
|
+
mode: KmerMode = "set",
|
|
528
|
+
zero_vector_policy: ZeroVectorPolicy = "identity",
|
|
529
|
+
) -> SimilarityResult:
|
|
530
|
+
"""Compare named exact k-mer vectors, including versioned fingerprints."""
|
|
531
|
+
|
|
532
|
+
if metric not in ("jaccard", "containment", "cosine"):
|
|
533
|
+
raise ConfigurationError(
|
|
534
|
+
"Unknown k-mer similarity metric.",
|
|
535
|
+
code="INVALID_KMER_METRIC",
|
|
536
|
+
context={"metric": metric},
|
|
537
|
+
)
|
|
538
|
+
if mode not in ("set", "count"):
|
|
539
|
+
raise ConfigurationError(
|
|
540
|
+
"k-mer mode must be 'set' or 'count'.",
|
|
541
|
+
code="INVALID_KMER_MODE",
|
|
542
|
+
context={"mode": mode},
|
|
543
|
+
)
|
|
544
|
+
zero_policy = _validate_zero_policy(zero_vector_policy)
|
|
545
|
+
left_values, right_values, features, left_id, right_id = _align_vectors(left, right)
|
|
546
|
+
if features is None:
|
|
547
|
+
raise ConfigurationError(
|
|
548
|
+
"k-mer vector comparison requires named features.",
|
|
549
|
+
code="UNNAMED_KMER_VECTOR",
|
|
550
|
+
)
|
|
551
|
+
if any(value < 0 for value in (*left_values, *right_values)):
|
|
552
|
+
raise ConfigurationError(
|
|
553
|
+
"k-mer vector values must be non-negative.",
|
|
554
|
+
code="NEGATIVE_VECTOR_NOT_ALLOWED",
|
|
555
|
+
)
|
|
556
|
+
left_mapping = dict(zip(features, left_values, strict=True))
|
|
557
|
+
right_mapping = dict(zip(features, right_values, strict=True))
|
|
558
|
+
value, components = _kmer_components(
|
|
559
|
+
left_mapping,
|
|
560
|
+
right_mapping,
|
|
561
|
+
mode=mode,
|
|
562
|
+
metric=metric,
|
|
563
|
+
zero_policy=zero_policy,
|
|
564
|
+
)
|
|
565
|
+
return SimilarityResult(
|
|
566
|
+
name="kmer_vector_similarity",
|
|
567
|
+
method=f"{mode}_{metric}",
|
|
568
|
+
value=value,
|
|
569
|
+
value_kind="similarity",
|
|
570
|
+
left_id=left_id,
|
|
571
|
+
right_id=right_id,
|
|
572
|
+
left_dimension=len(features),
|
|
573
|
+
right_dimension=len(features),
|
|
574
|
+
parameters=FrozenDict(
|
|
575
|
+
{
|
|
576
|
+
"metric": metric,
|
|
577
|
+
"mode": mode,
|
|
578
|
+
"feature_schema": features,
|
|
579
|
+
"source": "named_kmer_vector",
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
components=components,
|
|
583
|
+
zero_vector_policy=zero_policy,
|
|
584
|
+
iupac_matching=None,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def kmer_similarity(
|
|
589
|
+
left: SequenceInput,
|
|
590
|
+
right: SequenceInput,
|
|
591
|
+
*,
|
|
592
|
+
k: int,
|
|
593
|
+
metric: KmerMetric = "jaccard",
|
|
594
|
+
mode: KmerMode = "set",
|
|
595
|
+
canonical: bool = False,
|
|
596
|
+
overlapping: bool = True,
|
|
597
|
+
zero_vector_policy: ZeroVectorPolicy = "identity",
|
|
598
|
+
) -> SimilarityResult:
|
|
599
|
+
"""Compare exact literal-IUPAC k-mer sets or count multisets."""
|
|
600
|
+
|
|
601
|
+
validate_positive_int(k, "k")
|
|
602
|
+
validate_bool(canonical, "canonical")
|
|
603
|
+
validate_bool(overlapping, "overlapping")
|
|
604
|
+
if metric not in ("jaccard", "containment", "cosine"):
|
|
605
|
+
raise ConfigurationError(
|
|
606
|
+
"Unknown k-mer similarity metric.",
|
|
607
|
+
code="INVALID_KMER_METRIC",
|
|
608
|
+
context={"metric": metric},
|
|
609
|
+
)
|
|
610
|
+
if mode not in ("set", "count"):
|
|
611
|
+
raise ConfigurationError(
|
|
612
|
+
"k-mer mode must be 'set' or 'count'.",
|
|
613
|
+
code="INVALID_KMER_MODE",
|
|
614
|
+
context={"mode": mode},
|
|
615
|
+
)
|
|
616
|
+
zero_policy = _validate_zero_policy(zero_vector_policy)
|
|
617
|
+
left_sequence, left_id = sequence_and_id(left, role="left")
|
|
618
|
+
right_sequence, right_id = sequence_and_id(right, role="right")
|
|
619
|
+
left_counts = _kmer_counts(
|
|
620
|
+
left_sequence.symbols,
|
|
621
|
+
k=k,
|
|
622
|
+
overlapping=overlapping,
|
|
623
|
+
canonical=canonical,
|
|
624
|
+
)
|
|
625
|
+
right_counts = _kmer_counts(
|
|
626
|
+
right_sequence.symbols,
|
|
627
|
+
k=k,
|
|
628
|
+
overlapping=overlapping,
|
|
629
|
+
canonical=canonical,
|
|
630
|
+
)
|
|
631
|
+
value, components = _kmer_components(
|
|
632
|
+
left_counts,
|
|
633
|
+
right_counts,
|
|
634
|
+
mode=mode,
|
|
635
|
+
metric=metric,
|
|
636
|
+
zero_policy=zero_policy,
|
|
637
|
+
)
|
|
638
|
+
return SimilarityResult(
|
|
639
|
+
name="kmer_similarity",
|
|
640
|
+
method=f"{mode}_{metric}",
|
|
641
|
+
value=value,
|
|
642
|
+
value_kind="similarity",
|
|
643
|
+
left_id=left_id,
|
|
644
|
+
right_id=right_id,
|
|
645
|
+
left_dimension=len(left_counts),
|
|
646
|
+
right_dimension=len(right_counts),
|
|
647
|
+
parameters=FrozenDict(
|
|
648
|
+
{
|
|
649
|
+
"k": k,
|
|
650
|
+
"metric": metric,
|
|
651
|
+
"mode": mode,
|
|
652
|
+
"canonical": canonical,
|
|
653
|
+
"overlapping": overlapping,
|
|
654
|
+
}
|
|
655
|
+
),
|
|
656
|
+
components=components,
|
|
657
|
+
zero_vector_policy=zero_policy,
|
|
658
|
+
iupac_matching="literal",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
__all__ = ["fingerprint_similarity", "kmer_similarity", "kmer_vector_similarity"]
|