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,193 @@
|
|
|
1
|
+
"""Native length, composition, skew, and CpG descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from dnakit.core._json import FrozenDict
|
|
9
|
+
from dnakit.core.gap import Gap
|
|
10
|
+
from dnakit.descriptors._shared import (
|
|
11
|
+
DescriptorAmbiguityPolicy,
|
|
12
|
+
SequenceInput,
|
|
13
|
+
canonical_runs,
|
|
14
|
+
coerce_ambiguity_policy,
|
|
15
|
+
fragments,
|
|
16
|
+
reject_ambiguity,
|
|
17
|
+
sequence_and_id,
|
|
18
|
+
)
|
|
19
|
+
from dnakit.descriptors.results import (
|
|
20
|
+
CompositionResult,
|
|
21
|
+
ContentResult,
|
|
22
|
+
CpGResult,
|
|
23
|
+
LengthResult,
|
|
24
|
+
SkewResult,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def length_features(value: SequenceInput) -> LengthResult:
|
|
29
|
+
"""Return lengths without guessing the span of an unknown-length gap."""
|
|
30
|
+
|
|
31
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
32
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
33
|
+
return LengthResult(
|
|
34
|
+
name="length",
|
|
35
|
+
method="explicit_sequence_parts",
|
|
36
|
+
sequence_id=sequence_id,
|
|
37
|
+
ambiguity_policy=None,
|
|
38
|
+
cross_gaps=False,
|
|
39
|
+
gap_count=len(gaps),
|
|
40
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
41
|
+
symbol_length=sequence.symbol_length,
|
|
42
|
+
coordinate_span=sequence.coordinate_span,
|
|
43
|
+
canonical_base_count=sequence.canonical_base_count,
|
|
44
|
+
ambiguity_length=sequence.ambiguity_count,
|
|
45
|
+
known_gap_length=sum(gap.length or 0 for gap in gaps),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def base_composition(
|
|
50
|
+
value: SequenceInput,
|
|
51
|
+
*,
|
|
52
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
53
|
+
) -> CompositionResult:
|
|
54
|
+
"""Count A/C/G/T; ignored ambiguity is excluded from the fraction denominator."""
|
|
55
|
+
|
|
56
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
57
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
58
|
+
reject_ambiguity(sequence, policy)
|
|
59
|
+
counts = Counter(symbol for symbol in sequence.symbols if symbol in "ACGT")
|
|
60
|
+
denominator = sum(counts.values())
|
|
61
|
+
ordered_counts = {base: counts[base] for base in "ACGT"}
|
|
62
|
+
fractions = {
|
|
63
|
+
base: (ordered_counts[base] / denominator if denominator else 0.0) for base in "ACGT"
|
|
64
|
+
}
|
|
65
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
66
|
+
return CompositionResult(
|
|
67
|
+
name="base_composition",
|
|
68
|
+
method="canonical_base_count",
|
|
69
|
+
sequence_id=sequence_id,
|
|
70
|
+
ambiguity_policy=policy,
|
|
71
|
+
cross_gaps=False,
|
|
72
|
+
gap_count=len(gaps),
|
|
73
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
74
|
+
counts=FrozenDict(ordered_counts),
|
|
75
|
+
fractions=FrozenDict(fractions),
|
|
76
|
+
denominator=denominator,
|
|
77
|
+
ignored_ambiguity_count=(
|
|
78
|
+
sequence.ambiguity_count if policy is DescriptorAmbiguityPolicy.IGNORE else 0
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def gc_at_content(
|
|
84
|
+
value: SequenceInput,
|
|
85
|
+
*,
|
|
86
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
87
|
+
) -> ContentResult:
|
|
88
|
+
"""Calculate GC and AT fractions over the A/C/G/T denominator."""
|
|
89
|
+
|
|
90
|
+
composition = base_composition(value, ambiguity_policy=ambiguity_policy)
|
|
91
|
+
gc_count = cast(int, composition.counts["G"]) + cast(int, composition.counts["C"])
|
|
92
|
+
at_count = cast(int, composition.counts["A"]) + cast(int, composition.counts["T"])
|
|
93
|
+
denominator = composition.denominator
|
|
94
|
+
return ContentResult(
|
|
95
|
+
name="gc_at_content",
|
|
96
|
+
method="canonical_base_fraction",
|
|
97
|
+
sequence_id=composition.sequence_id,
|
|
98
|
+
ambiguity_policy=composition.ambiguity_policy,
|
|
99
|
+
cross_gaps=False,
|
|
100
|
+
gap_count=composition.gap_count,
|
|
101
|
+
unknown_gap_count=composition.unknown_gap_count,
|
|
102
|
+
gc_count=gc_count,
|
|
103
|
+
at_count=at_count,
|
|
104
|
+
gc_fraction=gc_count / denominator if denominator else None,
|
|
105
|
+
at_fraction=at_count / denominator if denominator else None,
|
|
106
|
+
denominator=denominator,
|
|
107
|
+
ignored_ambiguity_count=composition.ignored_ambiguity_count,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def base_skew(
|
|
112
|
+
value: SequenceInput,
|
|
113
|
+
*,
|
|
114
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
115
|
+
) -> SkewResult:
|
|
116
|
+
"""Calculate GC and AT skew; a zero formula denominator produces None."""
|
|
117
|
+
|
|
118
|
+
composition = base_composition(value, ambiguity_policy=ambiguity_policy)
|
|
119
|
+
a = cast(int, composition.counts["A"])
|
|
120
|
+
c = cast(int, composition.counts["C"])
|
|
121
|
+
g = cast(int, composition.counts["G"])
|
|
122
|
+
t = cast(int, composition.counts["T"])
|
|
123
|
+
gc_denominator = g + c
|
|
124
|
+
at_denominator = a + t
|
|
125
|
+
return SkewResult(
|
|
126
|
+
name="base_skew",
|
|
127
|
+
method="gc=(g-c)/(g+c);at=(a-t)/(a+t)",
|
|
128
|
+
sequence_id=composition.sequence_id,
|
|
129
|
+
ambiguity_policy=composition.ambiguity_policy,
|
|
130
|
+
cross_gaps=False,
|
|
131
|
+
gap_count=composition.gap_count,
|
|
132
|
+
unknown_gap_count=composition.unknown_gap_count,
|
|
133
|
+
gc_skew=(g - c) / gc_denominator if gc_denominator else None,
|
|
134
|
+
at_skew=(a - t) / at_denominator if at_denominator else None,
|
|
135
|
+
gc_denominator=gc_denominator,
|
|
136
|
+
at_denominator=at_denominator,
|
|
137
|
+
ignored_ambiguity_count=composition.ignored_ambiguity_count,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cpg_features(
|
|
142
|
+
value: SequenceInput,
|
|
143
|
+
*,
|
|
144
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
145
|
+
cross_gaps: bool = False,
|
|
146
|
+
) -> CpGResult:
|
|
147
|
+
"""Calculate CpG statistics without crossing gaps or ambiguity by default.
|
|
148
|
+
|
|
149
|
+
Density is ``count(CG) / eligible adjacent canonical pairs``. Observed/expected
|
|
150
|
+
is ``count(CG) * canonical_base_count / (count(C) * count(G))``. Undefined
|
|
151
|
+
ratios are returned as ``None`` rather than NaN or infinity.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
155
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
156
|
+
reject_ambiguity(sequence, policy)
|
|
157
|
+
text_runs = tuple(
|
|
158
|
+
run
|
|
159
|
+
for fragment in fragments(sequence, cross_gaps=cross_gaps)
|
|
160
|
+
for run in canonical_runs(fragment)
|
|
161
|
+
)
|
|
162
|
+
cpg_count = sum(run.count("CG") for run in text_runs)
|
|
163
|
+
pair_denominator = sum(max(0, len(run) - 1) for run in text_runs)
|
|
164
|
+
canonical_length = sum(len(run) for run in text_runs)
|
|
165
|
+
c_count = sum(run.count("C") for run in text_runs)
|
|
166
|
+
g_count = sum(run.count("G") for run in text_runs)
|
|
167
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
168
|
+
return CpGResult(
|
|
169
|
+
name="cpg",
|
|
170
|
+
method="adjacent_cg_and_length_normalized_oe",
|
|
171
|
+
sequence_id=sequence_id,
|
|
172
|
+
ambiguity_policy=policy,
|
|
173
|
+
cross_gaps=cross_gaps,
|
|
174
|
+
gap_count=len(gaps),
|
|
175
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
176
|
+
cpg_count=cpg_count,
|
|
177
|
+
density=cpg_count / pair_denominator if pair_denominator else None,
|
|
178
|
+
observed_expected=(
|
|
179
|
+
cpg_count * canonical_length / (c_count * g_count) if c_count and g_count else None
|
|
180
|
+
),
|
|
181
|
+
adjacent_pair_denominator=pair_denominator,
|
|
182
|
+
expected_length_denominator=canonical_length,
|
|
183
|
+
c_count=c_count,
|
|
184
|
+
g_count=g_count,
|
|
185
|
+
ignored_ambiguity_count=(
|
|
186
|
+
sequence.ambiguity_count if policy is DescriptorAmbiguityPolicy.IGNORE else 0
|
|
187
|
+
),
|
|
188
|
+
density_formula="count(CG)/eligible_adjacent_canonical_pairs",
|
|
189
|
+
observed_expected_formula="count(CG)*canonical_base_count/(count(C)*count(G))",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = ["base_composition", "base_skew", "cpg_features", "gc_at_content", "length_features"]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Native standard-code codon statistics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from dnakit.core._json import FrozenDict
|
|
9
|
+
from dnakit.core.gap import Gap
|
|
10
|
+
from dnakit.core.sequence import DNASequence
|
|
11
|
+
from dnakit.descriptors._shared import (
|
|
12
|
+
DescriptorAmbiguityPolicy,
|
|
13
|
+
SequenceInput,
|
|
14
|
+
coerce_ambiguity_policy,
|
|
15
|
+
reject_ambiguity,
|
|
16
|
+
sequence_and_id,
|
|
17
|
+
symbol_view,
|
|
18
|
+
validate_bool,
|
|
19
|
+
)
|
|
20
|
+
from dnakit.descriptors.results import CodonResult
|
|
21
|
+
from dnakit.exceptions import ConfigurationError
|
|
22
|
+
|
|
23
|
+
_STANDARD_STARTS = frozenset({"ATG"})
|
|
24
|
+
_STANDARD_STOPS = frozenset({"TAA", "TAG", "TGA"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class _CodonAudit:
|
|
29
|
+
counts: Counter[str]
|
|
30
|
+
ignored_ambiguity: int
|
|
31
|
+
gap_interrupted: int
|
|
32
|
+
incomplete_bases: int
|
|
33
|
+
phase_unresolved: bool
|
|
34
|
+
unresolved_downstream_bases: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _coordinate_codon_audit(sequence: DNASequence, frame: int) -> _CodonAudit:
|
|
38
|
+
slots: dict[int, dict[int, str]] = {}
|
|
39
|
+
known_gaps: list[tuple[int, int]] = []
|
|
40
|
+
coordinate: int | None = 0
|
|
41
|
+
unknown_gap_start: int | None = None
|
|
42
|
+
unresolved_downstream = 0
|
|
43
|
+
for part in sequence.parts:
|
|
44
|
+
if isinstance(part, str):
|
|
45
|
+
if coordinate is None:
|
|
46
|
+
unresolved_downstream += len(part)
|
|
47
|
+
continue
|
|
48
|
+
for symbol in part:
|
|
49
|
+
if coordinate >= frame:
|
|
50
|
+
relative = coordinate - frame
|
|
51
|
+
slots.setdefault(relative // 3, {})[relative % 3] = symbol
|
|
52
|
+
coordinate += 1
|
|
53
|
+
elif coordinate is not None:
|
|
54
|
+
if part.length is None:
|
|
55
|
+
unknown_gap_start = coordinate
|
|
56
|
+
coordinate = None
|
|
57
|
+
else:
|
|
58
|
+
known_gaps.append((coordinate, coordinate + part.length))
|
|
59
|
+
coordinate += part.length
|
|
60
|
+
|
|
61
|
+
counts: Counter[str] = Counter()
|
|
62
|
+
ignored_ambiguity = 0
|
|
63
|
+
interrupted = 0
|
|
64
|
+
incomplete = 0
|
|
65
|
+
for slot, symbols in slots.items():
|
|
66
|
+
codon_start = frame + slot * 3
|
|
67
|
+
codon_end = codon_start + 3
|
|
68
|
+
if len(symbols) == 3:
|
|
69
|
+
codon = "".join(symbols[position] for position in range(3))
|
|
70
|
+
if any(base not in "ACGT" for base in codon):
|
|
71
|
+
ignored_ambiguity += 1
|
|
72
|
+
else:
|
|
73
|
+
counts[codon] += 1
|
|
74
|
+
continue
|
|
75
|
+
overlaps_known_gap = any(
|
|
76
|
+
gap_start < codon_end and gap_end > codon_start for gap_start, gap_end in known_gaps
|
|
77
|
+
)
|
|
78
|
+
overlaps_unknown_gap = (
|
|
79
|
+
unknown_gap_start is not None and codon_start <= unknown_gap_start < codon_end
|
|
80
|
+
)
|
|
81
|
+
if overlaps_known_gap or overlaps_unknown_gap:
|
|
82
|
+
interrupted += 1
|
|
83
|
+
else:
|
|
84
|
+
incomplete += len(symbols)
|
|
85
|
+
return _CodonAudit(
|
|
86
|
+
counts,
|
|
87
|
+
ignored_ambiguity,
|
|
88
|
+
interrupted,
|
|
89
|
+
incomplete,
|
|
90
|
+
unknown_gap_start is not None,
|
|
91
|
+
unresolved_downstream,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _concatenated_codon_audit(sequence: DNASequence, frame: int) -> _CodonAudit:
|
|
96
|
+
view = symbol_view(sequence)
|
|
97
|
+
codon_segments = view.crossable_segment_ids
|
|
98
|
+
counts: Counter[str] = Counter()
|
|
99
|
+
ignored_ambiguity = 0
|
|
100
|
+
gap_interrupted = 0
|
|
101
|
+
complete_candidate_count = 0
|
|
102
|
+
for start in range(frame, len(view.text) - 2, 3):
|
|
103
|
+
complete_candidate_count += 1
|
|
104
|
+
segments = codon_segments[start : start + 3]
|
|
105
|
+
if len(set(segments)) != 1:
|
|
106
|
+
gap_interrupted += 1
|
|
107
|
+
continue
|
|
108
|
+
codon = view.text[start : start + 3]
|
|
109
|
+
if any(base not in "ACGT" for base in codon):
|
|
110
|
+
ignored_ambiguity += 1
|
|
111
|
+
else:
|
|
112
|
+
counts[codon] += 1
|
|
113
|
+
eligible_after_frame = max(0, len(view.text) - frame)
|
|
114
|
+
return _CodonAudit(
|
|
115
|
+
counts,
|
|
116
|
+
ignored_ambiguity,
|
|
117
|
+
gap_interrupted,
|
|
118
|
+
eligible_after_frame - complete_candidate_count * 3,
|
|
119
|
+
False,
|
|
120
|
+
0,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def codon_statistics(
|
|
125
|
+
value: SequenceInput,
|
|
126
|
+
*,
|
|
127
|
+
frame: int = 0,
|
|
128
|
+
genetic_code: int = 1,
|
|
129
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
130
|
+
cross_gaps: bool = False,
|
|
131
|
+
) -> CodonResult:
|
|
132
|
+
"""Count in-frame codons using NCBI standard genetic code table 1.
|
|
133
|
+
|
|
134
|
+
By default, ``frame`` is a zero-based offset in the full sequence coordinate
|
|
135
|
+
span: known Gap lengths advance phase, codons overlapping a Gap are omitted,
|
|
136
|
+
and an unknown-length Gap makes downstream phase unresolved. With
|
|
137
|
+
``cross_gaps=True``, crossable Gaps are explicitly omitted and phase follows
|
|
138
|
+
concatenated symbol coordinates; a ``crossable=False`` Gap remains a hard
|
|
139
|
+
boundary. An ignored ambiguous codon is omitted without changing phase.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
143
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
144
|
+
reject_ambiguity(sequence, policy)
|
|
145
|
+
if isinstance(frame, bool) or not isinstance(frame, int) or frame not in {0, 1, 2}:
|
|
146
|
+
raise ConfigurationError(
|
|
147
|
+
"frame must be the zero-based offset 0, 1, or 2.",
|
|
148
|
+
context={"frame": frame},
|
|
149
|
+
)
|
|
150
|
+
if genetic_code != 1 or isinstance(genetic_code, bool):
|
|
151
|
+
raise ConfigurationError(
|
|
152
|
+
"MVP codon statistics currently support only genetic_code=1.",
|
|
153
|
+
context={"genetic_code": genetic_code},
|
|
154
|
+
)
|
|
155
|
+
validate_bool(cross_gaps, "cross_gaps")
|
|
156
|
+
|
|
157
|
+
audit = (
|
|
158
|
+
_concatenated_codon_audit(sequence, frame)
|
|
159
|
+
if cross_gaps
|
|
160
|
+
else _coordinate_codon_audit(sequence, frame)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
ordered_counts = dict(sorted(audit.counts.items()))
|
|
164
|
+
codon_count = sum(ordered_counts.values())
|
|
165
|
+
frequencies = {codon: count / codon_count for codon, count in ordered_counts.items()}
|
|
166
|
+
start_count = sum(audit.counts[codon] for codon in _STANDARD_STARTS)
|
|
167
|
+
stop_count = sum(audit.counts[codon] for codon in _STANDARD_STOPS)
|
|
168
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
169
|
+
return CodonResult(
|
|
170
|
+
name="codon",
|
|
171
|
+
method="in_frame_standard_genetic_code",
|
|
172
|
+
sequence_id=sequence_id,
|
|
173
|
+
ambiguity_policy=policy,
|
|
174
|
+
cross_gaps=cross_gaps,
|
|
175
|
+
gap_count=len(gaps),
|
|
176
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
177
|
+
frame=frame,
|
|
178
|
+
genetic_code=genetic_code,
|
|
179
|
+
counts=FrozenDict(ordered_counts),
|
|
180
|
+
frequencies=FrozenDict(frequencies),
|
|
181
|
+
codon_count=codon_count,
|
|
182
|
+
start_count=start_count,
|
|
183
|
+
stop_count=stop_count,
|
|
184
|
+
start_density=start_count / codon_count if codon_count else None,
|
|
185
|
+
stop_density=stop_count / codon_count if codon_count else None,
|
|
186
|
+
incomplete_base_count=audit.incomplete_bases,
|
|
187
|
+
ignored_ambiguity_codon_count=audit.ignored_ambiguity,
|
|
188
|
+
gap_interrupted_codon_count=audit.gap_interrupted,
|
|
189
|
+
phase_coordinate_system=("concatenated-symbol" if cross_gaps else "sequence-coordinate"),
|
|
190
|
+
phase_unresolved_after_gap=audit.phase_unresolved,
|
|
191
|
+
unresolved_downstream_base_count=audit.unresolved_downstream_bases,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = ["codon_statistics"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Bounded, explicit sequence-complexity and exact-repeat descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
|
|
8
|
+
from dnakit.core import Gap, Topology
|
|
9
|
+
from dnakit.core._json import FrozenDict
|
|
10
|
+
from dnakit.exceptions import ConfigurationError
|
|
11
|
+
|
|
12
|
+
from ._shared import (
|
|
13
|
+
DescriptorAmbiguityPolicy,
|
|
14
|
+
SequenceInput,
|
|
15
|
+
canonical_runs,
|
|
16
|
+
coerce_ambiguity_policy,
|
|
17
|
+
fragments,
|
|
18
|
+
reject_ambiguity,
|
|
19
|
+
sequence_and_id,
|
|
20
|
+
validate_bool,
|
|
21
|
+
validate_positive_int,
|
|
22
|
+
)
|
|
23
|
+
from .results import ComplexityResult, ExactRepeatResult, RepeatRun
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def linguistic_complexity(
|
|
27
|
+
value: SequenceInput,
|
|
28
|
+
*,
|
|
29
|
+
max_word_size: int = 6,
|
|
30
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
31
|
+
cross_gaps: bool = False,
|
|
32
|
+
max_observations: int = 10_000_000,
|
|
33
|
+
) -> ComplexityResult:
|
|
34
|
+
"""Compute observed/possible vocabulary ratios for k=1..max_word_size.
|
|
35
|
+
|
|
36
|
+
For each k, the denominator is ``min(4**k, number of k-mer positions)``.
|
|
37
|
+
The aggregate score is the product of per-k ratios, a common explicit
|
|
38
|
+
linguistic-complexity definition; no DUST implementation is claimed.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
42
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
43
|
+
raise ConfigurationError(
|
|
44
|
+
"Linguistic complexity currently requires a linear sequence.",
|
|
45
|
+
code="COMPLEXITY_CIRCULAR_UNSUPPORTED",
|
|
46
|
+
)
|
|
47
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
48
|
+
reject_ambiguity(sequence, policy)
|
|
49
|
+
validate_positive_int(max_word_size, "max_word_size")
|
|
50
|
+
validate_positive_int(max_observations, "max_observations")
|
|
51
|
+
validate_bool(cross_gaps, "cross_gaps")
|
|
52
|
+
if max_word_size > 16:
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
"max_word_size cannot exceed 16.", code="COMPLEXITY_WORD_SIZE_LIMIT"
|
|
55
|
+
)
|
|
56
|
+
by_k: dict[str, float] = {}
|
|
57
|
+
observed_by_k: dict[str, int] = {}
|
|
58
|
+
possible_by_k: dict[str, int] = {}
|
|
59
|
+
total_observations = 0
|
|
60
|
+
for k in range(1, max_word_size + 1):
|
|
61
|
+
words: set[str] = set()
|
|
62
|
+
positions = 0
|
|
63
|
+
for fragment in fragments(sequence, cross_gaps=cross_gaps):
|
|
64
|
+
for run in canonical_runs(fragment):
|
|
65
|
+
run_positions = max(0, len(run) - k + 1)
|
|
66
|
+
positions += run_positions
|
|
67
|
+
total_observations += run_positions
|
|
68
|
+
if total_observations > max_observations:
|
|
69
|
+
raise ConfigurationError(
|
|
70
|
+
"Complexity calculation exceeds max_observations.",
|
|
71
|
+
code="COMPLEXITY_OBSERVATION_LIMIT",
|
|
72
|
+
)
|
|
73
|
+
words.update(run[offset : offset + k] for offset in range(run_positions))
|
|
74
|
+
possible = min(4**k, positions)
|
|
75
|
+
ratio = len(words) / possible if possible else 0.0
|
|
76
|
+
by_k[str(k)] = ratio
|
|
77
|
+
observed_by_k[str(k)] = len(words)
|
|
78
|
+
possible_by_k[str(k)] = possible
|
|
79
|
+
nonempty = tuple(value for key, value in by_k.items() if possible_by_k[key] > 0)
|
|
80
|
+
score = math.prod(nonempty) if nonempty else 0.0
|
|
81
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
82
|
+
return ComplexityResult(
|
|
83
|
+
name="linguistic_complexity",
|
|
84
|
+
method="vocabulary-observed-over-possible-product",
|
|
85
|
+
sequence_id=sequence_id,
|
|
86
|
+
ambiguity_policy=policy,
|
|
87
|
+
cross_gaps=cross_gaps,
|
|
88
|
+
gap_count=len(gaps),
|
|
89
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
90
|
+
score=score,
|
|
91
|
+
max_word_size=max_word_size,
|
|
92
|
+
by_k=FrozenDict(by_k),
|
|
93
|
+
observed_by_k=FrozenDict(observed_by_k),
|
|
94
|
+
possible_by_k=FrozenDict(possible_by_k),
|
|
95
|
+
observation_count=total_observations,
|
|
96
|
+
max_observations=max_observations,
|
|
97
|
+
formula="product_k(unique_kmers/min(4**k,valid_kmer_positions))",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def exact_repeat_fraction(
|
|
102
|
+
value: SequenceInput,
|
|
103
|
+
*,
|
|
104
|
+
min_unit_length: int = 1,
|
|
105
|
+
max_unit_length: int = 20,
|
|
106
|
+
min_repeats: int = 2,
|
|
107
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
108
|
+
cross_gaps: bool = False,
|
|
109
|
+
max_comparisons: int = 5_000_000,
|
|
110
|
+
) -> ExactRepeatResult:
|
|
111
|
+
"""Measure union coverage of maximal exact tandem-repeat runs."""
|
|
112
|
+
|
|
113
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
114
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
115
|
+
raise ConfigurationError(
|
|
116
|
+
"Exact tandem-repeat coverage currently requires a linear sequence.",
|
|
117
|
+
code="REPEAT_CIRCULAR_UNSUPPORTED",
|
|
118
|
+
)
|
|
119
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
120
|
+
reject_ambiguity(sequence, policy)
|
|
121
|
+
for name, item in (
|
|
122
|
+
("min_unit_length", min_unit_length),
|
|
123
|
+
("max_unit_length", max_unit_length),
|
|
124
|
+
("min_repeats", min_repeats),
|
|
125
|
+
("max_comparisons", max_comparisons),
|
|
126
|
+
):
|
|
127
|
+
validate_positive_int(item, name)
|
|
128
|
+
validate_bool(cross_gaps, "cross_gaps")
|
|
129
|
+
if min_unit_length > max_unit_length or max_unit_length > 100 or min_repeats < 2:
|
|
130
|
+
raise ConfigurationError(
|
|
131
|
+
"Repeat bounds require 1 <= min_unit_length <= max_unit_length <= 100 and "
|
|
132
|
+
"min_repeats >= 2.",
|
|
133
|
+
code="INVALID_REPEAT_DESCRIPTOR_CONFIG",
|
|
134
|
+
)
|
|
135
|
+
runs: list[RepeatRun] = []
|
|
136
|
+
covered_positions: set[int] = set()
|
|
137
|
+
comparisons = 0
|
|
138
|
+
symbol_offset = 0
|
|
139
|
+
for fragment in fragments(sequence, cross_gaps=cross_gaps):
|
|
140
|
+
start = 0
|
|
141
|
+
while start < len(fragment):
|
|
142
|
+
found: tuple[str, int, int] | None = None
|
|
143
|
+
for unit_length in range(min_unit_length, max_unit_length + 1):
|
|
144
|
+
if start + unit_length * min_repeats > len(fragment):
|
|
145
|
+
continue
|
|
146
|
+
unit = fragment[start : start + unit_length]
|
|
147
|
+
if any(symbol not in "ACGT" for symbol in unit):
|
|
148
|
+
continue
|
|
149
|
+
count = 1
|
|
150
|
+
cursor = start + unit_length
|
|
151
|
+
while cursor + unit_length <= len(fragment):
|
|
152
|
+
comparisons += 1
|
|
153
|
+
if comparisons > max_comparisons:
|
|
154
|
+
raise ConfigurationError(
|
|
155
|
+
"Repeat calculation exceeds max_comparisons.",
|
|
156
|
+
code="REPEAT_COMPARISON_LIMIT",
|
|
157
|
+
)
|
|
158
|
+
if fragment[cursor : cursor + unit_length] != unit:
|
|
159
|
+
break
|
|
160
|
+
count += 1
|
|
161
|
+
cursor += unit_length
|
|
162
|
+
if count >= min_repeats:
|
|
163
|
+
found = unit, count, cursor
|
|
164
|
+
break
|
|
165
|
+
if found is None:
|
|
166
|
+
start += 1
|
|
167
|
+
continue
|
|
168
|
+
unit, count, end = found
|
|
169
|
+
absolute_start = symbol_offset + start
|
|
170
|
+
absolute_end = symbol_offset + end
|
|
171
|
+
runs.append(RepeatRun(unit, len(unit), count, absolute_start, absolute_end))
|
|
172
|
+
covered_positions.update(range(absolute_start, absolute_end))
|
|
173
|
+
start = end
|
|
174
|
+
symbol_offset += len(fragment)
|
|
175
|
+
denominator = sequence.canonical_base_count
|
|
176
|
+
repeat_count_by_unit = Counter(run.unit_length for run in runs)
|
|
177
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
178
|
+
return ExactRepeatResult(
|
|
179
|
+
name="exact_repeat_fraction",
|
|
180
|
+
method="maximal-exact-tandem-repeat-union",
|
|
181
|
+
sequence_id=sequence_id,
|
|
182
|
+
ambiguity_policy=policy,
|
|
183
|
+
cross_gaps=cross_gaps,
|
|
184
|
+
gap_count=len(gaps),
|
|
185
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
186
|
+
repeat_fraction=len(covered_positions) / denominator if denominator else 0.0,
|
|
187
|
+
repeated_base_count=len(covered_positions),
|
|
188
|
+
denominator=denominator,
|
|
189
|
+
min_unit_length=min_unit_length,
|
|
190
|
+
max_unit_length=max_unit_length,
|
|
191
|
+
min_repeats=min_repeats,
|
|
192
|
+
runs=tuple(runs),
|
|
193
|
+
repeat_count_by_unit=FrozenDict(
|
|
194
|
+
{str(key): value for key, value in sorted(repeat_count_by_unit.items())}
|
|
195
|
+
),
|
|
196
|
+
comparisons=comparisons,
|
|
197
|
+
max_comparisons=max_comparisons,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = ["exact_repeat_fraction", "linguistic_complexity"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Native Shannon entropy descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
from dnakit.core.gap import Gap
|
|
10
|
+
from dnakit.descriptors._shared import (
|
|
11
|
+
DescriptorAmbiguityPolicy,
|
|
12
|
+
SequenceInput,
|
|
13
|
+
coerce_ambiguity_policy,
|
|
14
|
+
iter_kmers,
|
|
15
|
+
reject_ambiguity,
|
|
16
|
+
sequence_and_id,
|
|
17
|
+
validate_bool,
|
|
18
|
+
validate_positive_int,
|
|
19
|
+
)
|
|
20
|
+
from dnakit.descriptors.results import EntropyResult
|
|
21
|
+
from dnakit.exceptions import ConfigurationError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def shannon_entropy(
|
|
25
|
+
value: SequenceInput,
|
|
26
|
+
*,
|
|
27
|
+
unit: str = "base",
|
|
28
|
+
k: int = 1,
|
|
29
|
+
log_base: float = 2.0,
|
|
30
|
+
ambiguity_policy: DescriptorAmbiguityPolicy | str = DescriptorAmbiguityPolicy.ERROR,
|
|
31
|
+
cross_gaps: bool = False,
|
|
32
|
+
) -> EntropyResult:
|
|
33
|
+
"""Calculate Shannon entropy for canonical bases or valid k-mers.
|
|
34
|
+
|
|
35
|
+
An empty observation distribution has entropy ``0.0``. The log base must be
|
|
36
|
+
finite, positive, and different from one.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
40
|
+
policy = coerce_ambiguity_policy(ambiguity_policy)
|
|
41
|
+
reject_ambiguity(sequence, policy)
|
|
42
|
+
if unit not in {"base", "kmer"}:
|
|
43
|
+
raise ConfigurationError(
|
|
44
|
+
"Entropy unit must be 'base' or 'kmer'.",
|
|
45
|
+
context={"unit": unit},
|
|
46
|
+
)
|
|
47
|
+
validate_positive_int(k, "k")
|
|
48
|
+
validate_bool(cross_gaps, "cross_gaps")
|
|
49
|
+
if (
|
|
50
|
+
isinstance(log_base, bool)
|
|
51
|
+
or not isinstance(log_base, (int, float))
|
|
52
|
+
or not math.isfinite(log_base)
|
|
53
|
+
or log_base <= 0
|
|
54
|
+
or log_base == 1
|
|
55
|
+
):
|
|
56
|
+
raise ConfigurationError(
|
|
57
|
+
"log_base must be finite, positive, and different from one.",
|
|
58
|
+
context={"log_base": log_base},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
observations: Iterable[str]
|
|
62
|
+
if unit == "base":
|
|
63
|
+
observations = (symbol for symbol in sequence.symbols if symbol in "ACGT")
|
|
64
|
+
resolved_k = 1
|
|
65
|
+
else:
|
|
66
|
+
observations = iter_kmers(
|
|
67
|
+
sequence,
|
|
68
|
+
k=k,
|
|
69
|
+
overlapping=True,
|
|
70
|
+
cross_gaps=cross_gaps,
|
|
71
|
+
)
|
|
72
|
+
resolved_k = k
|
|
73
|
+
counts = Counter(observations)
|
|
74
|
+
total = sum(counts.values())
|
|
75
|
+
entropy = (
|
|
76
|
+
-sum((count / total) * math.log(count / total, log_base) for count in counts.values())
|
|
77
|
+
if total
|
|
78
|
+
else 0.0
|
|
79
|
+
)
|
|
80
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
81
|
+
return EntropyResult(
|
|
82
|
+
name="shannon_entropy",
|
|
83
|
+
method="shannon",
|
|
84
|
+
sequence_id=sequence_id,
|
|
85
|
+
ambiguity_policy=policy,
|
|
86
|
+
cross_gaps=cross_gaps,
|
|
87
|
+
gap_count=len(gaps),
|
|
88
|
+
unknown_gap_count=sum(gap.length is None for gap in gaps),
|
|
89
|
+
entropy=entropy,
|
|
90
|
+
unit=unit,
|
|
91
|
+
k=resolved_k,
|
|
92
|
+
log_base=float(log_base),
|
|
93
|
+
observation_count=total,
|
|
94
|
+
category_count=len(counts),
|
|
95
|
+
ignored_ambiguity_count=(
|
|
96
|
+
sequence.ambiguity_count if policy is DescriptorAmbiguityPolicy.IGNORE else 0
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ["shannon_entropy"]
|