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,829 @@
|
|
|
1
|
+
"""Interpretable fixed-schema advanced DNA fingerprints and preprocessing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import islice
|
|
9
|
+
from typing import Literal, Protocol, cast
|
|
10
|
+
|
|
11
|
+
from dnakit.core import DNASequence, Provenance, Topology
|
|
12
|
+
from dnakit.core._json import FrozenDict, to_json_compatible
|
|
13
|
+
from dnakit.descriptors import codon_statistics, exact_repeat_fraction, gc_at_content
|
|
14
|
+
from dnakit.exceptions import (
|
|
15
|
+
BackendExecutionError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
UnsupportedGapOperationError,
|
|
18
|
+
)
|
|
19
|
+
from dnakit.patterns import (
|
|
20
|
+
RestrictionEnzyme,
|
|
21
|
+
scan_codon_sites,
|
|
22
|
+
scan_motif,
|
|
23
|
+
scan_restriction_sites,
|
|
24
|
+
)
|
|
25
|
+
from dnakit.thermodynamics import (
|
|
26
|
+
Primer3ThermodynamicResult,
|
|
27
|
+
ThermodynamicConditions,
|
|
28
|
+
nearest_neighbor,
|
|
29
|
+
validate_primer3_result,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ._shared import SequenceInput, sequence_and_id
|
|
33
|
+
from .kmer import kmer as kmer_fingerprint
|
|
34
|
+
|
|
35
|
+
AdvancedVector = tuple[float, ...]
|
|
36
|
+
PreprocessMode = Literal["none", "standard", "minmax", "l1", "l2"]
|
|
37
|
+
ThermodynamicMissingStrategy = Literal["zero", "sentinel", "error"]
|
|
38
|
+
DEFAULT_MAX_PREPROCESS_ROWS = 1_000_000
|
|
39
|
+
DEFAULT_MAX_MULTISCALE_LEVELS = 16
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ThermodynamicStructureAdapter(Protocol):
|
|
43
|
+
"""Explicit structure adapter used by the thermodynamic fingerprint."""
|
|
44
|
+
|
|
45
|
+
def hairpin(
|
|
46
|
+
self,
|
|
47
|
+
sequence: DNASequence,
|
|
48
|
+
*,
|
|
49
|
+
conditions: ThermodynamicConditions | None = None,
|
|
50
|
+
max_loop: int = 30,
|
|
51
|
+
output_structure: bool = False,
|
|
52
|
+
) -> Primer3ThermodynamicResult: ...
|
|
53
|
+
|
|
54
|
+
def self_dimer(
|
|
55
|
+
self,
|
|
56
|
+
sequence: DNASequence,
|
|
57
|
+
*,
|
|
58
|
+
conditions: ThermodynamicConditions | None = None,
|
|
59
|
+
max_loop: int = 30,
|
|
60
|
+
output_structure: bool = False,
|
|
61
|
+
) -> Primer3ThermodynamicResult: ...
|
|
62
|
+
|
|
63
|
+
def heterodimer(
|
|
64
|
+
self,
|
|
65
|
+
sequence_a: DNASequence,
|
|
66
|
+
sequence_b: DNASequence,
|
|
67
|
+
*,
|
|
68
|
+
conditions: ThermodynamicConditions | None = None,
|
|
69
|
+
max_loop: int = 30,
|
|
70
|
+
output_structure: bool = False,
|
|
71
|
+
) -> Primer3ThermodynamicResult: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class AdvancedFingerprintResult:
|
|
76
|
+
"""One fixed-order, JSON-compatible interpretable feature vector."""
|
|
77
|
+
|
|
78
|
+
name: str
|
|
79
|
+
method: str
|
|
80
|
+
schema_version: str
|
|
81
|
+
sequence_id: str | None
|
|
82
|
+
feature_names: tuple[str, ...]
|
|
83
|
+
values: AdvancedVector
|
|
84
|
+
parameters: FrozenDict
|
|
85
|
+
algorithm_version: str
|
|
86
|
+
provenance: Provenance
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
for name in ("name", "method", "schema_version", "algorithm_version"):
|
|
90
|
+
if not isinstance(getattr(self, name), str) or not getattr(self, name).strip():
|
|
91
|
+
raise ConfigurationError(f"Advanced fingerprint {name} must be non-empty.")
|
|
92
|
+
if len(self.feature_names) != len(self.values) or len(set(self.feature_names)) != len(
|
|
93
|
+
self.feature_names
|
|
94
|
+
):
|
|
95
|
+
raise ConfigurationError("Advanced fingerprint schema and values do not align.")
|
|
96
|
+
if any(not isinstance(name, str) or not name for name in self.feature_names):
|
|
97
|
+
raise ConfigurationError("Advanced fingerprint names must be non-empty strings.")
|
|
98
|
+
if any(
|
|
99
|
+
isinstance(value, bool)
|
|
100
|
+
or not isinstance(value, (int, float))
|
|
101
|
+
or not math.isfinite(value)
|
|
102
|
+
for value in self.values
|
|
103
|
+
):
|
|
104
|
+
raise ConfigurationError("Advanced fingerprint values must be finite numbers.")
|
|
105
|
+
if self.sequence_id is not None and (
|
|
106
|
+
not isinstance(self.sequence_id, str) or not self.sequence_id.strip()
|
|
107
|
+
):
|
|
108
|
+
raise ConfigurationError("Advanced fingerprint sequence_id must be non-empty or None.")
|
|
109
|
+
if not isinstance(self.parameters, FrozenDict):
|
|
110
|
+
raise ConfigurationError("Advanced fingerprint parameters must be FrozenDict.")
|
|
111
|
+
if not isinstance(self.provenance, Provenance):
|
|
112
|
+
raise ConfigurationError("Advanced fingerprint provenance must be Provenance.")
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict[str, object]:
|
|
115
|
+
return cast(dict[str, object], to_json_compatible(self))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class FeaturePreprocessor:
|
|
120
|
+
"""Training-set-fitted deterministic column transform."""
|
|
121
|
+
|
|
122
|
+
feature_names: tuple[str, ...]
|
|
123
|
+
mode: PreprocessMode
|
|
124
|
+
centers: tuple[float, ...]
|
|
125
|
+
scales: tuple[float, ...]
|
|
126
|
+
imputation_values: tuple[float, ...]
|
|
127
|
+
kept_indices: tuple[int, ...]
|
|
128
|
+
variance_threshold: float
|
|
129
|
+
missing_strategy: Literal["error", "mean", "zero"]
|
|
130
|
+
max_rows: int = DEFAULT_MAX_PREPROCESS_ROWS
|
|
131
|
+
schema_version: str = "dnakit.feature-preprocessor.v2"
|
|
132
|
+
|
|
133
|
+
def __post_init__(self) -> None:
|
|
134
|
+
width = len(self.feature_names)
|
|
135
|
+
if not self.feature_names or any(
|
|
136
|
+
not isinstance(name, str) or not name.strip() for name in self.feature_names
|
|
137
|
+
):
|
|
138
|
+
raise ConfigurationError("Preprocessor feature names must be non-empty strings.")
|
|
139
|
+
if len(set(self.feature_names)) != width:
|
|
140
|
+
raise ConfigurationError("Preprocessor feature names must be unique.")
|
|
141
|
+
if self.mode not in {"none", "standard", "minmax", "l1", "l2"}:
|
|
142
|
+
raise ConfigurationError("Unknown preprocessing mode.")
|
|
143
|
+
if self.missing_strategy not in {"error", "mean", "zero"}:
|
|
144
|
+
raise ConfigurationError("Unknown missing strategy.")
|
|
145
|
+
if not all(
|
|
146
|
+
isinstance(values, tuple) and len(values) == width
|
|
147
|
+
for values in (self.centers, self.scales, self.imputation_values)
|
|
148
|
+
):
|
|
149
|
+
raise ConfigurationError("Preprocessor numeric vectors must match its feature schema.")
|
|
150
|
+
if any(
|
|
151
|
+
isinstance(value, bool)
|
|
152
|
+
or not isinstance(value, (int, float))
|
|
153
|
+
or not math.isfinite(value)
|
|
154
|
+
for values in (self.centers, self.scales, self.imputation_values)
|
|
155
|
+
for value in values
|
|
156
|
+
):
|
|
157
|
+
raise ConfigurationError("Preprocessor numeric vectors must contain finite numbers.")
|
|
158
|
+
if any(value <= 0 for value in self.scales):
|
|
159
|
+
raise ConfigurationError("Preprocessor scales must be positive.")
|
|
160
|
+
if (
|
|
161
|
+
any(
|
|
162
|
+
isinstance(index, bool) or not isinstance(index, int) or not 0 <= index < width
|
|
163
|
+
for index in self.kept_indices
|
|
164
|
+
)
|
|
165
|
+
or tuple(sorted(set(self.kept_indices))) != self.kept_indices
|
|
166
|
+
):
|
|
167
|
+
raise ConfigurationError("Preprocessor kept_indices must be sorted unique indices.")
|
|
168
|
+
if (
|
|
169
|
+
isinstance(self.variance_threshold, bool)
|
|
170
|
+
or not isinstance(self.variance_threshold, (int, float))
|
|
171
|
+
or not math.isfinite(self.variance_threshold)
|
|
172
|
+
or self.variance_threshold < 0
|
|
173
|
+
):
|
|
174
|
+
raise ConfigurationError("variance_threshold must be finite and non-negative.")
|
|
175
|
+
if (
|
|
176
|
+
isinstance(self.max_rows, bool)
|
|
177
|
+
or not isinstance(self.max_rows, int)
|
|
178
|
+
or not 0 < self.max_rows <= DEFAULT_MAX_PREPROCESS_ROWS
|
|
179
|
+
):
|
|
180
|
+
raise ConfigurationError(f"max_rows must be in [1, {DEFAULT_MAX_PREPROCESS_ROWS}].")
|
|
181
|
+
if self.schema_version != "dnakit.feature-preprocessor.v2":
|
|
182
|
+
raise ConfigurationError("Unsupported feature-preprocessor schema version.")
|
|
183
|
+
|
|
184
|
+
def transform(self, rows: Iterable[Iterable[int | float | None]]) -> tuple[AdvancedVector, ...]:
|
|
185
|
+
return _transform_rows(rows, self)
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> dict[str, object]:
|
|
188
|
+
return cast(dict[str, object], to_json_compatible(self))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _result(
|
|
192
|
+
value: SequenceInput,
|
|
193
|
+
*,
|
|
194
|
+
name: str,
|
|
195
|
+
method: str,
|
|
196
|
+
feature_names: Sequence[str],
|
|
197
|
+
values: Sequence[int | float],
|
|
198
|
+
parameters: Mapping[str, object],
|
|
199
|
+
schema_version: str | None = None,
|
|
200
|
+
provenance: Provenance | None = None,
|
|
201
|
+
) -> AdvancedFingerprintResult:
|
|
202
|
+
_, sequence_id = sequence_and_id(value)
|
|
203
|
+
return AdvancedFingerprintResult(
|
|
204
|
+
name,
|
|
205
|
+
method,
|
|
206
|
+
schema_version or f"dnakit.{name}.v1",
|
|
207
|
+
sequence_id,
|
|
208
|
+
tuple(feature_names),
|
|
209
|
+
tuple(float(item) for item in values),
|
|
210
|
+
FrozenDict(parameters),
|
|
211
|
+
"1.0",
|
|
212
|
+
provenance or Provenance(),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def motif_fingerprint(
|
|
217
|
+
value: SequenceInput,
|
|
218
|
+
motifs: Mapping[str, str],
|
|
219
|
+
*,
|
|
220
|
+
binary: bool = False,
|
|
221
|
+
) -> AdvancedFingerprintResult:
|
|
222
|
+
"""Encode ordered exact/IUPAC motif counts or presence."""
|
|
223
|
+
|
|
224
|
+
if not isinstance(motifs, Mapping) or not motifs:
|
|
225
|
+
raise ConfigurationError("motifs must be a non-empty ordered mapping.")
|
|
226
|
+
if any(not isinstance(name, str) or not name.strip() for name in motifs):
|
|
227
|
+
raise ConfigurationError("motif names must be non-empty strings.")
|
|
228
|
+
if not isinstance(binary, bool):
|
|
229
|
+
raise ConfigurationError("binary must be a boolean.")
|
|
230
|
+
names = tuple(sorted(motifs))
|
|
231
|
+
results = [
|
|
232
|
+
scan_motif(value, motifs[name], mode="iupac", name=name, merge_strands=True)
|
|
233
|
+
for name in names
|
|
234
|
+
]
|
|
235
|
+
counts = tuple(len(result.hits) for result in results)
|
|
236
|
+
return _result(
|
|
237
|
+
value,
|
|
238
|
+
name="motif_fingerprint",
|
|
239
|
+
method="iupac-motif-presence" if binary else "iupac-motif-count",
|
|
240
|
+
feature_names=tuple(f"motif:{name}" for name in names),
|
|
241
|
+
values=tuple(int(count > 0) for count in counts) if binary else counts,
|
|
242
|
+
parameters={"motifs": {name: motifs[name] for name in names}, "binary": binary},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def restriction_fingerprint(
|
|
247
|
+
value: SequenceInput,
|
|
248
|
+
enzymes: Iterable[str | RestrictionEnzyme],
|
|
249
|
+
*,
|
|
250
|
+
binary: bool = False,
|
|
251
|
+
max_enzymes: int = 10_000,
|
|
252
|
+
) -> AdvancedFingerprintResult:
|
|
253
|
+
"""Encode recognition-site counts for a versioned/user-defined panel."""
|
|
254
|
+
|
|
255
|
+
if isinstance(enzymes, (str, bytes)):
|
|
256
|
+
raise ConfigurationError("enzymes must be an iterable of enzyme definitions.")
|
|
257
|
+
if not isinstance(binary, bool):
|
|
258
|
+
raise ConfigurationError("binary must be a boolean.")
|
|
259
|
+
scan = scan_restriction_sites(value, enzymes, max_enzymes=max_enzymes)
|
|
260
|
+
requested = scan.parameters.get("enzymes")
|
|
261
|
+
if not isinstance(requested, tuple):
|
|
262
|
+
raise AssertionError("Restriction scan must audit its resolved enzyme panel.")
|
|
263
|
+
resolved: list[str] = []
|
|
264
|
+
for item in requested:
|
|
265
|
+
if not isinstance(item, Mapping):
|
|
266
|
+
raise AssertionError("Restriction scan enzyme audit entries must be mappings.")
|
|
267
|
+
name = item.get("name")
|
|
268
|
+
if not isinstance(name, str):
|
|
269
|
+
raise AssertionError("Restriction scan enzyme audit entries must contain names.")
|
|
270
|
+
resolved.append(name)
|
|
271
|
+
resolved_names = tuple(resolved)
|
|
272
|
+
counts = {name: 0 for name in resolved_names}
|
|
273
|
+
for hit in scan.hits:
|
|
274
|
+
counts[hit.enzyme] += 1
|
|
275
|
+
return _result(
|
|
276
|
+
value,
|
|
277
|
+
name="restriction_fingerprint",
|
|
278
|
+
method="restriction-site-presence" if binary else "restriction-site-count",
|
|
279
|
+
feature_names=tuple(f"restriction:{name}" for name in resolved_names),
|
|
280
|
+
values=tuple(int(counts[name] > 0) if binary else counts[name] for name in resolved_names),
|
|
281
|
+
parameters={"enzymes": resolved_names, "binary": binary, "max_enzymes": max_enzymes},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def gc_spatial_fingerprint(
|
|
286
|
+
value: SequenceInput,
|
|
287
|
+
*,
|
|
288
|
+
bins: int = 10,
|
|
289
|
+
) -> AdvancedFingerprintResult:
|
|
290
|
+
"""Pool canonical GC fraction over a fixed number of normalized-position bins."""
|
|
291
|
+
|
|
292
|
+
if isinstance(bins, bool) or not isinstance(bins, int) or bins <= 0 or bins > 10_000:
|
|
293
|
+
raise ConfigurationError("bins must be an integer in [1, 10000].")
|
|
294
|
+
sequence, _ = sequence_and_id(value)
|
|
295
|
+
if sequence.is_gapped:
|
|
296
|
+
raise UnsupportedGapOperationError(
|
|
297
|
+
"Spatial fingerprints cannot silently omit explicit Gap objects.",
|
|
298
|
+
code="SPATIAL_FINGERPRINT_GAP_NOT_ALLOWED",
|
|
299
|
+
)
|
|
300
|
+
text = sequence.symbols
|
|
301
|
+
fractions: list[float] = []
|
|
302
|
+
for index in range(bins):
|
|
303
|
+
start = len(text) * index // bins
|
|
304
|
+
end = len(text) * (index + 1) // bins
|
|
305
|
+
window = text[start:end]
|
|
306
|
+
canonical = tuple(symbol for symbol in window if symbol in "ACGT")
|
|
307
|
+
fractions.append(
|
|
308
|
+
sum(symbol in "GC" for symbol in canonical) / len(canonical) if canonical else 0.0
|
|
309
|
+
)
|
|
310
|
+
return _result(
|
|
311
|
+
value,
|
|
312
|
+
name="gc_spatial_fingerprint",
|
|
313
|
+
method="equal-symbol-bin-gc-fraction",
|
|
314
|
+
feature_names=tuple(f"gc_bin:{index}" for index in range(bins)),
|
|
315
|
+
values=fractions,
|
|
316
|
+
parameters={
|
|
317
|
+
"bins": bins,
|
|
318
|
+
"empty_bin_value": 0.0,
|
|
319
|
+
"ambiguity_policy": "ignore",
|
|
320
|
+
"topology": sequence.topology.value,
|
|
321
|
+
"circular_origin": "stored-origin" if sequence.topology is Topology.CIRCULAR else None,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def repeat_fingerprint(value: SequenceInput) -> AdvancedFingerprintResult:
|
|
327
|
+
"""Encode homopolymer/tandem-repeat summary values."""
|
|
328
|
+
|
|
329
|
+
repeat = exact_repeat_fraction(value)
|
|
330
|
+
from dnakit.descriptors import homopolymer_runs
|
|
331
|
+
|
|
332
|
+
homopolymer = homopolymer_runs(value, ambiguity_policy="ignore")
|
|
333
|
+
return _result(
|
|
334
|
+
value,
|
|
335
|
+
name="repeat_fingerprint",
|
|
336
|
+
method="interpretable-exact-repeat-summary",
|
|
337
|
+
feature_names=(
|
|
338
|
+
"repeat_fraction",
|
|
339
|
+
"repeat_run_count",
|
|
340
|
+
"longest_homopolymer",
|
|
341
|
+
"homopolymer_run_count",
|
|
342
|
+
),
|
|
343
|
+
values=(
|
|
344
|
+
repeat.repeat_fraction,
|
|
345
|
+
len(repeat.runs),
|
|
346
|
+
homopolymer.longest_length,
|
|
347
|
+
len(homopolymer.runs),
|
|
348
|
+
),
|
|
349
|
+
parameters={"repeat_method": repeat.method, "homopolymer_method": homopolymer.method},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def coding_fingerprint(
|
|
354
|
+
value: SequenceInput,
|
|
355
|
+
*,
|
|
356
|
+
frame: int = 0,
|
|
357
|
+
genetic_code: int = 1,
|
|
358
|
+
) -> AdvancedFingerprintResult:
|
|
359
|
+
"""Encode codon/start/stop/ORF summaries without activity prediction."""
|
|
360
|
+
|
|
361
|
+
codons = codon_statistics(value, frame=frame, genetic_code=genetic_code)
|
|
362
|
+
sites = scan_codon_sites(value, genetic_code=genetic_code)
|
|
363
|
+
return _result(
|
|
364
|
+
value,
|
|
365
|
+
name="coding_fingerprint",
|
|
366
|
+
method="codon-and-site-summary",
|
|
367
|
+
feature_names=("codon_count", "start_count", "stop_count", "six_frame_site_count"),
|
|
368
|
+
values=(codons.codon_count, codons.start_count, codons.stop_count, len(sites.hits)),
|
|
369
|
+
parameters={"frame": frame, "genetic_code": genetic_code},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def thermodynamic_fingerprint(
|
|
374
|
+
value: SequenceInput,
|
|
375
|
+
*,
|
|
376
|
+
conditions: ThermodynamicConditions | None = None,
|
|
377
|
+
structure_adapter: ThermodynamicStructureAdapter | None = None,
|
|
378
|
+
paired_value: SequenceInput | None = None,
|
|
379
|
+
missing_strategy: ThermodynamicMissingStrategy = "zero",
|
|
380
|
+
missing_value: float = -999.0,
|
|
381
|
+
max_loop: int = 30,
|
|
382
|
+
) -> AdvancedFingerprintResult:
|
|
383
|
+
"""Encode a fixed v2 duplex/hairpin/dimer schema with explicit missing data."""
|
|
384
|
+
|
|
385
|
+
sequence, _ = sequence_and_id(value)
|
|
386
|
+
paired_sequence = None if paired_value is None else sequence_and_id(paired_value)[0]
|
|
387
|
+
if missing_strategy not in {"zero", "sentinel", "error"}:
|
|
388
|
+
raise ConfigurationError("Unknown thermodynamic fingerprint missing strategy.")
|
|
389
|
+
if (
|
|
390
|
+
isinstance(missing_value, bool)
|
|
391
|
+
or not isinstance(missing_value, (int, float))
|
|
392
|
+
or not math.isfinite(missing_value)
|
|
393
|
+
):
|
|
394
|
+
raise ConfigurationError("missing_value must be finite numeric data.")
|
|
395
|
+
if isinstance(max_loop, bool) or not isinstance(max_loop, int) or not 1 <= max_loop <= 30:
|
|
396
|
+
raise ConfigurationError("max_loop must be an integer in [1, 30].")
|
|
397
|
+
if structure_adapter is not None and any(
|
|
398
|
+
not callable(getattr(structure_adapter, name, None))
|
|
399
|
+
for name in ("hairpin", "self_dimer", "heterodimer")
|
|
400
|
+
):
|
|
401
|
+
raise ConfigurationError(
|
|
402
|
+
"structure_adapter must implement hairpin, self_dimer, and heterodimer.",
|
|
403
|
+
code="INVALID_THERMODYNAMIC_STRUCTURE_ADAPTER",
|
|
404
|
+
)
|
|
405
|
+
nearest = nearest_neighbor(sequence, conditions=conditions)
|
|
406
|
+
fill = 0.0 if missing_strategy == "zero" else float(missing_value)
|
|
407
|
+
if missing_strategy == "error" and structure_adapter is None:
|
|
408
|
+
raise ConfigurationError(
|
|
409
|
+
"The hairpin fingerprint feature is unavailable under missing_strategy=error.",
|
|
410
|
+
code="THERMODYNAMIC_FINGERPRINT_MISSING_FEATURE",
|
|
411
|
+
context={"capability": "hairpin"},
|
|
412
|
+
)
|
|
413
|
+
if missing_strategy == "error" and paired_sequence is None:
|
|
414
|
+
raise ConfigurationError(
|
|
415
|
+
"The heterodimer fingerprint feature is unavailable under missing_strategy=error.",
|
|
416
|
+
code="THERMODYNAMIC_FINGERPRINT_MISSING_FEATURE",
|
|
417
|
+
context={"capability": "heterodimer"},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def missing_features(capability: str) -> tuple[float, float, float, float]:
|
|
421
|
+
if missing_strategy == "error":
|
|
422
|
+
raise ConfigurationError(
|
|
423
|
+
f"The {capability} fingerprint feature is unavailable under "
|
|
424
|
+
"missing_strategy=error.",
|
|
425
|
+
code="THERMODYNAMIC_FINGERPRINT_MISSING_FEATURE",
|
|
426
|
+
context={"capability": capability},
|
|
427
|
+
)
|
|
428
|
+
return (0.0, fill, fill, fill)
|
|
429
|
+
|
|
430
|
+
validated_structure_results: list[Primer3ThermodynamicResult] = []
|
|
431
|
+
|
|
432
|
+
def structure_features(
|
|
433
|
+
result: object,
|
|
434
|
+
*,
|
|
435
|
+
capability: str,
|
|
436
|
+
expected_sequences: tuple[str, ...],
|
|
437
|
+
) -> tuple[float, float, float, float]:
|
|
438
|
+
validated = validate_primer3_result(
|
|
439
|
+
result,
|
|
440
|
+
capability=capability,
|
|
441
|
+
sequences_5to3=expected_sequences,
|
|
442
|
+
conditions=nearest.conditions,
|
|
443
|
+
max_loop=max_loop,
|
|
444
|
+
output_structure=False,
|
|
445
|
+
error_code="MISMATCHED_THERMODYNAMIC_FINGERPRINT_RESULT",
|
|
446
|
+
)
|
|
447
|
+
if validated.structure_found is None or validated.delta_g_kcal_per_mol is None:
|
|
448
|
+
raise BackendExecutionError(
|
|
449
|
+
"Structure adapter omitted required fingerprint values.",
|
|
450
|
+
code="INVALID_THERMODYNAMIC_FINGERPRINT_RESULT",
|
|
451
|
+
context={"capability": capability},
|
|
452
|
+
)
|
|
453
|
+
validated_structure_results.append(validated)
|
|
454
|
+
return (
|
|
455
|
+
1.0,
|
|
456
|
+
float(validated.structure_found),
|
|
457
|
+
validated.tm_celsius,
|
|
458
|
+
validated.delta_g_kcal_per_mol,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
adapter_provenance: Provenance | None = None
|
|
462
|
+
backend_name: str | None = None
|
|
463
|
+
backend_version: str | None = None
|
|
464
|
+
if structure_adapter is None:
|
|
465
|
+
hairpin_values = missing_features("hairpin")
|
|
466
|
+
self_dimer_values = missing_features("self_dimer")
|
|
467
|
+
heterodimer_values = missing_features("heterodimer")
|
|
468
|
+
else:
|
|
469
|
+
hairpin_result = structure_adapter.hairpin(
|
|
470
|
+
sequence,
|
|
471
|
+
conditions=conditions,
|
|
472
|
+
max_loop=max_loop,
|
|
473
|
+
output_structure=False,
|
|
474
|
+
)
|
|
475
|
+
self_dimer_result = structure_adapter.self_dimer(
|
|
476
|
+
sequence,
|
|
477
|
+
conditions=conditions,
|
|
478
|
+
max_loop=max_loop,
|
|
479
|
+
output_structure=False,
|
|
480
|
+
)
|
|
481
|
+
hairpin_values = structure_features(
|
|
482
|
+
hairpin_result,
|
|
483
|
+
capability="hairpin",
|
|
484
|
+
expected_sequences=(sequence.symbols,),
|
|
485
|
+
)
|
|
486
|
+
self_dimer_values = structure_features(
|
|
487
|
+
self_dimer_result,
|
|
488
|
+
capability="self_dimer",
|
|
489
|
+
expected_sequences=(sequence.symbols,),
|
|
490
|
+
)
|
|
491
|
+
if paired_sequence is None:
|
|
492
|
+
heterodimer_values = missing_features("heterodimer")
|
|
493
|
+
else:
|
|
494
|
+
heterodimer_values = structure_features(
|
|
495
|
+
structure_adapter.heterodimer(
|
|
496
|
+
sequence,
|
|
497
|
+
paired_sequence,
|
|
498
|
+
conditions=conditions,
|
|
499
|
+
max_loop=max_loop,
|
|
500
|
+
output_structure=False,
|
|
501
|
+
),
|
|
502
|
+
capability="heterodimer",
|
|
503
|
+
expected_sequences=(sequence.symbols, paired_sequence.symbols),
|
|
504
|
+
)
|
|
505
|
+
adapter_provenance = hairpin_result.provenance
|
|
506
|
+
backend_name = hairpin_result.backend.name
|
|
507
|
+
backend_version = hairpin_result.backend.version
|
|
508
|
+
if any(
|
|
509
|
+
result.backend != hairpin_result.backend
|
|
510
|
+
or result.provenance != hairpin_result.provenance
|
|
511
|
+
for result in validated_structure_results[1:]
|
|
512
|
+
):
|
|
513
|
+
raise BackendExecutionError(
|
|
514
|
+
"Structure adapter returned inconsistent backend or provenance metadata.",
|
|
515
|
+
code="INCONSISTENT_THERMODYNAMIC_FINGERPRINT_RESULTS",
|
|
516
|
+
)
|
|
517
|
+
return _result(
|
|
518
|
+
value,
|
|
519
|
+
name="thermodynamic_fingerprint",
|
|
520
|
+
method="santalucia1998-duplex-with-explicit-primer3-structure-features",
|
|
521
|
+
feature_names=(
|
|
522
|
+
"tm_celsius",
|
|
523
|
+
"delta_h",
|
|
524
|
+
"delta_s",
|
|
525
|
+
"delta_g",
|
|
526
|
+
"hairpin_available",
|
|
527
|
+
"hairpin_found",
|
|
528
|
+
"hairpin_tm_celsius",
|
|
529
|
+
"hairpin_delta_g",
|
|
530
|
+
"self_dimer_available",
|
|
531
|
+
"self_dimer_found",
|
|
532
|
+
"self_dimer_tm_celsius",
|
|
533
|
+
"self_dimer_delta_g",
|
|
534
|
+
"heterodimer_available",
|
|
535
|
+
"heterodimer_found",
|
|
536
|
+
"heterodimer_tm_celsius",
|
|
537
|
+
"heterodimer_delta_g",
|
|
538
|
+
),
|
|
539
|
+
values=(
|
|
540
|
+
nearest.tm_celsius,
|
|
541
|
+
nearest.delta_h_kcal_per_mol,
|
|
542
|
+
nearest.delta_s_cal_per_k_mol,
|
|
543
|
+
nearest.delta_g_kcal_per_mol,
|
|
544
|
+
*hairpin_values,
|
|
545
|
+
*self_dimer_values,
|
|
546
|
+
*heterodimer_values,
|
|
547
|
+
),
|
|
548
|
+
parameters={
|
|
549
|
+
"conditions": to_json_compatible(nearest.conditions),
|
|
550
|
+
"parameter_set": nearest.parameter_set,
|
|
551
|
+
"hairpin_dimer_included": structure_adapter is not None,
|
|
552
|
+
"heterodimer_included": structure_adapter is not None and paired_sequence is not None,
|
|
553
|
+
"structure_adapter_supplied": structure_adapter is not None,
|
|
554
|
+
"automatic_backend_probe": False,
|
|
555
|
+
"automatic_backend_execution": False,
|
|
556
|
+
"missing_strategy": missing_strategy,
|
|
557
|
+
"missing_value": fill,
|
|
558
|
+
"paired_sequence_supplied": paired_sequence is not None,
|
|
559
|
+
"max_loop": max_loop,
|
|
560
|
+
"backend_name": backend_name,
|
|
561
|
+
"backend_version": backend_version,
|
|
562
|
+
},
|
|
563
|
+
schema_version="dnakit.thermodynamic_fingerprint.v2",
|
|
564
|
+
provenance=adapter_provenance,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def multiscale_fingerprint(
|
|
569
|
+
value: SequenceInput,
|
|
570
|
+
*,
|
|
571
|
+
k_values: Iterable[int] = (1, 2, 3),
|
|
572
|
+
) -> AdvancedFingerprintResult:
|
|
573
|
+
"""Concatenate frequency k-mer vectors at explicit scales plus global GC."""
|
|
574
|
+
|
|
575
|
+
if isinstance(k_values, (str, bytes)):
|
|
576
|
+
raise ConfigurationError("k_values must be an iterable of positive integers.")
|
|
577
|
+
scales_list: list[int] = []
|
|
578
|
+
try:
|
|
579
|
+
iterator = iter(k_values)
|
|
580
|
+
except TypeError as exc:
|
|
581
|
+
raise ConfigurationError("k_values must be an iterable of positive integers.") from exc
|
|
582
|
+
for index, scale in enumerate(iterator):
|
|
583
|
+
if index >= DEFAULT_MAX_MULTISCALE_LEVELS:
|
|
584
|
+
raise ConfigurationError(
|
|
585
|
+
"k_values exceeds the hard multiscale-level limit.",
|
|
586
|
+
code="MULTISCALE_LEVEL_LIMIT",
|
|
587
|
+
)
|
|
588
|
+
scales_list.append(scale)
|
|
589
|
+
scales = tuple(scales_list)
|
|
590
|
+
if not scales or any(isinstance(k, bool) or not isinstance(k, int) or k <= 0 for k in scales):
|
|
591
|
+
raise ConfigurationError("k_values must contain positive integers.")
|
|
592
|
+
if len(scales) != len(set(scales)):
|
|
593
|
+
raise ConfigurationError("k_values must be unique.")
|
|
594
|
+
names: list[str] = []
|
|
595
|
+
values: list[float] = []
|
|
596
|
+
for k in scales:
|
|
597
|
+
result = kmer_fingerprint(value, k=k, mode="frequency")
|
|
598
|
+
names.extend(f"k{k}:{name}" for name in result.feature_names)
|
|
599
|
+
values.extend(float(item) for item in result.dense_values())
|
|
600
|
+
gc = gc_at_content(value, ambiguity_policy="ignore")
|
|
601
|
+
names.append("global_gc")
|
|
602
|
+
values.append(0.0 if gc.gc_fraction is None else gc.gc_fraction)
|
|
603
|
+
return _result(
|
|
604
|
+
value,
|
|
605
|
+
name="multiscale_fingerprint",
|
|
606
|
+
method="concatenated-kmer-frequency-and-global-gc",
|
|
607
|
+
feature_names=names,
|
|
608
|
+
values=values,
|
|
609
|
+
parameters={"k_values": scales},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def hybrid_fingerprint(
|
|
614
|
+
components: Mapping[str, AdvancedFingerprintResult],
|
|
615
|
+
*,
|
|
616
|
+
weights: Mapping[str, int | float] | None = None,
|
|
617
|
+
) -> AdvancedFingerprintResult:
|
|
618
|
+
"""Concatenate named compatible component results with optional scalar weights."""
|
|
619
|
+
|
|
620
|
+
if not isinstance(components, Mapping) or not components:
|
|
621
|
+
raise ConfigurationError("components must be a non-empty mapping.")
|
|
622
|
+
if any(not isinstance(name, str) or not name.strip() for name in components):
|
|
623
|
+
raise ConfigurationError("Component names must be non-empty strings.")
|
|
624
|
+
if any(not isinstance(value, AdvancedFingerprintResult) for value in components.values()):
|
|
625
|
+
raise ConfigurationError("Component values must be AdvancedFingerprintResult objects.")
|
|
626
|
+
if weights is not None and not isinstance(weights, Mapping):
|
|
627
|
+
raise ConfigurationError("weights must be a mapping or None.")
|
|
628
|
+
ordered = tuple(sorted(components))
|
|
629
|
+
resolved_weights = {name: 1.0 for name in ordered}
|
|
630
|
+
for name, raw in (weights or {}).items():
|
|
631
|
+
if name not in resolved_weights:
|
|
632
|
+
raise ConfigurationError("Weight names must match component names.")
|
|
633
|
+
if (
|
|
634
|
+
isinstance(raw, bool)
|
|
635
|
+
or not isinstance(raw, (int, float))
|
|
636
|
+
or not math.isfinite(raw)
|
|
637
|
+
or raw < 0
|
|
638
|
+
):
|
|
639
|
+
raise ConfigurationError("Component weights must be finite non-negative numbers.")
|
|
640
|
+
resolved_weights[name] = float(raw)
|
|
641
|
+
sequence_ids = {components[name].sequence_id for name in ordered}
|
|
642
|
+
if len(sequence_ids) > 1:
|
|
643
|
+
raise ConfigurationError("Hybrid components must refer to the same sequence ID.")
|
|
644
|
+
names = tuple(
|
|
645
|
+
f"{component}:{feature}"
|
|
646
|
+
for component in ordered
|
|
647
|
+
for feature in components[component].feature_names
|
|
648
|
+
)
|
|
649
|
+
values = tuple(
|
|
650
|
+
value * resolved_weights[component]
|
|
651
|
+
for component in ordered
|
|
652
|
+
for value in components[component].values
|
|
653
|
+
)
|
|
654
|
+
return AdvancedFingerprintResult(
|
|
655
|
+
"hybrid_fingerprint",
|
|
656
|
+
"weighted-component-concatenation",
|
|
657
|
+
"dnakit.hybrid_fingerprint.v1",
|
|
658
|
+
next(iter(sequence_ids)),
|
|
659
|
+
names,
|
|
660
|
+
values,
|
|
661
|
+
FrozenDict(
|
|
662
|
+
{
|
|
663
|
+
"components": {name: components[name].schema_version for name in ordered},
|
|
664
|
+
"weights": resolved_weights,
|
|
665
|
+
}
|
|
666
|
+
),
|
|
667
|
+
"1.0",
|
|
668
|
+
Provenance(),
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _materialize_rows(
|
|
673
|
+
rows: Iterable[Iterable[int | float | None]],
|
|
674
|
+
*,
|
|
675
|
+
max_rows: int,
|
|
676
|
+
width: int,
|
|
677
|
+
) -> tuple[tuple[float | None, ...], ...]:
|
|
678
|
+
output: list[tuple[float | None, ...]] = []
|
|
679
|
+
for index, row in enumerate(rows):
|
|
680
|
+
if index >= max_rows:
|
|
681
|
+
raise ConfigurationError("Feature rows exceed max_rows.")
|
|
682
|
+
try:
|
|
683
|
+
values = tuple(islice(iter(row), width + 1))
|
|
684
|
+
except TypeError as exc:
|
|
685
|
+
raise ConfigurationError("Each feature row must be an iterable.") from exc
|
|
686
|
+
if len(values) != width:
|
|
687
|
+
raise ConfigurationError("Feature row width does not match the fitted schema.")
|
|
688
|
+
for value in values:
|
|
689
|
+
if value is not None and (
|
|
690
|
+
isinstance(value, bool)
|
|
691
|
+
or not isinstance(value, (int, float))
|
|
692
|
+
or not math.isfinite(value)
|
|
693
|
+
):
|
|
694
|
+
raise ConfigurationError("Feature values must be finite numbers or None.")
|
|
695
|
+
output.append(tuple(None if item is None else float(item) for item in values))
|
|
696
|
+
if not output:
|
|
697
|
+
raise ConfigurationError("Feature matrix must contain at least one row.")
|
|
698
|
+
return tuple(output)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def fit_preprocessor(
|
|
702
|
+
rows: Iterable[Iterable[int | float | None]],
|
|
703
|
+
*,
|
|
704
|
+
feature_names: Sequence[str],
|
|
705
|
+
mode: PreprocessMode = "standard",
|
|
706
|
+
missing_strategy: Literal["error", "mean", "zero"] = "error",
|
|
707
|
+
variance_threshold: float = 0.0,
|
|
708
|
+
max_rows: int = DEFAULT_MAX_PREPROCESS_ROWS,
|
|
709
|
+
) -> FeaturePreprocessor:
|
|
710
|
+
"""Fit imputation/scaling/low-variance parameters on training rows only."""
|
|
711
|
+
|
|
712
|
+
if isinstance(feature_names, (str, bytes)):
|
|
713
|
+
raise ConfigurationError("feature_names must be a sequence of names, not text.")
|
|
714
|
+
names = tuple(feature_names)
|
|
715
|
+
if (
|
|
716
|
+
not names
|
|
717
|
+
or any(not isinstance(name, str) or not name.strip() for name in names)
|
|
718
|
+
or len(set(names)) != len(names)
|
|
719
|
+
):
|
|
720
|
+
raise ConfigurationError("feature_names must be unique non-empty strings.")
|
|
721
|
+
if mode not in {"none", "standard", "minmax", "l1", "l2"}:
|
|
722
|
+
raise ConfigurationError("Unknown preprocessing mode.")
|
|
723
|
+
if missing_strategy not in {"error", "mean", "zero"}:
|
|
724
|
+
raise ConfigurationError("Unknown missing strategy.")
|
|
725
|
+
if (
|
|
726
|
+
isinstance(variance_threshold, bool)
|
|
727
|
+
or not isinstance(variance_threshold, (int, float))
|
|
728
|
+
or not math.isfinite(variance_threshold)
|
|
729
|
+
or variance_threshold < 0
|
|
730
|
+
):
|
|
731
|
+
raise ConfigurationError("variance_threshold must be finite and non-negative.")
|
|
732
|
+
if (
|
|
733
|
+
isinstance(max_rows, bool)
|
|
734
|
+
or not isinstance(max_rows, int)
|
|
735
|
+
or not 0 < max_rows <= DEFAULT_MAX_PREPROCESS_ROWS
|
|
736
|
+
):
|
|
737
|
+
raise ConfigurationError(f"max_rows must be in [1, {DEFAULT_MAX_PREPROCESS_ROWS}].")
|
|
738
|
+
matrix = _materialize_rows(rows, max_rows=max_rows, width=len(names))
|
|
739
|
+
columns = tuple(tuple(row[index] for row in matrix) for index in range(len(names)))
|
|
740
|
+
centers: list[float] = []
|
|
741
|
+
scales: list[float] = []
|
|
742
|
+
imputations: list[float] = []
|
|
743
|
+
variances: list[float] = []
|
|
744
|
+
for column in columns:
|
|
745
|
+
observed = tuple(value for value in column if value is not None)
|
|
746
|
+
if missing_strategy == "error" and len(observed) != len(column):
|
|
747
|
+
raise ConfigurationError("Missing feature encountered under error policy.")
|
|
748
|
+
center = math.fsum(observed) / len(observed) if observed else 0.0
|
|
749
|
+
imputation = center if missing_strategy == "mean" else 0.0
|
|
750
|
+
imputed = tuple(imputation if value is None else value for value in column)
|
|
751
|
+
mean = math.fsum(imputed) / len(imputed)
|
|
752
|
+
variance = math.fsum((value - mean) ** 2 for value in imputed) / len(imputed)
|
|
753
|
+
if mode == "standard":
|
|
754
|
+
centers.append(mean)
|
|
755
|
+
scales.append(math.sqrt(variance) or 1.0)
|
|
756
|
+
elif mode == "minmax":
|
|
757
|
+
minimum, maximum = min(imputed), max(imputed)
|
|
758
|
+
centers.append(minimum)
|
|
759
|
+
scales.append(maximum - minimum or 1.0)
|
|
760
|
+
else:
|
|
761
|
+
centers.append(0.0)
|
|
762
|
+
scales.append(1.0)
|
|
763
|
+
imputations.append(imputation)
|
|
764
|
+
variances.append(variance)
|
|
765
|
+
kept = tuple(
|
|
766
|
+
index for index, variance in enumerate(variances) if variance >= variance_threshold
|
|
767
|
+
)
|
|
768
|
+
return FeaturePreprocessor(
|
|
769
|
+
names,
|
|
770
|
+
mode,
|
|
771
|
+
tuple(centers),
|
|
772
|
+
tuple(scales),
|
|
773
|
+
tuple(imputations),
|
|
774
|
+
kept,
|
|
775
|
+
float(variance_threshold),
|
|
776
|
+
missing_strategy,
|
|
777
|
+
max_rows,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _transform_rows(
|
|
782
|
+
rows: Iterable[Iterable[int | float | None]], preprocessor: FeaturePreprocessor
|
|
783
|
+
) -> tuple[AdvancedVector, ...]:
|
|
784
|
+
if not isinstance(preprocessor, FeaturePreprocessor):
|
|
785
|
+
raise ConfigurationError("preprocessor must be FeaturePreprocessor.")
|
|
786
|
+
matrix = _materialize_rows(
|
|
787
|
+
rows,
|
|
788
|
+
max_rows=preprocessor.max_rows,
|
|
789
|
+
width=len(preprocessor.feature_names),
|
|
790
|
+
)
|
|
791
|
+
transformed: list[AdvancedVector] = []
|
|
792
|
+
for row in matrix:
|
|
793
|
+
values: list[float] = []
|
|
794
|
+
for index, raw in enumerate(row):
|
|
795
|
+
if raw is None:
|
|
796
|
+
if preprocessor.missing_strategy == "error":
|
|
797
|
+
raise ConfigurationError("Missing feature encountered under error policy.")
|
|
798
|
+
raw = preprocessor.imputation_values[index]
|
|
799
|
+
values.append((raw - preprocessor.centers[index]) / preprocessor.scales[index])
|
|
800
|
+
selected = [values[index] for index in preprocessor.kept_indices]
|
|
801
|
+
if preprocessor.mode in {"l1", "l2"}:
|
|
802
|
+
norm = (
|
|
803
|
+
math.fsum(abs(value) for value in selected)
|
|
804
|
+
if preprocessor.mode == "l1"
|
|
805
|
+
else math.sqrt(math.fsum(value * value for value in selected))
|
|
806
|
+
)
|
|
807
|
+
if norm:
|
|
808
|
+
selected = [value / norm for value in selected]
|
|
809
|
+
transformed.append(tuple(selected))
|
|
810
|
+
return tuple(transformed)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
__all__ = [
|
|
814
|
+
"DEFAULT_MAX_MULTISCALE_LEVELS",
|
|
815
|
+
"DEFAULT_MAX_PREPROCESS_ROWS",
|
|
816
|
+
"AdvancedFingerprintResult",
|
|
817
|
+
"FeaturePreprocessor",
|
|
818
|
+
"ThermodynamicMissingStrategy",
|
|
819
|
+
"ThermodynamicStructureAdapter",
|
|
820
|
+
"coding_fingerprint",
|
|
821
|
+
"fit_preprocessor",
|
|
822
|
+
"gc_spatial_fingerprint",
|
|
823
|
+
"hybrid_fingerprint",
|
|
824
|
+
"motif_fingerprint",
|
|
825
|
+
"multiscale_fingerprint",
|
|
826
|
+
"repeat_fingerprint",
|
|
827
|
+
"restriction_fingerprint",
|
|
828
|
+
"thermodynamic_fingerprint",
|
|
829
|
+
]
|