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,77 @@
|
|
|
1
|
+
"""Public DNA normalization and validation API."""
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
AmbiguityPolicy,
|
|
5
|
+
DatasetValidationConfig,
|
|
6
|
+
NormalizationConfig,
|
|
7
|
+
UPolicy,
|
|
8
|
+
ValidationConfig,
|
|
9
|
+
)
|
|
10
|
+
from .gaps import (
|
|
11
|
+
AGPAssemblyConfig,
|
|
12
|
+
AGPAssemblyResult,
|
|
13
|
+
AGPAssemblySegment,
|
|
14
|
+
AGPComponentValue,
|
|
15
|
+
CircularBoundaryPolicy,
|
|
16
|
+
GapNormalizable,
|
|
17
|
+
GapNormalizationChange,
|
|
18
|
+
GapNormalizationConfig,
|
|
19
|
+
GapNormalizationResult,
|
|
20
|
+
normalize_gaps,
|
|
21
|
+
sequence_from_agp,
|
|
22
|
+
)
|
|
23
|
+
from .normalize import RawSequenceInput, normalize
|
|
24
|
+
from .results import (
|
|
25
|
+
AmbiguityReport,
|
|
26
|
+
DatasetValidationReport,
|
|
27
|
+
DuplicateID,
|
|
28
|
+
InputPosition,
|
|
29
|
+
InvalidSymbol,
|
|
30
|
+
NormalizationChange,
|
|
31
|
+
NormalizationResult,
|
|
32
|
+
NormalizationStep,
|
|
33
|
+
ProbabilityResolution,
|
|
34
|
+
QualitySummary,
|
|
35
|
+
RawInputSnapshot,
|
|
36
|
+
SymbolCount,
|
|
37
|
+
SymbolOccurrence,
|
|
38
|
+
ValidationReport,
|
|
39
|
+
)
|
|
40
|
+
from .validate import validate, validate_set
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"AGPAssemblyConfig",
|
|
44
|
+
"AGPAssemblyResult",
|
|
45
|
+
"AGPAssemblySegment",
|
|
46
|
+
"AGPComponentValue",
|
|
47
|
+
"AmbiguityPolicy",
|
|
48
|
+
"AmbiguityReport",
|
|
49
|
+
"CircularBoundaryPolicy",
|
|
50
|
+
"DatasetValidationConfig",
|
|
51
|
+
"DatasetValidationReport",
|
|
52
|
+
"DuplicateID",
|
|
53
|
+
"GapNormalizable",
|
|
54
|
+
"GapNormalizationChange",
|
|
55
|
+
"GapNormalizationConfig",
|
|
56
|
+
"GapNormalizationResult",
|
|
57
|
+
"InputPosition",
|
|
58
|
+
"InvalidSymbol",
|
|
59
|
+
"NormalizationChange",
|
|
60
|
+
"NormalizationConfig",
|
|
61
|
+
"NormalizationResult",
|
|
62
|
+
"NormalizationStep",
|
|
63
|
+
"ProbabilityResolution",
|
|
64
|
+
"QualitySummary",
|
|
65
|
+
"RawInputSnapshot",
|
|
66
|
+
"RawSequenceInput",
|
|
67
|
+
"SymbolCount",
|
|
68
|
+
"SymbolOccurrence",
|
|
69
|
+
"UPolicy",
|
|
70
|
+
"ValidationConfig",
|
|
71
|
+
"ValidationReport",
|
|
72
|
+
"normalize",
|
|
73
|
+
"normalize_gaps",
|
|
74
|
+
"sequence_from_agp",
|
|
75
|
+
"validate",
|
|
76
|
+
"validate_set",
|
|
77
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Private character-analysis kernels shared by normalization and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
|
|
8
|
+
from dnakit.core.gap import Gap
|
|
9
|
+
|
|
10
|
+
from .results import (
|
|
11
|
+
AmbiguityReport,
|
|
12
|
+
ProbabilityResolution,
|
|
13
|
+
SymbolCount,
|
|
14
|
+
SymbolOccurrence,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CANONICAL_BASES = frozenset("ACGT")
|
|
18
|
+
IUPAC_AMBIGUITY = frozenset("RYSWKMBDHVN")
|
|
19
|
+
IUPAC_BASES = CANONICAL_BASES | IUPAC_AMBIGUITY
|
|
20
|
+
IUPAC_EXPANSION: Mapping[str, frozenset[str]] = {
|
|
21
|
+
"R": frozenset("AG"),
|
|
22
|
+
"Y": frozenset("CT"),
|
|
23
|
+
"S": frozenset("CG"),
|
|
24
|
+
"W": frozenset("AT"),
|
|
25
|
+
"K": frozenset("GT"),
|
|
26
|
+
"M": frozenset("AC"),
|
|
27
|
+
"B": frozenset("CGT"),
|
|
28
|
+
"D": frozenset("AGT"),
|
|
29
|
+
"H": frozenset("ACT"),
|
|
30
|
+
"V": frozenset("ACG"),
|
|
31
|
+
"N": frozenset("ACGT"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ambiguity_report(
|
|
36
|
+
parts: Iterable[str | Gap],
|
|
37
|
+
*,
|
|
38
|
+
denominator_includes_gap: bool = False,
|
|
39
|
+
base_priors: Mapping[str, float] | None = None,
|
|
40
|
+
) -> AmbiguityReport:
|
|
41
|
+
"""Describe every ambiguity symbol using normalized symbol coordinates."""
|
|
42
|
+
positions: dict[str, list[int]] = defaultdict(list)
|
|
43
|
+
occurrences: list[SymbolOccurrence] = []
|
|
44
|
+
resolutions: list[ProbabilityResolution] = []
|
|
45
|
+
symbol_offset = 0
|
|
46
|
+
gap_total = 0
|
|
47
|
+
denominator_known = True
|
|
48
|
+
|
|
49
|
+
for part_index, part in enumerate(parts):
|
|
50
|
+
if isinstance(part, Gap):
|
|
51
|
+
if denominator_includes_gap:
|
|
52
|
+
if part.length is None:
|
|
53
|
+
denominator_known = False
|
|
54
|
+
else:
|
|
55
|
+
gap_total += part.length
|
|
56
|
+
continue
|
|
57
|
+
for part_offset, symbol in enumerate(part):
|
|
58
|
+
if symbol in IUPAC_AMBIGUITY:
|
|
59
|
+
positions[symbol].append(symbol_offset)
|
|
60
|
+
occurrences.append(
|
|
61
|
+
SymbolOccurrence(
|
|
62
|
+
symbol=symbol,
|
|
63
|
+
symbol_offset=symbol_offset,
|
|
64
|
+
part_index=part_index,
|
|
65
|
+
part_offset=part_offset,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
if base_priors is not None:
|
|
69
|
+
resolutions.append(
|
|
70
|
+
ProbabilityResolution(
|
|
71
|
+
symbol=symbol,
|
|
72
|
+
symbol_offset=symbol_offset,
|
|
73
|
+
probabilities=_probabilities(symbol, base_priors),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
symbol_offset += 1
|
|
77
|
+
|
|
78
|
+
total = sum(len(item) for item in positions.values())
|
|
79
|
+
denominator = symbol_offset + gap_total if denominator_known else None
|
|
80
|
+
fraction = None if not denominator else total / denominator
|
|
81
|
+
return AmbiguityReport(
|
|
82
|
+
total_count=total,
|
|
83
|
+
denominator=denominator,
|
|
84
|
+
fraction=fraction,
|
|
85
|
+
by_symbol=tuple(
|
|
86
|
+
SymbolCount(symbol=symbol, count=len(items), positions=tuple(items))
|
|
87
|
+
for symbol, items in sorted(positions.items())
|
|
88
|
+
),
|
|
89
|
+
occurrences=tuple(occurrences),
|
|
90
|
+
probability_resolutions=tuple(resolutions),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _probabilities(symbol: str, priors: Mapping[str, float]) -> tuple[tuple[str, float], ...]:
|
|
95
|
+
allowed = IUPAC_EXPANSION[symbol]
|
|
96
|
+
total = sum(priors[base] for base in allowed)
|
|
97
|
+
if total == 0.0:
|
|
98
|
+
uniform = 1.0 / len(allowed)
|
|
99
|
+
return tuple((base, uniform) for base in sorted(allowed))
|
|
100
|
+
return tuple((base, priors[base] / total) for base in sorted(allowed))
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Configuration objects for deterministic sequence standardization and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from itertools import islice
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
from dnakit.core.enums import DNAAlphabet
|
|
13
|
+
from dnakit.exceptions import ConfigurationError
|
|
14
|
+
|
|
15
|
+
_MAX_REQUIRED_FIELDS = 10_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UPolicy(str, Enum):
|
|
19
|
+
"""How :func:`normalize` handles uracil in a DNA input.
|
|
20
|
+
|
|
21
|
+
``WARN`` and ``KEEP`` retain U for audit purposes, but U is not a valid
|
|
22
|
+
DNA alphabet symbol. Consequently those policies produce no
|
|
23
|
+
:class:`~dnakit.core.sequence.DNASequence`; ``WARN`` additionally emits a
|
|
24
|
+
warning. ``DELETE`` removes U and ``REPLACE`` converts U to valid DNA.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
DELETE = "delete"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
WARN = "warn"
|
|
30
|
+
REPLACE = "replace"
|
|
31
|
+
KEEP = "keep"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AmbiguityPolicy(str, Enum):
|
|
35
|
+
"""How IUPAC ambiguity symbols are represented during normalization.
|
|
36
|
+
|
|
37
|
+
``IGNORE`` means that the symbols are retained and excluded from any
|
|
38
|
+
canonical-base-only calculation. It does not delete sequence positions.
|
|
39
|
+
``PROBABILITY`` also retains the symbols and records a deterministic base
|
|
40
|
+
probability distribution in the normalization result. ``DELETE`` removes
|
|
41
|
+
ambiguity symbols while retaining an audit change for every source position.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
DELETE = "delete"
|
|
45
|
+
ERROR = "error"
|
|
46
|
+
IGNORE = "ignore"
|
|
47
|
+
MASK = "mask"
|
|
48
|
+
PROBABILITY = "probability"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class NormalizationConfig:
|
|
53
|
+
"""Control a normalization run and retain an audit entry for every edit.
|
|
54
|
+
|
|
55
|
+
The three simple keep flags define the default character policy. Explicit
|
|
56
|
+
``u_policy`` or ``ambiguity_policy`` values remain available for advanced
|
|
57
|
+
behavior and take precedence over their corresponding keep flag.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
alphabet: DNAAlphabet | None = None
|
|
61
|
+
uppercase: bool = True
|
|
62
|
+
remove_whitespace: bool = True
|
|
63
|
+
remove_invisible: bool = True
|
|
64
|
+
removable_separators: tuple[str, ...] = ()
|
|
65
|
+
keep_ambiguous: bool = True
|
|
66
|
+
keep_u: bool = False
|
|
67
|
+
keep_other: bool = False
|
|
68
|
+
u_policy: UPolicy | None = None
|
|
69
|
+
ambiguity_policy: AmbiguityPolicy | None = None
|
|
70
|
+
ambiguity_mask: str = "N"
|
|
71
|
+
base_priors: Mapping[str, float] = field(
|
|
72
|
+
default_factory=lambda: MappingProxyType({"A": 0.25, "C": 0.25, "G": 0.25, "T": 0.25})
|
|
73
|
+
)
|
|
74
|
+
allow_gaps: bool = True
|
|
75
|
+
raise_on_error: bool = False
|
|
76
|
+
operator: str | None = None
|
|
77
|
+
run_id: str | None = None
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
for field_name in (
|
|
81
|
+
"uppercase",
|
|
82
|
+
"remove_whitespace",
|
|
83
|
+
"remove_invisible",
|
|
84
|
+
"keep_ambiguous",
|
|
85
|
+
"keep_u",
|
|
86
|
+
"keep_other",
|
|
87
|
+
"allow_gaps",
|
|
88
|
+
"raise_on_error",
|
|
89
|
+
):
|
|
90
|
+
if not isinstance(getattr(self, field_name), bool):
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
f"{field_name} must be a boolean.",
|
|
93
|
+
code="INVALID_NORMALIZATION_FLAG",
|
|
94
|
+
context={"field": field_name},
|
|
95
|
+
)
|
|
96
|
+
for field_name in ("operator", "run_id"):
|
|
97
|
+
value = getattr(self, field_name)
|
|
98
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
99
|
+
raise ConfigurationError(
|
|
100
|
+
f"{field_name} must be a non-empty string or None.",
|
|
101
|
+
code="INVALID_NORMALIZATION_AUDIT_FIELD",
|
|
102
|
+
context={"field": field_name},
|
|
103
|
+
)
|
|
104
|
+
raw_separators: object = self.removable_separators
|
|
105
|
+
if isinstance(raw_separators, (str, bytes)):
|
|
106
|
+
raise ConfigurationError(
|
|
107
|
+
"removable_separators must be an iterable of one-character strings.",
|
|
108
|
+
code="INVALID_NORMALIZATION_SEPARATOR",
|
|
109
|
+
)
|
|
110
|
+
separators = tuple(self.removable_separators)
|
|
111
|
+
if any(not isinstance(separator, str) or len(separator) != 1 for separator in separators):
|
|
112
|
+
raise ConfigurationError(
|
|
113
|
+
"Each removable separator must contain exactly one character.",
|
|
114
|
+
code="INVALID_NORMALIZATION_SEPARATOR",
|
|
115
|
+
)
|
|
116
|
+
if len(set(separators)) != len(separators):
|
|
117
|
+
raise ConfigurationError(
|
|
118
|
+
"removable_separators must not contain duplicates.",
|
|
119
|
+
code="DUPLICATE_NORMALIZATION_SEPARATOR",
|
|
120
|
+
)
|
|
121
|
+
if any(separator.isspace() for separator in separators):
|
|
122
|
+
raise ConfigurationError(
|
|
123
|
+
"Whitespace must be controlled with remove_whitespace.",
|
|
124
|
+
code="WHITESPACE_SEPARATOR_CONFLICT",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not isinstance(self.ambiguity_mask, str):
|
|
128
|
+
raise ConfigurationError(
|
|
129
|
+
"ambiguity_mask must be a string.", code="INVALID_AMBIGUITY_MASK"
|
|
130
|
+
)
|
|
131
|
+
mask = self.ambiguity_mask.upper()
|
|
132
|
+
if len(mask) != 1 or mask not in "RYSWKMBDHVN":
|
|
133
|
+
raise ConfigurationError(
|
|
134
|
+
"ambiguity_mask must be one IUPAC ambiguity symbol.",
|
|
135
|
+
code="INVALID_AMBIGUITY_MASK",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
raw_priors = dict(self.base_priors.items())
|
|
140
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
141
|
+
raise ConfigurationError(
|
|
142
|
+
"base_priors must map A, C, G and T to finite numbers.",
|
|
143
|
+
code="INVALID_BASE_PRIORS",
|
|
144
|
+
) from exc
|
|
145
|
+
if any(
|
|
146
|
+
not isinstance(base, str) or base not in {"A", "C", "G", "T"} for base in raw_priors
|
|
147
|
+
):
|
|
148
|
+
raise ConfigurationError(
|
|
149
|
+
"base_priors keys must be exactly uppercase A, C, G and T.",
|
|
150
|
+
code="INVALID_BASE_PRIORS",
|
|
151
|
+
)
|
|
152
|
+
if any(
|
|
153
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
154
|
+
for value in raw_priors.values()
|
|
155
|
+
):
|
|
156
|
+
raise ConfigurationError(
|
|
157
|
+
"base_priors must map A, C, G and T to finite numbers.",
|
|
158
|
+
code="INVALID_BASE_PRIORS",
|
|
159
|
+
)
|
|
160
|
+
priors = {base: float(value) for base, value in raw_priors.items()}
|
|
161
|
+
if set(priors) != {"A", "C", "G", "T"}:
|
|
162
|
+
raise ConfigurationError(
|
|
163
|
+
"base_priors must define exactly A, C, G and T.",
|
|
164
|
+
code="INVALID_BASE_PRIORS",
|
|
165
|
+
)
|
|
166
|
+
if (
|
|
167
|
+
any(not math.isfinite(value) or value < 0.0 for value in priors.values())
|
|
168
|
+
or sum(priors.values()) <= 0.0
|
|
169
|
+
):
|
|
170
|
+
raise ConfigurationError(
|
|
171
|
+
"base_priors must be non-negative and have a positive sum.",
|
|
172
|
+
code="INVALID_BASE_PRIORS",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
alphabet = (
|
|
177
|
+
None
|
|
178
|
+
if self.alphabet is None
|
|
179
|
+
else self.alphabet
|
|
180
|
+
if isinstance(self.alphabet, DNAAlphabet)
|
|
181
|
+
else DNAAlphabet(self.alphabet)
|
|
182
|
+
)
|
|
183
|
+
u_policy = (
|
|
184
|
+
None
|
|
185
|
+
if self.u_policy is None
|
|
186
|
+
else self.u_policy
|
|
187
|
+
if isinstance(self.u_policy, UPolicy)
|
|
188
|
+
else UPolicy(self.u_policy)
|
|
189
|
+
)
|
|
190
|
+
ambiguity_policy = (
|
|
191
|
+
None
|
|
192
|
+
if self.ambiguity_policy is None
|
|
193
|
+
else self.ambiguity_policy
|
|
194
|
+
if isinstance(self.ambiguity_policy, AmbiguityPolicy)
|
|
195
|
+
else AmbiguityPolicy(self.ambiguity_policy)
|
|
196
|
+
)
|
|
197
|
+
except (TypeError, ValueError) as exc:
|
|
198
|
+
raise ConfigurationError(
|
|
199
|
+
"Unknown normalization policy or DNA alphabet.",
|
|
200
|
+
code="INVALID_NORMALIZATION_POLICY",
|
|
201
|
+
) from exc
|
|
202
|
+
|
|
203
|
+
object.__setattr__(self, "alphabet", alphabet)
|
|
204
|
+
object.__setattr__(self, "u_policy", u_policy)
|
|
205
|
+
object.__setattr__(self, "ambiguity_policy", ambiguity_policy)
|
|
206
|
+
object.__setattr__(self, "removable_separators", separators)
|
|
207
|
+
object.__setattr__(self, "ambiguity_mask", mask)
|
|
208
|
+
object.__setattr__(self, "base_priors", MappingProxyType(priors))
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def effective_u_policy(self) -> UPolicy:
|
|
212
|
+
"""Return the advanced U policy or the simple keep/delete policy."""
|
|
213
|
+
|
|
214
|
+
if self.u_policy is not None:
|
|
215
|
+
return self.u_policy
|
|
216
|
+
return UPolicy.KEEP if self.keep_u else UPolicy.DELETE
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def effective_ambiguity_policy(self) -> AmbiguityPolicy:
|
|
220
|
+
"""Return the advanced ambiguity policy or the simple keep/delete policy."""
|
|
221
|
+
|
|
222
|
+
if self.ambiguity_policy is not None:
|
|
223
|
+
return self.ambiguity_policy
|
|
224
|
+
return AmbiguityPolicy.IGNORE if self.keep_ambiguous else AmbiguityPolicy.DELETE
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, slots=True)
|
|
228
|
+
class ValidationConfig:
|
|
229
|
+
"""Thresholds and policies used by :func:`validate`.
|
|
230
|
+
|
|
231
|
+
``sequence_length`` optionally requires an exact ``symbol_length``. It is
|
|
232
|
+
independent of the inclusive ``min_length`` and ``max_length`` bounds.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
alphabet: DNAAlphabet | None = None
|
|
236
|
+
allow_empty: bool = False
|
|
237
|
+
allow_gaps: bool = True
|
|
238
|
+
allow_unknown_gap_length: bool = True
|
|
239
|
+
min_length: int | None = 1
|
|
240
|
+
max_length: int | None = None
|
|
241
|
+
max_ambiguity_fraction: float | None = 1.0
|
|
242
|
+
ambiguity_denominator_includes_gap: bool = False
|
|
243
|
+
required_metadata_fields: tuple[str, ...] = ()
|
|
244
|
+
required_letter_annotations: tuple[str, ...] = ()
|
|
245
|
+
check_phred_quality: bool = True
|
|
246
|
+
minimum_phred: float = 0.0
|
|
247
|
+
maximum_phred: float = 93.0
|
|
248
|
+
minimum_mean_phred: float | None = None
|
|
249
|
+
sequence_length: int | None = None
|
|
250
|
+
|
|
251
|
+
def __post_init__(self) -> None:
|
|
252
|
+
try:
|
|
253
|
+
alphabet = (
|
|
254
|
+
None
|
|
255
|
+
if self.alphabet is None
|
|
256
|
+
else self.alphabet
|
|
257
|
+
if isinstance(self.alphabet, DNAAlphabet)
|
|
258
|
+
else DNAAlphabet(self.alphabet)
|
|
259
|
+
)
|
|
260
|
+
except (TypeError, ValueError) as exc:
|
|
261
|
+
raise ConfigurationError(
|
|
262
|
+
"Unknown validation DNA alphabet.", code="INVALID_VALIDATION_ALPHABET"
|
|
263
|
+
) from exc
|
|
264
|
+
for field_name in (
|
|
265
|
+
"allow_empty",
|
|
266
|
+
"allow_gaps",
|
|
267
|
+
"allow_unknown_gap_length",
|
|
268
|
+
"ambiguity_denominator_includes_gap",
|
|
269
|
+
"check_phred_quality",
|
|
270
|
+
):
|
|
271
|
+
if not isinstance(getattr(self, field_name), bool):
|
|
272
|
+
raise ConfigurationError(
|
|
273
|
+
f"{field_name} must be a boolean.",
|
|
274
|
+
code="INVALID_VALIDATION_FLAG",
|
|
275
|
+
context={"field": field_name},
|
|
276
|
+
)
|
|
277
|
+
for field_name, threshold in (
|
|
278
|
+
("min_length", self.min_length),
|
|
279
|
+
("max_length", self.max_length),
|
|
280
|
+
("sequence_length", self.sequence_length),
|
|
281
|
+
):
|
|
282
|
+
if threshold is not None and (
|
|
283
|
+
isinstance(threshold, bool) or not isinstance(threshold, int) or threshold < 0
|
|
284
|
+
):
|
|
285
|
+
raise ConfigurationError(
|
|
286
|
+
f"{field_name} must be a non-negative integer or None.",
|
|
287
|
+
code="INVALID_LENGTH_THRESHOLD",
|
|
288
|
+
context={"field": field_name},
|
|
289
|
+
)
|
|
290
|
+
if (
|
|
291
|
+
self.min_length is not None
|
|
292
|
+
and self.max_length is not None
|
|
293
|
+
and self.min_length > self.max_length
|
|
294
|
+
):
|
|
295
|
+
raise ConfigurationError(
|
|
296
|
+
"min_length must not exceed max_length.", code="INVALID_LENGTH_THRESHOLD"
|
|
297
|
+
)
|
|
298
|
+
ambiguity_fraction = self.max_ambiguity_fraction
|
|
299
|
+
if ambiguity_fraction is not None and (
|
|
300
|
+
isinstance(ambiguity_fraction, bool)
|
|
301
|
+
or not isinstance(ambiguity_fraction, (int, float))
|
|
302
|
+
or not math.isfinite(ambiguity_fraction)
|
|
303
|
+
or not 0.0 <= ambiguity_fraction <= 1.0
|
|
304
|
+
):
|
|
305
|
+
raise ConfigurationError(
|
|
306
|
+
"max_ambiguity_fraction must be a finite number between 0 and 1.",
|
|
307
|
+
code="INVALID_AMBIGUITY_THRESHOLD",
|
|
308
|
+
)
|
|
309
|
+
quality_values: dict[str, float | None] = {}
|
|
310
|
+
quality_thresholds: tuple[tuple[str, float | None, bool], ...] = (
|
|
311
|
+
("minimum_phred", self.minimum_phred, False),
|
|
312
|
+
("maximum_phred", self.maximum_phred, False),
|
|
313
|
+
("minimum_mean_phred", self.minimum_mean_phred, True),
|
|
314
|
+
)
|
|
315
|
+
for field_name, quality_threshold, optional in quality_thresholds:
|
|
316
|
+
if quality_threshold is None and optional:
|
|
317
|
+
quality_values[field_name] = None
|
|
318
|
+
continue
|
|
319
|
+
if (
|
|
320
|
+
isinstance(quality_threshold, bool)
|
|
321
|
+
or not isinstance(quality_threshold, (int, float))
|
|
322
|
+
or not math.isfinite(quality_threshold)
|
|
323
|
+
):
|
|
324
|
+
raise ConfigurationError(
|
|
325
|
+
f"{field_name} must be a finite number" + (" or None." if optional else "."),
|
|
326
|
+
code="INVALID_QUALITY_THRESHOLD",
|
|
327
|
+
context={"field": field_name},
|
|
328
|
+
)
|
|
329
|
+
quality_values[field_name] = float(quality_threshold)
|
|
330
|
+
minimum_phred = quality_values["minimum_phred"]
|
|
331
|
+
maximum_phred = quality_values["maximum_phred"]
|
|
332
|
+
assert minimum_phred is not None and maximum_phred is not None
|
|
333
|
+
if minimum_phred > maximum_phred:
|
|
334
|
+
raise ConfigurationError(
|
|
335
|
+
"minimum_phred must not exceed maximum_phred.",
|
|
336
|
+
code="INVALID_QUALITY_THRESHOLD",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
resolved_required_fields: dict[str, tuple[str, ...]] = {}
|
|
340
|
+
for field_name, values in (
|
|
341
|
+
("required_metadata_fields", self.required_metadata_fields),
|
|
342
|
+
("required_letter_annotations", self.required_letter_annotations),
|
|
343
|
+
):
|
|
344
|
+
raw_values: object = values
|
|
345
|
+
if isinstance(raw_values, (str, bytes)) or not isinstance(raw_values, Iterable):
|
|
346
|
+
raise ConfigurationError(
|
|
347
|
+
f"{field_name} must be an iterable of non-empty strings.",
|
|
348
|
+
code="INVALID_REQUIRED_FIELD",
|
|
349
|
+
context={"field": field_name},
|
|
350
|
+
)
|
|
351
|
+
resolved = tuple(islice(iter(values), _MAX_REQUIRED_FIELDS + 1))
|
|
352
|
+
if len(resolved) > _MAX_REQUIRED_FIELDS:
|
|
353
|
+
raise ConfigurationError(
|
|
354
|
+
f"{field_name} exceeds the supported field limit.",
|
|
355
|
+
code="REQUIRED_FIELD_LIMIT",
|
|
356
|
+
context={"field": field_name, "max_fields": _MAX_REQUIRED_FIELDS},
|
|
357
|
+
)
|
|
358
|
+
if any(not isinstance(value, str) or not value.strip() for value in resolved):
|
|
359
|
+
raise ConfigurationError(
|
|
360
|
+
f"{field_name} must contain only non-empty strings.",
|
|
361
|
+
code="INVALID_REQUIRED_FIELD",
|
|
362
|
+
context={"field": field_name},
|
|
363
|
+
)
|
|
364
|
+
if len(set(resolved)) != len(resolved):
|
|
365
|
+
raise ConfigurationError(
|
|
366
|
+
f"{field_name} must not contain duplicates.",
|
|
367
|
+
code="DUPLICATE_REQUIRED_FIELD",
|
|
368
|
+
context={"field": field_name},
|
|
369
|
+
)
|
|
370
|
+
resolved_required_fields[field_name] = resolved
|
|
371
|
+
|
|
372
|
+
object.__setattr__(self, "alphabet", alphabet)
|
|
373
|
+
object.__setattr__(
|
|
374
|
+
self,
|
|
375
|
+
"max_ambiguity_fraction",
|
|
376
|
+
None if ambiguity_fraction is None else float(ambiguity_fraction),
|
|
377
|
+
)
|
|
378
|
+
object.__setattr__(self, "minimum_phred", minimum_phred)
|
|
379
|
+
object.__setattr__(self, "maximum_phred", maximum_phred)
|
|
380
|
+
object.__setattr__(self, "minimum_mean_phred", quality_values["minimum_mean_phred"])
|
|
381
|
+
object.__setattr__(
|
|
382
|
+
self, "required_metadata_fields", resolved_required_fields["required_metadata_fields"]
|
|
383
|
+
)
|
|
384
|
+
object.__setattr__(
|
|
385
|
+
self,
|
|
386
|
+
"required_letter_annotations",
|
|
387
|
+
resolved_required_fields["required_letter_annotations"],
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass(frozen=True, slots=True)
|
|
392
|
+
class DatasetValidationConfig:
|
|
393
|
+
"""Collection-level validation rules."""
|
|
394
|
+
|
|
395
|
+
record: ValidationConfig = field(default_factory=ValidationConfig)
|
|
396
|
+
require_unique_ids: bool = True
|
|
397
|
+
collect_record_reports: bool = True
|
|
398
|
+
|
|
399
|
+
def __post_init__(self) -> None:
|
|
400
|
+
if not isinstance(self.record, ValidationConfig):
|
|
401
|
+
raise ConfigurationError(
|
|
402
|
+
"record must be a ValidationConfig object.",
|
|
403
|
+
code="INVALID_DATASET_VALIDATION_CONFIG",
|
|
404
|
+
)
|
|
405
|
+
for field_name in ("require_unique_ids", "collect_record_reports"):
|
|
406
|
+
if not isinstance(getattr(self, field_name), bool):
|
|
407
|
+
raise ConfigurationError(
|
|
408
|
+
f"{field_name} must be a boolean.",
|
|
409
|
+
code="INVALID_VALIDATION_FLAG",
|
|
410
|
+
context={"field": field_name},
|
|
411
|
+
)
|