plot-io 0.12.0__py3-none-win_arm64.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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Reading genome data from readers, without importing any reader library: the calls
|
|
2
|
+
plot_io makes, as the protocols of :mod:`plot_io.genome.readers` describe them.
|
|
3
|
+
|
|
4
|
+
With ``bin_size``, a signal row grows to the grid of the size and its bounds come from
|
|
5
|
+
``resolve_regions`` (same arguments) when the reader has it; the bins are then cut to the
|
|
6
|
+
region. A contact read checks ``normalization`` against the reader's ``normalizations``
|
|
7
|
+
first. Results are read as buffers (numpy arrays included) without importing numpy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from array import array
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from vec_io.arrays import doubles
|
|
16
|
+
|
|
17
|
+
from .. import kernels
|
|
18
|
+
from .regions import Region
|
|
19
|
+
|
|
20
|
+
__all__ = ["kind_of", "check_reader", "read_signal", "read_entries", "read_models",
|
|
21
|
+
"read_contacts", "bin_size_for", "entry_columns", "pair_columns", "read_pairs", "even_edges", "matrix", "ints",
|
|
22
|
+
"BED_COLUMNS", "BEDPE_COLUMNS"]
|
|
23
|
+
|
|
24
|
+
#: The BED columns the interval tracks use, by their names in autoSql and in BED files.
|
|
25
|
+
BED_COLUMNS = ("chr", "start", "end", "name", "score", "strand", "thickStart", "thickEnd",
|
|
26
|
+
"itemRgb", "blockCount", "blockSizes", "blockStarts")
|
|
27
|
+
#: The BEDPE columns links use, in BEDPE order.
|
|
28
|
+
BEDPE_COLUMNS = ("chr1", "start1", "end1", "chr2", "start2", "end2", "name", "score")
|
|
29
|
+
_PAIR_ALIASES = {"chrom1": "chr1", "chromosome1": "chr1", "chr": "chr1", "chrom": "chr1",
|
|
30
|
+
"chromosome": "chr1", "start": "start1", "end": "end1", "chrom2": "chr2",
|
|
31
|
+
"chromosome2": "chr2"}
|
|
32
|
+
_TYPES = {"bigwig": "signal", "bigbed": "intervals", "bed": "intervals", "peaks": "intervals",
|
|
33
|
+
"hic": "contacts", "bam": "alignments", "cram": "alignments", "gtf": "annotation",
|
|
34
|
+
"gff3": "annotation"}
|
|
35
|
+
_ALIASES = {"chrom": "chr", "chromosome": "chr", "chromStart": "start", "chromEnd": "end",
|
|
36
|
+
"chrom_start": "start", "chrom_end": "end", "reserved": "itemRgb",
|
|
37
|
+
"item_rgb": "itemRgb",
|
|
38
|
+
"thick_start": "thickStart", "thick_end": "thickEnd",
|
|
39
|
+
"block_count": "blockCount", "block_sizes": "blockSizes",
|
|
40
|
+
"block_starts": "blockStarts", "chromStarts": "blockStarts",
|
|
41
|
+
"chrom_starts": "blockStarts", "exonCount": "blockCount",
|
|
42
|
+
"exonSizes": "blockSizes", "exonStarts": "blockStarts"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def kind_of(reader: Any) -> str | None:
|
|
46
|
+
"""``"signal"``, ``"intervals"``, ``"annotation"``, ``"contacts"``, ``"alignments"`` or
|
|
47
|
+
``None`` (not a reader)."""
|
|
48
|
+
t = getattr(reader, "type", None)
|
|
49
|
+
if isinstance(t, str) and t.lower() in _TYPES:
|
|
50
|
+
return _TYPES[t.lower()]
|
|
51
|
+
if hasattr(reader, "read_sparse_values") or hasattr(reader, "bin_sizes"):
|
|
52
|
+
return "contacts"
|
|
53
|
+
if hasattr(reader, "tss"):
|
|
54
|
+
return "annotation"
|
|
55
|
+
if hasattr(reader, "read_entries"):
|
|
56
|
+
return "intervals"
|
|
57
|
+
if hasattr(reader, "read_values"):
|
|
58
|
+
return "signal"
|
|
59
|
+
if hasattr(reader, "read_alignments"):
|
|
60
|
+
return "alignments"
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_reader(reader: Any, want: tuple[str, ...], what: str) -> str:
|
|
65
|
+
"""The kind of ``reader``, which must be one of ``want``."""
|
|
66
|
+
kind = kind_of(reader)
|
|
67
|
+
if kind == "alignments":
|
|
68
|
+
raise TypeError(f"{what} does not read alignments: pile the reads up into a bigWig "
|
|
69
|
+
f"first (gwseq_io.pileup_reads)")
|
|
70
|
+
if kind == "annotation" and kind not in want:
|
|
71
|
+
raise TypeError(f"{what} does not read gene annotations: draw a GTF or GFF3 reader "
|
|
72
|
+
f"with gtf()")
|
|
73
|
+
if kind not in want:
|
|
74
|
+
raise TypeError(f"{what} needs a reader with "
|
|
75
|
+
+ {"signal": "read_values (a bigWig)",
|
|
76
|
+
"intervals": "read_entries (a bigBed, BED or peaks file)",
|
|
77
|
+
"annotation": "read_entries(model=) and tss (a GTF or GFF3 file)",
|
|
78
|
+
"contacts": "bin_sizes and read_values (a Hi-C file)"}[want[0]]
|
|
79
|
+
+ f", got {type(reader).__name__}")
|
|
80
|
+
return kind
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _first(v: Any) -> Any:
|
|
84
|
+
"""Row 0 of a result with one row per region."""
|
|
85
|
+
rows = None
|
|
86
|
+
shape = getattr(v, "shape", None)
|
|
87
|
+
if shape is not None and len(shape) == 2:
|
|
88
|
+
rows = shape[0]
|
|
89
|
+
if rows != 1:
|
|
90
|
+
raise ValueError(f"expected one row of values, got {rows}")
|
|
91
|
+
return v[0]
|
|
92
|
+
if isinstance(v, Sequence) and v and isinstance(v[0], (Sequence, array)) \
|
|
93
|
+
and not isinstance(v[0], str):
|
|
94
|
+
return v[0]
|
|
95
|
+
return v
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _bounds(reader: Any, region: Region, kw: dict[str, Any], hic: bool) -> tuple[int, int]:
|
|
99
|
+
"""The start and end of the row a binned read gives, from ``resolve_regions``."""
|
|
100
|
+
target = (region.chr, region.start, region.end)
|
|
101
|
+
r = reader.resolve_regions(target if hic else [target], **kw)
|
|
102
|
+
if hic:
|
|
103
|
+
r = r["rows"]
|
|
104
|
+
return int(r["start"][0]), int(r["end"][0])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def even_edges(start: float, end: float, n: int) -> array:
|
|
108
|
+
"""``n + 1`` evenly spaced edges from ``start`` to ``end`` (no edges for no bins)."""
|
|
109
|
+
if n <= 0:
|
|
110
|
+
return array("d")
|
|
111
|
+
span = end - start
|
|
112
|
+
edges = array("d", [start + span * i / n for i in range(n + 1)])
|
|
113
|
+
edges[-1] = float(end)
|
|
114
|
+
return edges
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def matrix(m: Any) -> tuple[int, int, int, array]:
|
|
118
|
+
"""``(height, width, k, values)`` of a 2-D buffer or a sequence of rows."""
|
|
119
|
+
g = kernels.grid(m)
|
|
120
|
+
if g is not None:
|
|
121
|
+
return g
|
|
122
|
+
rows = [list(r) for r in m]
|
|
123
|
+
return (len(rows), len(rows[0]) if rows else 0, 1,
|
|
124
|
+
doubles([v for r in rows for v in r]))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def ints(v: Any) -> list[int]:
|
|
128
|
+
"""Whole numbers of a column or a BED list (``"10,20,"``): ``[]`` for ``None``."""
|
|
129
|
+
if v is None:
|
|
130
|
+
return []
|
|
131
|
+
if isinstance(v, str):
|
|
132
|
+
return [int(p) for p in v.replace(" ", "").split(",") if p]
|
|
133
|
+
if isinstance(v, (int, float)):
|
|
134
|
+
return [int(v)]
|
|
135
|
+
return [int(x) for x in (v.tolist() if hasattr(v, "tolist") else v)]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_signal(reader: Any, region: Region, bins: int | None, bin_size: int | None,
|
|
139
|
+
bin_mode: str, def_value: float) -> tuple[array, array]:
|
|
140
|
+
"""``(edges, values)``: n + 1 bin edges (bp) and n values of ``region``, in ``bins``
|
|
141
|
+
bins or bins of ``bin_size``."""
|
|
142
|
+
target = (region.chr, region.start, region.end)
|
|
143
|
+
if bin_size is None:
|
|
144
|
+
n = max(1, int(bins or 1))
|
|
145
|
+
v = reader.read_values([target], bin_count=n, bin_mode=bin_mode, def_value=def_value,
|
|
146
|
+
zoom="auto")
|
|
147
|
+
values = doubles(_first(v))
|
|
148
|
+
return even_edges(region.start, region.end, len(values)), values
|
|
149
|
+
kw: dict[str, Any] = {"bin_size": int(bin_size)}
|
|
150
|
+
if hasattr(reader, "resolve_regions"):
|
|
151
|
+
kw["snap_to_grid"] = "expand"
|
|
152
|
+
start, end = _bounds(reader, region, dict(kw), False)
|
|
153
|
+
else:
|
|
154
|
+
kw.update(snap_to_grid=False, full_bin=True)
|
|
155
|
+
start, end = region.start, region.end
|
|
156
|
+
v = reader.read_values([target], bin_mode=bin_mode, def_value=def_value, zoom="auto",
|
|
157
|
+
**kw)
|
|
158
|
+
values = doubles(_first(v))
|
|
159
|
+
n = len(values)
|
|
160
|
+
edges = array("d", [float(start + i * bin_size) for i in range(n + 1)])
|
|
161
|
+
edges[-1] = float(min(end, start + n * bin_size)) # a last partial bin ends there
|
|
162
|
+
lo, hi = float(region.start), float(region.end) # bins cut to the region
|
|
163
|
+
return array("d", [min(max(e, lo), hi) for e in edges]), values
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_LOWER = {k.lower(): v for k, v in _ALIASES.items()}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _column(cols: Mapping[str, Any], name: str) -> Any:
|
|
170
|
+
"""The column ``name`` or an alias of it; then the same without case (``Start``)."""
|
|
171
|
+
for k, v in cols.items():
|
|
172
|
+
if k == name or _ALIASES.get(k) == name:
|
|
173
|
+
return v
|
|
174
|
+
low = name.lower()
|
|
175
|
+
for k, v in cols.items():
|
|
176
|
+
if isinstance(k, str) and (k.lower() == low or _LOWER.get(k.lower()) == name):
|
|
177
|
+
return v
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _position(v: Any, i: int, name: str) -> int:
|
|
182
|
+
try:
|
|
183
|
+
return int(v)
|
|
184
|
+
except (TypeError, ValueError, OverflowError):
|
|
185
|
+
raise ValueError(f"interval {i + 1}: {name} {v!r} is not a position") from None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _score(v: Any) -> float | None:
|
|
189
|
+
"""A BED score as a number; ``None`` when missing or not a number (``"."``)."""
|
|
190
|
+
if v is None or isinstance(v, bool):
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
f = float(v)
|
|
194
|
+
except (TypeError, ValueError):
|
|
195
|
+
return None
|
|
196
|
+
return f if math.isfinite(f) else None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def entry_columns(cols: Any) -> dict[str, list[Any]]:
|
|
200
|
+
"""The BED columns of a dict of columns, a DataFrame, or rows (dicts, or tuples in BED
|
|
201
|
+
order, ``chr`` first): ``chr`` as strings, ``start`` and ``end`` as ints, ``score`` as
|
|
202
|
+
numbers (``None`` where missing or not a number), ``blockSizes`` and ``blockStarts`` as
|
|
203
|
+
lists of ints, the others as given (missing ones left out). Reversed intervals and
|
|
204
|
+
blocks not matching ``blockCount`` (or several blocks without ``blockStarts``) are refused."""
|
|
205
|
+
if hasattr(cols, "to_dict") and hasattr(cols, "columns"): # a DataFrame
|
|
206
|
+
cols = {str(c): list(cols[c]) for c in cols.columns}
|
|
207
|
+
elif not isinstance(cols, Mapping):
|
|
208
|
+
rows = list(cols)
|
|
209
|
+
if not rows:
|
|
210
|
+
return {"start": [], "end": []}
|
|
211
|
+
if isinstance(rows[0], Mapping) or hasattr(rows[0], "keys"):
|
|
212
|
+
keys = list(rows[0].keys())
|
|
213
|
+
cols = {k: [r[k] for r in rows] for k in keys}
|
|
214
|
+
else:
|
|
215
|
+
order = BED_COLUMNS
|
|
216
|
+
width = max(len(r) for r in rows)
|
|
217
|
+
cols = {order[i]: [r[i] if i < len(r) else None for r in rows]
|
|
218
|
+
for i in range(min(width, len(order)))}
|
|
219
|
+
out: dict[str, list[Any]] = {}
|
|
220
|
+
for name in BED_COLUMNS:
|
|
221
|
+
v = _column(cols, name)
|
|
222
|
+
if v is None:
|
|
223
|
+
continue
|
|
224
|
+
out[name] = list(v.tolist() if hasattr(v, "tolist") else v)
|
|
225
|
+
if "start" not in out or "end" not in out:
|
|
226
|
+
raise ValueError("intervals need start and end columns")
|
|
227
|
+
if len(out["start"]) != len(out["end"]):
|
|
228
|
+
raise ValueError("start and end have different lengths")
|
|
229
|
+
if "chr" in out:
|
|
230
|
+
out["chr"] = [str(c) for c in out["chr"]]
|
|
231
|
+
out["start"] = [_position(s, i, "start") for i, s in enumerate(out["start"])]
|
|
232
|
+
out["end"] = [_position(e, i, "end") for i, e in enumerate(out["end"])]
|
|
233
|
+
for i, (s, e) in enumerate(zip(out["start"], out["end"])):
|
|
234
|
+
if e < s:
|
|
235
|
+
raise ValueError(f"interval {i + 1} ends before it starts ({s}-{e})")
|
|
236
|
+
if "score" in out:
|
|
237
|
+
out["score"] = [_score(v) for v in out["score"]]
|
|
238
|
+
for name in ("blockSizes", "blockStarts"):
|
|
239
|
+
if name in out:
|
|
240
|
+
out[name] = [None if v is None else ints(v) for v in out[name]]
|
|
241
|
+
counts = out.get("blockCount")
|
|
242
|
+
if counts is not None:
|
|
243
|
+
for i, c in enumerate(counts):
|
|
244
|
+
if c is None:
|
|
245
|
+
continue
|
|
246
|
+
starts = out.get("blockStarts")
|
|
247
|
+
if int(c) > 1 and (starts is None or starts[i] is None):
|
|
248
|
+
raise ValueError(f"interval {i + 1}: blockCount {int(c)} without blockStarts")
|
|
249
|
+
for name in ("blockSizes", "blockStarts"):
|
|
250
|
+
got = out.get(name)
|
|
251
|
+
if got is not None and got[i] is not None and len(got[i]) != int(c):
|
|
252
|
+
raise ValueError(f"interval {i + 1}: blockCount {int(c)} with "
|
|
253
|
+
f"{len(got[i])} {name}")
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def read_entries(reader: Any, region: Region) -> dict[str, list[Any]]:
|
|
258
|
+
"""The entries of ``region``, as BED columns (see :func:`entry_columns`)."""
|
|
259
|
+
sql = getattr(reader, "auto_sql", None)
|
|
260
|
+
kw: dict[str, Any] = {}
|
|
261
|
+
if isinstance(sql, Mapping) and sql:
|
|
262
|
+
names = list(sql)
|
|
263
|
+
wanted = [i for i, k in enumerate(names) if (_ALIASES.get(k, k) in BED_COLUMNS)]
|
|
264
|
+
kw["columns"] = max(3, max(wanted, default=2) + 1)
|
|
265
|
+
return entry_columns(reader.read_entries([(region.chr, region.start, region.end)], **kw))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def pair_columns(cols: Any) -> dict[str, list[Any]]:
|
|
269
|
+
"""The BEDPE columns of a dict of columns, a DataFrame, or rows (dicts, or tuples in
|
|
270
|
+
BEDPE order): chromosomes as strings, positions as ints, ``score`` as numbers
|
|
271
|
+
(``None`` where missing); ``name`` and ``score`` optional. Strand columns are left
|
|
272
|
+
out. Reversed anchors are refused."""
|
|
273
|
+
if hasattr(cols, "to_dict") and hasattr(cols, "columns"): # a DataFrame
|
|
274
|
+
cols = {str(c): list(cols[c]) for c in cols.columns}
|
|
275
|
+
elif not isinstance(cols, Mapping):
|
|
276
|
+
rows = list(cols)
|
|
277
|
+
if not rows:
|
|
278
|
+
return {k: [] for k in BEDPE_COLUMNS[:6]}
|
|
279
|
+
if isinstance(rows[0], Mapping) or hasattr(rows[0], "keys"):
|
|
280
|
+
cols = {k: [r[k] for r in rows] for k in rows[0].keys()}
|
|
281
|
+
else:
|
|
282
|
+
width = max(len(r) for r in rows)
|
|
283
|
+
if width < 6:
|
|
284
|
+
raise ValueError(f"links need 6 BEDPE columns (chr1 start1 end1 chr2 start2 "
|
|
285
|
+
f"end2), got {width}")
|
|
286
|
+
cols = {BEDPE_COLUMNS[i]: [r[i] if i < len(r) else None for r in rows]
|
|
287
|
+
for i in range(min(width, len(BEDPE_COLUMNS)))}
|
|
288
|
+
named: dict[str, Any] = {}
|
|
289
|
+
for k, v in cols.items():
|
|
290
|
+
key = str(k)
|
|
291
|
+
name = key if key in BEDPE_COLUMNS else _PAIR_ALIASES.get(key) \
|
|
292
|
+
or _PAIR_ALIASES.get(key.lower()) or key.lower()
|
|
293
|
+
if name in BEDPE_COLUMNS and name not in named:
|
|
294
|
+
named[name] = v
|
|
295
|
+
missing = [k for k in BEDPE_COLUMNS[:6] if k not in named]
|
|
296
|
+
if missing:
|
|
297
|
+
raise ValueError(f"links need the BEDPE columns {', '.join(missing)}")
|
|
298
|
+
out = {k: list(v.tolist() if hasattr(v, "tolist") else v) for k, v in named.items()}
|
|
299
|
+
n = len(out["start1"])
|
|
300
|
+
if any(len(v) != n for v in out.values()):
|
|
301
|
+
raise ValueError("the BEDPE columns have different lengths")
|
|
302
|
+
for k in ("chr1", "chr2"):
|
|
303
|
+
out[k] = [str(c) for c in out[k]]
|
|
304
|
+
for k in ("start1", "end1", "start2", "end2"):
|
|
305
|
+
out[k] = [_position(v, i, k) for i, v in enumerate(out[k])]
|
|
306
|
+
for i in range(n):
|
|
307
|
+
for a in ("1", "2"):
|
|
308
|
+
if out["end" + a][i] < out["start" + a][i]:
|
|
309
|
+
raise ValueError(f"link {i + 1}: anchor {a} ends before it starts "
|
|
310
|
+
f"({out['start' + a][i]}-{out['end' + a][i]})")
|
|
311
|
+
if "score" in out:
|
|
312
|
+
out["score"] = [_score(v) for v in out["score"]]
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def read_pairs(reader: Any, regions: Sequence[Region]) -> dict[str, list[Any]]:
|
|
317
|
+
"""The pairs the reader gives for ``regions`` (those with an anchor in them; for
|
|
318
|
+
gwseq_io's peaks readers, those whose first anchor is), as BEDPE columns (see
|
|
319
|
+
:func:`pair_columns`)."""
|
|
320
|
+
return pair_columns(reader.read_entries([(r.chr, r.start, r.end) for r in regions]))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def read_models(reader: Any, region: Region, model: str, gene_type: Any,
|
|
324
|
+
transcript_type: Any) -> dict[str, list[Any]]:
|
|
325
|
+
"""The gene models overlapping ``region`` as BED12 columns: one per transcript
|
|
326
|
+
(``model="transcript"``, named by ``transcript_name``), or one per gene (named by
|
|
327
|
+
``gene_name``): its transcripts' exons merged, the coding part from the first coding
|
|
328
|
+
start to the last coding end. Ids name what has no name."""
|
|
329
|
+
cols = reader.read_entries([(region.chr, region.start, region.end)], model="transcript",
|
|
330
|
+
gene_type=gene_type, transcript_type=transcript_type)
|
|
331
|
+
starts, ends = ints(cols["start"]), ints(cols["end"])
|
|
332
|
+
cds0, cds1 = ints(cols["cds_start"]), ints(cols["cds_end"])
|
|
333
|
+
exons = cols.nested["exons"]
|
|
334
|
+
ex0, ex1 = ints(exons["start"]), ints(exons["end"])
|
|
335
|
+
offs = ints(exons.groups)
|
|
336
|
+
strands = list(cols["strand"])
|
|
337
|
+
level = "transcript" if model == "transcript" else "gene"
|
|
338
|
+
ids = list(cols[f"{level}_id"])
|
|
339
|
+
names = list(cols.get(f"{level}_name") or [None] * len(ids))
|
|
340
|
+
models: dict[Any, dict[str, Any]] = {}
|
|
341
|
+
for i, key in enumerate(ids):
|
|
342
|
+
m = models.setdefault(key, {"start": starts[i], "end": ends[i], "strand": strands[i],
|
|
343
|
+
"name": names[i] or key, "exons": [], "cds": []})
|
|
344
|
+
m["start"], m["end"] = min(m["start"], starts[i]), max(m["end"], ends[i])
|
|
345
|
+
m["exons"].extend(zip(ex0[offs[i]:offs[i + 1]], ex1[offs[i]:offs[i + 1]]))
|
|
346
|
+
if cds0[i] >= 0:
|
|
347
|
+
m["cds"].append((cds0[i], cds1[i]))
|
|
348
|
+
out: dict[str, list[Any]] = {k: [] for k in (
|
|
349
|
+
"chr", "start", "end", "name", "strand", "thickStart", "thickEnd", "blockCount",
|
|
350
|
+
"blockSizes", "blockStarts")}
|
|
351
|
+
for m in models.values():
|
|
352
|
+
merged: list[list[int]] = []
|
|
353
|
+
for a, b in sorted(m["exons"]):
|
|
354
|
+
if merged and a <= merged[-1][1]:
|
|
355
|
+
merged[-1][1] = max(merged[-1][1], b)
|
|
356
|
+
else:
|
|
357
|
+
merged.append([a, b])
|
|
358
|
+
s, e = m["start"], m["end"]
|
|
359
|
+
cds = m["cds"]
|
|
360
|
+
out["chr"].append(region.chr)
|
|
361
|
+
out["start"].append(s)
|
|
362
|
+
out["end"].append(e)
|
|
363
|
+
out["name"].append(str(m["name"]))
|
|
364
|
+
out["strand"].append(m["strand"])
|
|
365
|
+
out["thickStart"].append(min(a for a, _ in cds) if cds else e)
|
|
366
|
+
out["thickEnd"].append(max(b for _, b in cds) if cds else e)
|
|
367
|
+
out["blockCount"].append(len(merged))
|
|
368
|
+
out["blockSizes"].append([b - a for a, b in merged])
|
|
369
|
+
out["blockStarts"].append([a - s for a, _ in merged])
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def read_contacts(reader: Any, region: Region, bin_size: int | None, normalization: str,
|
|
374
|
+
matrix_type: str, depth: int | None,
|
|
375
|
+
bin_count: int | None = None) -> tuple[Any, float]:
|
|
376
|
+
"""``(matrix, start)``: the contact matrix of ``region`` at ``bin_size`` (rows and
|
|
377
|
+
columns from ``start``, the row grown to the grid of ``bin_size``), or in ``bin_count``
|
|
378
|
+
bins over the region, and where its first bin starts."""
|
|
379
|
+
normalization = _normalization(reader, normalization)
|
|
380
|
+
target = (region.chr, region.start, region.end)
|
|
381
|
+
kw: dict[str, Any]
|
|
382
|
+
start: float = region.start
|
|
383
|
+
if bin_count is not None:
|
|
384
|
+
kw = {"bin_count": int(bin_count)}
|
|
385
|
+
elif hasattr(reader, "resolve_regions"):
|
|
386
|
+
kw = {"bin_size": _bin_size(reader, bin_size), "snap_to_grid": "expand"}
|
|
387
|
+
start = _bounds(reader, region, dict(kw), True)[0]
|
|
388
|
+
else:
|
|
389
|
+
kw = {"bin_size": _bin_size(reader, bin_size), "snap_to_grid": False,
|
|
390
|
+
"full_bin": True}
|
|
391
|
+
extra: dict[str, Any] = {"normalization": normalization, "matrix_type": matrix_type}
|
|
392
|
+
if depth is not None:
|
|
393
|
+
extra["max_distance"] = int(depth)
|
|
394
|
+
m = reader.read_values(target, **kw, **extra)
|
|
395
|
+
return m, start
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _bin_size(reader: Any, bin_size: int | None) -> int:
|
|
399
|
+
"""``bin_size`` as an int, which must be one of ``reader.bin_sizes`` when it has them."""
|
|
400
|
+
n = int(bin_size or 0)
|
|
401
|
+
sizes = sorted(int(b) for b in getattr(reader, "bin_sizes", None) or ())
|
|
402
|
+
if sizes and n not in sizes:
|
|
403
|
+
def bp(b: int) -> str:
|
|
404
|
+
unit, f = next(((u, f) for u, f in (("Mb", 1_000_000), ("kb", 1_000))
|
|
405
|
+
if b >= f and b % f == 0), ("bp", 1))
|
|
406
|
+
return f"{b // f:,} {unit}"
|
|
407
|
+
raise ValueError(f"the Hi-C file has no matrix in bins of {bp(n)} (it has "
|
|
408
|
+
f"{', '.join(bp(b) for b in sizes)})")
|
|
409
|
+
return n
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _normalization(reader: Any, name: str) -> str:
|
|
413
|
+
"""``name`` as the reader spells it (``KR``, ``kr``); ``ValueError`` when it has none
|
|
414
|
+
such."""
|
|
415
|
+
norms = getattr(reader, "normalizations", None)
|
|
416
|
+
if norms is not None and name not in norms and name != "none":
|
|
417
|
+
same = [n for n in norms if str(n).lower() == name.lower()]
|
|
418
|
+
if not same:
|
|
419
|
+
raise ValueError(f"normalization {name!r} is not in the file (it has "
|
|
420
|
+
f"{', '.join(map(repr, norms))})")
|
|
421
|
+
return same[0]
|
|
422
|
+
return name
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def read_square(reader: Any, region: Region, region2: Region, bin_size: int | None,
|
|
426
|
+
normalization: str, matrix_type: str,
|
|
427
|
+
bin_count: tuple[int, int] | None = None) -> tuple[Any, float, float]:
|
|
428
|
+
"""``(matrix, start, start2)``: the contact matrix of ``region`` (rows) against
|
|
429
|
+
``region2`` (columns) at ``bin_size`` (each grown to its grid) or in ``bin_count``
|
|
430
|
+
``(rows, columns)`` bins, and where its first row and first column start."""
|
|
431
|
+
t1 = (region.chr, region.start, region.end)
|
|
432
|
+
t2 = (region2.chr, region2.start, region2.end)
|
|
433
|
+
kw: dict[str, Any]
|
|
434
|
+
start, start2 = float(region.start), float(region2.start)
|
|
435
|
+
if bin_count is not None:
|
|
436
|
+
kw = {"bin_count": (int(bin_count[0]), int(bin_count[1]))}
|
|
437
|
+
elif hasattr(reader, "resolve_regions"):
|
|
438
|
+
kw = {"bin_size": _bin_size(reader, bin_size), "snap_to_grid": "expand"}
|
|
439
|
+
r = reader.resolve_regions(t1, t2, **kw)
|
|
440
|
+
start, start2 = float(r["rows"]["start"][0]), float(r["cols"]["start"][0])
|
|
441
|
+
else:
|
|
442
|
+
kw = {"bin_size": _bin_size(reader, bin_size), "snap_to_grid": False,
|
|
443
|
+
"full_bin": True}
|
|
444
|
+
m = reader.read_values(t1, t2, **kw, normalization=_normalization(reader, normalization),
|
|
445
|
+
matrix_type=matrix_type)
|
|
446
|
+
return m, start, start2
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def bin_size_for(reader: Any, span: int, cells: float) -> int:
|
|
450
|
+
"""The finest of ``reader.bin_sizes`` giving at most ``cells`` cells across ``span``;
|
|
451
|
+
the coarsest when none does."""
|
|
452
|
+
sizes = sorted(int(b) for b in getattr(reader, "bin_sizes", []) or [])
|
|
453
|
+
if not sizes:
|
|
454
|
+
raise ValueError("the Hi-C reader has no bin_sizes; give bin_size")
|
|
455
|
+
for b in sizes:
|
|
456
|
+
if span / b <= max(1.0, cells):
|
|
457
|
+
return b
|
|
458
|
+
return sizes[-1]
|
|
459
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""A conformance check of readers: :func:`check` calls a reader as plot_io does and says
|
|
2
|
+
what it gives that plot_io cannot draw.
|
|
3
|
+
|
|
4
|
+
>>> from plot_io.genome.testing import check
|
|
5
|
+
>>> check(gwseq_io.open("atac.bw"), "signal", "chr2:30,000,001-31,000,000")
|
|
6
|
+
[]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import readers, sources
|
|
14
|
+
from .regions import Region, parse_region
|
|
15
|
+
|
|
16
|
+
__all__ = ["check", "KINDS"]
|
|
17
|
+
|
|
18
|
+
#: The reader roles :func:`check` knows (as :func:`plot_io.genome.readers.adapt` takes them).
|
|
19
|
+
KINDS = tuple(readers.WANTS)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _rows(v: Any) -> list[list[Any]]:
|
|
23
|
+
"""The rows of a 2-D result (a buffer or a sequence of rows)."""
|
|
24
|
+
if hasattr(v, "tolist"):
|
|
25
|
+
v = v.tolist()
|
|
26
|
+
rows = list(v)
|
|
27
|
+
if not all(hasattr(r, "__len__") and not isinstance(r, (str, bytes)) for r in rows):
|
|
28
|
+
raise ValueError("expected one row per region, got a flat sequence")
|
|
29
|
+
return [list(r.tolist() if hasattr(r, "tolist") else r) for r in rows]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _number(v: Any) -> bool:
|
|
33
|
+
try:
|
|
34
|
+
float(v)
|
|
35
|
+
except (TypeError, ValueError):
|
|
36
|
+
return False
|
|
37
|
+
return not isinstance(v, (str, bytes, bool))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check(reader: Any, kind: str, region: Any) -> list[str]:
|
|
41
|
+
"""The problems plot_io meets reading ``reader`` as a ``kind`` reader (``"signal"``,
|
|
42
|
+
``"intervals"``, ``"annotation"``, ``"pairs"``, ``"contacts"``) over ``region`` (a
|
|
43
|
+
region with data, as ``region=`` takes it); ``[]``: none. Each protocol member is
|
|
44
|
+
called with the arguments plot_io passes (:mod:`plot_io.genome.readers`), and the
|
|
45
|
+
results checked: rows per region and bins per row, numbers, the columns needed,
|
|
46
|
+
entries in the region, a square matrix, ``resolve_regions`` bounds against the read,
|
|
47
|
+
``chr_sizes`` a mapping of whole numbers."""
|
|
48
|
+
if kind not in readers.WANTS:
|
|
49
|
+
raise ValueError(f"kind must be one of {', '.join(KINDS)}, got {kind!r}")
|
|
50
|
+
r = parse_region(region)
|
|
51
|
+
problems: list[str] = []
|
|
52
|
+
got = sources.kind_of(reader)
|
|
53
|
+
if got not in readers.WANTS[kind]:
|
|
54
|
+
return [f"recognised as {got or 'no reader'}, not as a {kind} reader: give it a "
|
|
55
|
+
f"type attribute or the members of its protocol"]
|
|
56
|
+
for name, test in (("chr_sizes", _sized), (kind, _CHECKS[kind])):
|
|
57
|
+
if name == "chr_sizes" and not hasattr(reader, "chr_sizes"):
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
test(reader, r, problems)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
problems.append(f"{name}: {type(e).__name__}: {e}")
|
|
63
|
+
return problems
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _sized(reader: Any, r: Region, out: list[str]) -> None:
|
|
67
|
+
sizes = reader.chr_sizes
|
|
68
|
+
if not isinstance(sizes, Mapping):
|
|
69
|
+
out.append(f"chr_sizes is a {type(sizes).__name__}, not a mapping")
|
|
70
|
+
return
|
|
71
|
+
for k, v in sizes.items():
|
|
72
|
+
if not isinstance(k, str) or not isinstance(v, int) or isinstance(v, bool) or v <= 0:
|
|
73
|
+
out.append(f"chr_sizes: {k!r}: {v!r} is not a name and a positive whole number")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _signal(reader: Any, r: Region, out: list[str]) -> None:
|
|
78
|
+
t = (r.chr, r.start, r.end)
|
|
79
|
+
half = (r.chr, r.start, r.start + max(1, r.span // 2))
|
|
80
|
+
for regions, n in (([t], 7), ([t, half], 3)):
|
|
81
|
+
rows = _rows(reader.read_values(regions, bin_count=n, bin_mode="mean", def_value=0.0,
|
|
82
|
+
zoom="auto"))
|
|
83
|
+
if len(rows) != len(regions):
|
|
84
|
+
out.append(f"read_values of {len(regions)} region(s): {len(rows)} rows")
|
|
85
|
+
for row in rows:
|
|
86
|
+
if len(row) != n:
|
|
87
|
+
out.append(f"read_values(bin_count={n}): {len(row)} bins in a row")
|
|
88
|
+
if not all(_number(v) for v in row):
|
|
89
|
+
out.append("read_values: values that are not numbers")
|
|
90
|
+
break
|
|
91
|
+
size = max(1, r.span // 5)
|
|
92
|
+
edges, values = sources.read_signal(reader, r, None, size, "mean", 0.0)
|
|
93
|
+
if len(values) < r.span // size or len(edges) != len(values) + 1:
|
|
94
|
+
out.append(f"read_values(bin_size={size}): {len(values)} bins over {r.span} bp")
|
|
95
|
+
if hasattr(reader, "resolve_regions"):
|
|
96
|
+
got = reader.resolve_regions([t], bin_size=size, snap_to_grid="expand")
|
|
97
|
+
a, b = int(got["start"][0]), int(got["end"][0])
|
|
98
|
+
if a > r.start or b < r.end or (b - a) != len(values) * size:
|
|
99
|
+
out.append(f"resolve_regions gives {a}-{b} for {len(values)} bins of {size} bp "
|
|
100
|
+
f"over {r.start}-{r.end}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _in(s: Any, e: Any, r: Region) -> bool:
|
|
104
|
+
return int(e) > r.start and int(s) < r.end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _intervals(reader: Any, r: Region, out: list[str]) -> None:
|
|
108
|
+
cols = sources.read_entries(reader, r)
|
|
109
|
+
chrs = cols.get("chr")
|
|
110
|
+
for i, (s, e) in enumerate(zip(cols["start"], cols["end"])):
|
|
111
|
+
if not _in(s, e, r) or (chrs is not None and chrs[i] != r.chr):
|
|
112
|
+
out.append(f"read_entries: entry {i + 1} ({s}-{e}) is not in the region")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _annotation(reader: Any, r: Region, out: list[str]) -> None:
|
|
117
|
+
for model in ("transcript", "gene"):
|
|
118
|
+
cols = sources.read_models(reader, r, model, None, None)
|
|
119
|
+
for i, (s, e) in enumerate(zip(cols["start"], cols["end"])):
|
|
120
|
+
if not _in(s, e, r):
|
|
121
|
+
out.append(f"read_entries(model=): model {i + 1} ({s}-{e}) is not in the "
|
|
122
|
+
f"region")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _pairs(reader: Any, r: Region, out: list[str]) -> None:
|
|
127
|
+
cols = sources.read_pairs(reader, [r])
|
|
128
|
+
for i in range(len(cols["start1"])):
|
|
129
|
+
if not any(cols["chr" + a][i] == r.chr and _in(cols["start" + a][i],
|
|
130
|
+
cols["end" + a][i], r)
|
|
131
|
+
for a in "12"):
|
|
132
|
+
out.append(f"read_entries: pair {i + 1} has no anchor in the region")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _contacts(reader: Any, r: Region, out: list[str]) -> None:
|
|
137
|
+
sizes = list(getattr(reader, "bin_sizes", None) or [])
|
|
138
|
+
if not sizes or not all(isinstance(b, int) and b > 0 for b in sizes):
|
|
139
|
+
out.append(f"bin_sizes {sizes!r}: expected positive whole numbers")
|
|
140
|
+
return
|
|
141
|
+
norms = getattr(reader, "normalizations", None)
|
|
142
|
+
if norms is None or not all(isinstance(n, str) for n in norms):
|
|
143
|
+
out.append(f"normalizations {norms!r}: expected names")
|
|
144
|
+
b = sources.bin_size_for(reader, r.span, 50)
|
|
145
|
+
m, start = sources.read_contacts(reader, r, b, "none", "observed", None)
|
|
146
|
+
h, w, k, flat = sources.matrix(m)
|
|
147
|
+
if h != w or k != 1:
|
|
148
|
+
out.append(f"read_values(bin_size={b}): a {h} × {w} matrix for one region")
|
|
149
|
+
elif start > r.start or start + h * b < r.end:
|
|
150
|
+
out.append(f"read_values(bin_size={b}): {h} bins from {start} do not cover "
|
|
151
|
+
f"{r.start}-{r.end}")
|
|
152
|
+
if not all(_number(v) and not math.isinf(float(v)) for v in flat):
|
|
153
|
+
out.append("read_values: values that are not finite numbers or NaN")
|
|
154
|
+
m, _ = sources.read_contacts(reader, r, None, "none", "observed", None, bin_count=4)
|
|
155
|
+
h, w, _, _ = sources.matrix(m)
|
|
156
|
+
if (h, w) != (4, 4):
|
|
157
|
+
out.append(f"read_values(bin_count=4): a {h} × {w} matrix")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_CHECKS = {"signal": _signal, "intervals": _intervals, "annotation": _annotation,
|
|
161
|
+
"pairs": _pairs, "contacts": _contacts}
|