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,459 @@
|
|
|
1
|
+
"""Dependency-free gap-aware DNA sequence text visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from dnakit.core.enums import IssueSeverity, Topology
|
|
10
|
+
from dnakit.core.facade import DNA, resolve_single_dna
|
|
11
|
+
from dnakit.core.gap import Gap
|
|
12
|
+
from dnakit.core.issues import Issue
|
|
13
|
+
from dnakit.core.record import DNARecord
|
|
14
|
+
from dnakit.core.sequence import DNASequence
|
|
15
|
+
from dnakit.exceptions import ConfigurationError
|
|
16
|
+
|
|
17
|
+
from ._svg import SVGBuilder, validate_xml_text
|
|
18
|
+
from .config import LimitPolicy, SequencePlotConfig, _color
|
|
19
|
+
from .results import SVGArtifact
|
|
20
|
+
|
|
21
|
+
_COMPLEMENT = str.maketrans(
|
|
22
|
+
{
|
|
23
|
+
"A": "T",
|
|
24
|
+
"C": "G",
|
|
25
|
+
"G": "C",
|
|
26
|
+
"T": "A",
|
|
27
|
+
"R": "Y",
|
|
28
|
+
"Y": "R",
|
|
29
|
+
"S": "S",
|
|
30
|
+
"W": "W",
|
|
31
|
+
"K": "M",
|
|
32
|
+
"M": "K",
|
|
33
|
+
"B": "V",
|
|
34
|
+
"D": "H",
|
|
35
|
+
"H": "D",
|
|
36
|
+
"V": "B",
|
|
37
|
+
"N": "N",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Highlight:
|
|
44
|
+
"""A zero-based, half-open highlight in nucleotide-symbol coordinates."""
|
|
45
|
+
|
|
46
|
+
start: int
|
|
47
|
+
end: int
|
|
48
|
+
label: str | None = None
|
|
49
|
+
color: str = "#fde68a"
|
|
50
|
+
foreground: str | None = None
|
|
51
|
+
priority: int = 0
|
|
52
|
+
opacity: float = 0.7
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
for name, value in (("start", self.start), ("end", self.end)):
|
|
56
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
57
|
+
raise ConfigurationError(
|
|
58
|
+
f"Highlight {name} must be a non-negative integer.",
|
|
59
|
+
code="INVALID_HIGHLIGHT",
|
|
60
|
+
)
|
|
61
|
+
if self.end <= self.start:
|
|
62
|
+
raise ConfigurationError(
|
|
63
|
+
"Highlight end must be greater than start.", code="INVALID_HIGHLIGHT"
|
|
64
|
+
)
|
|
65
|
+
if self.label is not None:
|
|
66
|
+
if not isinstance(self.label, str) or not self.label.strip():
|
|
67
|
+
raise ConfigurationError(
|
|
68
|
+
"Highlight label must be non-empty or None.", code="INVALID_HIGHLIGHT"
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
validate_xml_text(self.label, name="highlight label")
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise ConfigurationError(
|
|
74
|
+
"Highlight label contains a character forbidden by XML.",
|
|
75
|
+
code="INVALID_HIGHLIGHT",
|
|
76
|
+
) from exc
|
|
77
|
+
_color(self.color, "highlight color")
|
|
78
|
+
if self.foreground is not None:
|
|
79
|
+
_color(self.foreground, "highlight foreground")
|
|
80
|
+
if isinstance(self.priority, bool) or not isinstance(self.priority, int):
|
|
81
|
+
raise ConfigurationError(
|
|
82
|
+
"Highlight priority must be an integer.", code="INVALID_HIGHLIGHT"
|
|
83
|
+
)
|
|
84
|
+
if (
|
|
85
|
+
isinstance(self.opacity, bool)
|
|
86
|
+
or not isinstance(self.opacity, (int, float))
|
|
87
|
+
or not math.isfinite(self.opacity)
|
|
88
|
+
or not 0.0 <= self.opacity <= 1.0
|
|
89
|
+
):
|
|
90
|
+
raise ConfigurationError(
|
|
91
|
+
"Highlight opacity must be finite and between zero and one.",
|
|
92
|
+
code="INVALID_HIGHLIGHT",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class _Cell:
|
|
98
|
+
text: str
|
|
99
|
+
columns: int
|
|
100
|
+
symbol_index: int | None
|
|
101
|
+
coordinate_start: int | None
|
|
102
|
+
coordinate_end: int | None
|
|
103
|
+
is_gap: bool
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class _Row:
|
|
108
|
+
cells: tuple[_Cell, ...]
|
|
109
|
+
columns: int
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _coerce_input(value: DNA | DNASequence | DNARecord) -> tuple[DNASequence, str | None, int]:
|
|
113
|
+
if isinstance(value, (DNA, DNARecord, DNASequence)):
|
|
114
|
+
sequence, record = resolve_single_dna(value)
|
|
115
|
+
return (
|
|
116
|
+
sequence,
|
|
117
|
+
None if record is None else record.id,
|
|
118
|
+
0 if record is None else len(record.features),
|
|
119
|
+
)
|
|
120
|
+
raise TypeError("plot_sequence() accepts DNA, DNASequence, or DNARecord.")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _gap_label(gap: Gap) -> str:
|
|
124
|
+
return "[… bp]" if gap.length is None else f"[{gap.length} bp]"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _cells(sequence: DNASequence, display_limit: int) -> tuple[_Cell, ...]:
|
|
128
|
+
cells: list[_Cell] = []
|
|
129
|
+
symbol_index = 0
|
|
130
|
+
coordinate: int | None = 0
|
|
131
|
+
for part in sequence.parts:
|
|
132
|
+
if isinstance(part, Gap):
|
|
133
|
+
if symbol_index >= display_limit and sequence.symbol_length > display_limit:
|
|
134
|
+
break
|
|
135
|
+
label = _gap_label(part)
|
|
136
|
+
coordinate_end = (
|
|
137
|
+
coordinate + part.length
|
|
138
|
+
if coordinate is not None and part.length is not None
|
|
139
|
+
else None
|
|
140
|
+
)
|
|
141
|
+
cells.append(
|
|
142
|
+
_Cell(
|
|
143
|
+
label,
|
|
144
|
+
max(4, len(label) + 1),
|
|
145
|
+
None,
|
|
146
|
+
coordinate,
|
|
147
|
+
coordinate_end,
|
|
148
|
+
True,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
coordinate = coordinate_end
|
|
152
|
+
continue
|
|
153
|
+
for symbol in part:
|
|
154
|
+
if symbol_index >= display_limit:
|
|
155
|
+
return tuple(cells)
|
|
156
|
+
coordinate_end = None if coordinate is None else coordinate + 1
|
|
157
|
+
cells.append(_Cell(symbol, 1, symbol_index, coordinate, coordinate_end, False))
|
|
158
|
+
symbol_index += 1
|
|
159
|
+
coordinate = coordinate_end
|
|
160
|
+
return tuple(cells)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _rows(cells: tuple[_Cell, ...], columns_per_line: int) -> tuple[_Row, ...]:
|
|
164
|
+
rows: list[_Row] = []
|
|
165
|
+
current: list[_Cell] = []
|
|
166
|
+
columns = 0
|
|
167
|
+
for cell in cells:
|
|
168
|
+
if current and columns + cell.columns > columns_per_line:
|
|
169
|
+
rows.append(_Row(tuple(current), columns))
|
|
170
|
+
current = []
|
|
171
|
+
columns = 0
|
|
172
|
+
current.append(cell)
|
|
173
|
+
columns += cell.columns
|
|
174
|
+
if current:
|
|
175
|
+
rows.append(_Row(tuple(current), columns))
|
|
176
|
+
return tuple(rows)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _active_highlight(
|
|
180
|
+
symbol_index: int, highlights: tuple[Highlight, ...]
|
|
181
|
+
) -> tuple[int, Highlight] | None:
|
|
182
|
+
candidates = tuple(
|
|
183
|
+
(index, highlight)
|
|
184
|
+
for index, highlight in enumerate(highlights)
|
|
185
|
+
if highlight.start <= symbol_index < highlight.end
|
|
186
|
+
)
|
|
187
|
+
if not candidates:
|
|
188
|
+
return None
|
|
189
|
+
return max(candidates, key=lambda item: (item[1].priority, item[0]))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _coordinate_label(value: int | None, offset: int) -> str:
|
|
193
|
+
return "?" if value is None else str(value + offset)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def plot_sequence(
|
|
197
|
+
value: DNA | DNASequence | DNARecord,
|
|
198
|
+
*,
|
|
199
|
+
highlights: Iterable[Highlight] = (),
|
|
200
|
+
config: SequencePlotConfig | None = None,
|
|
201
|
+
) -> SVGArtifact:
|
|
202
|
+
"""Render symbols, explicit gaps, coordinates, and optional highlights.
|
|
203
|
+
|
|
204
|
+
Highlight coordinates always refer to ``DNASequence.symbols`` and therefore
|
|
205
|
+
exclude :class:`~dnakit.core.Gap` spans. Record features are deliberately
|
|
206
|
+
not rendered implicitly; linear feature maps remain an advanced module.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
sequence, sequence_id, feature_count = _coerce_input(value)
|
|
210
|
+
resolved = SequencePlotConfig() if config is None else config
|
|
211
|
+
if not isinstance(resolved, SequencePlotConfig):
|
|
212
|
+
raise TypeError("config must be SequencePlotConfig or None.")
|
|
213
|
+
highlight_tuple = tuple(highlights)
|
|
214
|
+
if any(not isinstance(item, Highlight) for item in highlight_tuple):
|
|
215
|
+
raise TypeError("highlights must contain only Highlight objects.")
|
|
216
|
+
if any(item.end > sequence.symbol_length for item in highlight_tuple):
|
|
217
|
+
raise ConfigurationError(
|
|
218
|
+
"A highlight exceeds sequence symbol_length.",
|
|
219
|
+
code="HIGHLIGHT_OUT_OF_RANGE",
|
|
220
|
+
context={"symbol_length": sequence.symbol_length},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
224
|
+
if len(gaps) > resolved.max_gaps:
|
|
225
|
+
raise ConfigurationError(
|
|
226
|
+
"Sequence plot exceeds max_gaps.",
|
|
227
|
+
code="VISUALIZATION_SIZE_LIMIT",
|
|
228
|
+
context={"gap_count": len(gaps), "max_gaps": resolved.max_gaps},
|
|
229
|
+
)
|
|
230
|
+
truncated = sequence.symbol_length > resolved.max_symbols
|
|
231
|
+
if truncated and resolved.limit_policy is LimitPolicy.ERROR:
|
|
232
|
+
raise ConfigurationError(
|
|
233
|
+
"Sequence plot exceeds max_symbols.",
|
|
234
|
+
code="VISUALIZATION_SIZE_LIMIT",
|
|
235
|
+
context={
|
|
236
|
+
"symbol_length": sequence.symbol_length,
|
|
237
|
+
"max_symbols": resolved.max_symbols,
|
|
238
|
+
},
|
|
239
|
+
hint="Raise max_symbols or explicitly choose limit_policy='truncate'.",
|
|
240
|
+
)
|
|
241
|
+
display_limit = min(sequence.symbol_length, resolved.max_symbols)
|
|
242
|
+
cells = _cells(sequence, display_limit)
|
|
243
|
+
rows = _rows(cells, resolved.bases_per_line)
|
|
244
|
+
|
|
245
|
+
issues: list[Issue] = []
|
|
246
|
+
if truncated:
|
|
247
|
+
issues.append(
|
|
248
|
+
Issue(
|
|
249
|
+
"VIZ_SEQUENCE_TRUNCATED",
|
|
250
|
+
IssueSeverity.WARNING,
|
|
251
|
+
"The sequence text plot was explicitly truncated.",
|
|
252
|
+
details={
|
|
253
|
+
"symbol_length": sequence.symbol_length,
|
|
254
|
+
"displayed_symbols": display_limit,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
clipped_highlights = sum(item.end > display_limit for item in highlight_tuple)
|
|
259
|
+
if clipped_highlights:
|
|
260
|
+
issues.append(
|
|
261
|
+
Issue(
|
|
262
|
+
"VIZ_HIGHLIGHT_CLIPPED",
|
|
263
|
+
IssueSeverity.WARNING,
|
|
264
|
+
"At least one highlight extends beyond the displayed sequence prefix.",
|
|
265
|
+
details={"count": clipped_highlights},
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
269
|
+
issues.append(
|
|
270
|
+
Issue(
|
|
271
|
+
"VIZ_CIRCULAR_LINEARIZED",
|
|
272
|
+
IssueSeverity.INFO,
|
|
273
|
+
"The circular sequence is shown as a linear text view.",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
title = resolved.title or (
|
|
278
|
+
f"DNA sequence: {sequence_id}" if sequence_id is not None else "DNA sequence"
|
|
279
|
+
)
|
|
280
|
+
coordinate_gutter = 72 if resolved.show_coordinates else 0
|
|
281
|
+
max_columns = max((row.columns for row in rows), default=20)
|
|
282
|
+
width = max(
|
|
283
|
+
420,
|
|
284
|
+
resolved.margin * 2
|
|
285
|
+
+ coordinate_gutter * (2 if resolved.show_coordinates else 0)
|
|
286
|
+
+ max_columns * resolved.cell_width,
|
|
287
|
+
)
|
|
288
|
+
row_height = resolved.line_height + (resolved.font_size + 8 if resolved.show_complement else 0)
|
|
289
|
+
title_height = resolved.font_size + 32
|
|
290
|
+
height = max(
|
|
291
|
+
120,
|
|
292
|
+
resolved.margin * 2 + title_height + max(1, len(rows)) * row_height,
|
|
293
|
+
)
|
|
294
|
+
metadata = {
|
|
295
|
+
"coordinate_space": "symbol-highlights; sequence-span-labels",
|
|
296
|
+
"display_start_coordinate": resolved.start_coordinate,
|
|
297
|
+
"displayed_symbols": display_limit,
|
|
298
|
+
"feature_count_not_rendered": feature_count,
|
|
299
|
+
"gap_count": len(gaps),
|
|
300
|
+
"highlight_count": len(highlight_tuple),
|
|
301
|
+
"sequence_id": sequence_id,
|
|
302
|
+
"symbol_length": sequence.symbol_length,
|
|
303
|
+
"topology": sequence.topology.value,
|
|
304
|
+
"truncated": truncated,
|
|
305
|
+
"unknown_gap_count": sum(gap.length is None for gap in gaps),
|
|
306
|
+
}
|
|
307
|
+
builder = SVGBuilder(
|
|
308
|
+
width,
|
|
309
|
+
height,
|
|
310
|
+
title=title,
|
|
311
|
+
description="Gap-aware DNA symbol text plot in input order.",
|
|
312
|
+
kind="sequence",
|
|
313
|
+
theme=resolved.theme,
|
|
314
|
+
metadata=metadata,
|
|
315
|
+
)
|
|
316
|
+
builder.text(
|
|
317
|
+
resolved.margin,
|
|
318
|
+
resolved.margin + resolved.font_size,
|
|
319
|
+
title,
|
|
320
|
+
fill=resolved.theme.foreground,
|
|
321
|
+
font_family=resolved.theme.font_family,
|
|
322
|
+
font_size=resolved.font_size + 2,
|
|
323
|
+
font_weight="bold",
|
|
324
|
+
class_="plot-title",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if not rows:
|
|
328
|
+
builder.text(
|
|
329
|
+
width / 2,
|
|
330
|
+
height / 2,
|
|
331
|
+
"Empty sequence",
|
|
332
|
+
fill=resolved.theme.muted,
|
|
333
|
+
font_family=resolved.theme.font_family,
|
|
334
|
+
font_size=resolved.font_size,
|
|
335
|
+
text_anchor="middle",
|
|
336
|
+
class_="empty-state",
|
|
337
|
+
)
|
|
338
|
+
return builder.artifact(issues=issues)
|
|
339
|
+
|
|
340
|
+
base_colors = {
|
|
341
|
+
"A": "#16a34a",
|
|
342
|
+
"C": "#2563eb",
|
|
343
|
+
"G": "#d97706",
|
|
344
|
+
"T": "#dc2626",
|
|
345
|
+
}
|
|
346
|
+
for row_index, row in enumerate(rows):
|
|
347
|
+
y = resolved.margin + title_height + row_index * row_height + resolved.font_size
|
|
348
|
+
x = resolved.margin + coordinate_gutter
|
|
349
|
+
if resolved.show_coordinates:
|
|
350
|
+
builder.text(
|
|
351
|
+
resolved.margin,
|
|
352
|
+
y,
|
|
353
|
+
_coordinate_label(row.cells[0].coordinate_start, resolved.start_coordinate),
|
|
354
|
+
fill=resolved.theme.muted,
|
|
355
|
+
font_family=resolved.theme.font_family,
|
|
356
|
+
font_size=max(9, resolved.font_size - 2),
|
|
357
|
+
class_="coordinate coordinate-start",
|
|
358
|
+
)
|
|
359
|
+
for cell in row.cells:
|
|
360
|
+
cell_width = cell.columns * resolved.cell_width
|
|
361
|
+
if cell.is_gap:
|
|
362
|
+
builder.rect(
|
|
363
|
+
x,
|
|
364
|
+
y - resolved.font_size,
|
|
365
|
+
cell_width - 2,
|
|
366
|
+
resolved.font_size + 6,
|
|
367
|
+
fill=resolved.theme.missing,
|
|
368
|
+
stroke=resolved.theme.grid,
|
|
369
|
+
stroke_dasharray="3 2",
|
|
370
|
+
rx=3,
|
|
371
|
+
class_="gap",
|
|
372
|
+
data_coordinate_start=_coordinate_label(
|
|
373
|
+
cell.coordinate_start, resolved.start_coordinate
|
|
374
|
+
),
|
|
375
|
+
data_coordinate_end=_coordinate_label(
|
|
376
|
+
cell.coordinate_end, resolved.start_coordinate
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
builder.text(
|
|
380
|
+
x + cell_width / 2,
|
|
381
|
+
y,
|
|
382
|
+
cell.text,
|
|
383
|
+
fill=resolved.theme.muted,
|
|
384
|
+
font_family=resolved.theme.font_family,
|
|
385
|
+
font_size=max(9, resolved.font_size - 2),
|
|
386
|
+
text_anchor="middle",
|
|
387
|
+
class_="gap-label",
|
|
388
|
+
)
|
|
389
|
+
else:
|
|
390
|
+
assert cell.symbol_index is not None
|
|
391
|
+
active = _active_highlight(cell.symbol_index, highlight_tuple)
|
|
392
|
+
foreground = base_colors.get(cell.text, resolved.theme.muted)
|
|
393
|
+
if active is not None:
|
|
394
|
+
highlight_index, highlight = active
|
|
395
|
+
builder.rect(
|
|
396
|
+
x,
|
|
397
|
+
y - resolved.font_size,
|
|
398
|
+
cell_width,
|
|
399
|
+
resolved.font_size + 6,
|
|
400
|
+
fill=highlight.color,
|
|
401
|
+
fill_opacity=highlight.opacity,
|
|
402
|
+
class_="highlight",
|
|
403
|
+
data_highlight_index=highlight_index,
|
|
404
|
+
data_priority=highlight.priority,
|
|
405
|
+
)
|
|
406
|
+
if highlight.foreground is not None:
|
|
407
|
+
foreground = highlight.foreground
|
|
408
|
+
if highlight.label is not None and cell.symbol_index == highlight.start:
|
|
409
|
+
builder.text(
|
|
410
|
+
x,
|
|
411
|
+
y - resolved.font_size - 3,
|
|
412
|
+
highlight.label,
|
|
413
|
+
fill=resolved.theme.foreground,
|
|
414
|
+
font_family=resolved.theme.font_family,
|
|
415
|
+
font_size=max(8, resolved.font_size - 4),
|
|
416
|
+
class_="highlight-label",
|
|
417
|
+
)
|
|
418
|
+
builder.text(
|
|
419
|
+
x + cell_width / 2,
|
|
420
|
+
y,
|
|
421
|
+
cell.text,
|
|
422
|
+
fill=foreground,
|
|
423
|
+
font_family=resolved.theme.font_family,
|
|
424
|
+
font_size=resolved.font_size,
|
|
425
|
+
font_weight="bold",
|
|
426
|
+
text_anchor="middle",
|
|
427
|
+
class_="base",
|
|
428
|
+
data_symbol_index=cell.symbol_index,
|
|
429
|
+
data_coordinate=_coordinate_label(
|
|
430
|
+
cell.coordinate_start, resolved.start_coordinate
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
if resolved.show_complement:
|
|
434
|
+
builder.text(
|
|
435
|
+
x + cell_width / 2,
|
|
436
|
+
y + resolved.font_size + 8,
|
|
437
|
+
cell.text.translate(_COMPLEMENT),
|
|
438
|
+
fill=foreground,
|
|
439
|
+
font_family=resolved.theme.font_family,
|
|
440
|
+
font_size=resolved.font_size,
|
|
441
|
+
text_anchor="middle",
|
|
442
|
+
class_="complement",
|
|
443
|
+
data_symbol_index=cell.symbol_index,
|
|
444
|
+
)
|
|
445
|
+
x += cell_width
|
|
446
|
+
if resolved.show_coordinates:
|
|
447
|
+
builder.text(
|
|
448
|
+
x + 8,
|
|
449
|
+
y,
|
|
450
|
+
_coordinate_label(row.cells[-1].coordinate_end, resolved.start_coordinate),
|
|
451
|
+
fill=resolved.theme.muted,
|
|
452
|
+
font_family=resolved.theme.font_family,
|
|
453
|
+
font_size=max(9, resolved.font_size - 2),
|
|
454
|
+
class_="coordinate coordinate-end",
|
|
455
|
+
)
|
|
456
|
+
return builder.artifact(issues=issues)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
__all__ = ["Highlight", "plot_sequence"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Strict local workflows, manifests, and auditable execution helpers."""
|
|
2
|
+
|
|
3
|
+
from dnakit.workflows.manifest import (
|
|
4
|
+
RunManifestBuilder,
|
|
5
|
+
artifact_from_path,
|
|
6
|
+
load_manifest,
|
|
7
|
+
save_manifest,
|
|
8
|
+
)
|
|
9
|
+
from dnakit.workflows.runner import (
|
|
10
|
+
ProgressCallback,
|
|
11
|
+
WorkflowProgress,
|
|
12
|
+
WorkflowRunResult,
|
|
13
|
+
WorkflowStepResult,
|
|
14
|
+
run_workflow,
|
|
15
|
+
)
|
|
16
|
+
from dnakit.workflows.schema import (
|
|
17
|
+
LoadedWorkflow,
|
|
18
|
+
WorkflowErrorPolicy,
|
|
19
|
+
WorkflowInput,
|
|
20
|
+
WorkflowLimits,
|
|
21
|
+
WorkflowOperation,
|
|
22
|
+
WorkflowSpec,
|
|
23
|
+
WorkflowStep,
|
|
24
|
+
load_workflow,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"LoadedWorkflow",
|
|
29
|
+
"ProgressCallback",
|
|
30
|
+
"RunManifestBuilder",
|
|
31
|
+
"WorkflowErrorPolicy",
|
|
32
|
+
"WorkflowInput",
|
|
33
|
+
"WorkflowLimits",
|
|
34
|
+
"WorkflowOperation",
|
|
35
|
+
"WorkflowProgress",
|
|
36
|
+
"WorkflowRunResult",
|
|
37
|
+
"WorkflowSpec",
|
|
38
|
+
"WorkflowStep",
|
|
39
|
+
"WorkflowStepResult",
|
|
40
|
+
"artifact_from_path",
|
|
41
|
+
"load_manifest",
|
|
42
|
+
"load_workflow",
|
|
43
|
+
"run_workflow",
|
|
44
|
+
"save_manifest",
|
|
45
|
+
]
|