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
plot_io/formatters.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Tick label formatters, one per axis kind: numbers, times and categories.
|
|
2
|
+
|
|
3
|
+
A formatter gives the labels of tick values and the offset content written at the end of
|
|
4
|
+
the axis (the common ×10ᵏ factor and additive offset of numbers, ``+1×10⁹``; the date
|
|
5
|
+
part common to time ticks, ``Mar 2026``), or ``None`` when there is none. Axes and
|
|
6
|
+
colorbars label their ticks through the formatter of their axis
|
|
7
|
+
(:meth:`plot_io.axis.AxisState.formatter`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any, NamedTuple
|
|
12
|
+
|
|
13
|
+
from .numbers import NumberStyle, additive_offset, apply_format, format_axis, offset_label
|
|
14
|
+
from .text import superscript
|
|
15
|
+
from .times import time_labels, time_ticks
|
|
16
|
+
|
|
17
|
+
__all__ = ["Formatted", "NumberFormatter", "TimeFormatter", "CategoryFormatter",
|
|
18
|
+
"number_offset"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Formatted(NamedTuple):
|
|
22
|
+
labels: list[Any]
|
|
23
|
+
offset: Any = None # content written at the end of the axis, or None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def number_offset(factor: int, shift: float, style: NumberStyle) -> Any:
|
|
27
|
+
"""The offset content of numeric labels: ``×10ᵏ`` and the value added (``+1×10⁹``);
|
|
28
|
+
``None`` for neither."""
|
|
29
|
+
if not factor and not shift:
|
|
30
|
+
return None
|
|
31
|
+
out: list[Any] = []
|
|
32
|
+
if factor:
|
|
33
|
+
out += ["×10", superscript(style.exponent(factor))]
|
|
34
|
+
if shift:
|
|
35
|
+
out += offset_label(shift, style)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NumberFormatter:
|
|
40
|
+
"""Labels of numbers formatted together (shared decimals) in ``fmt``; ``"auto"``
|
|
41
|
+
writes a common part of four digits or more as an additive offset and large or small
|
|
42
|
+
values as mantissas of a common ×10ᵏ factor."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, fmt: Any, style: NumberStyle, sci_limits: tuple[int, int]) -> None:
|
|
45
|
+
self.fmt, self.style, self.sci_limits = fmt, style, sci_limits
|
|
46
|
+
|
|
47
|
+
def __call__(self, values: list[float]) -> Formatted:
|
|
48
|
+
shift = additive_offset(values) if self.fmt == "auto" else 0.0
|
|
49
|
+
if shift: # the differences, rounded relative to the tick spacing
|
|
50
|
+
step = min((abs(b - a) for a, b in zip(values, values[1:]) if b != a),
|
|
51
|
+
default=abs(shift))
|
|
52
|
+
digits = 2 - math.floor(math.log10(step))
|
|
53
|
+
values = [round(v - shift, digits) + 0.0 for v in values]
|
|
54
|
+
labels, factor = format_axis(values, self.fmt, self.style, self.sci_limits)
|
|
55
|
+
return Formatted(labels, number_offset(factor, shift, self.style))
|
|
56
|
+
|
|
57
|
+
def offset(self, factor: int) -> Any:
|
|
58
|
+
"""The offset content of labels written as mantissas of ×10^``factor``."""
|
|
59
|
+
return number_offset(factor, 0.0, self.style)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TimeFormatter:
|
|
63
|
+
"""Concise labels of times (seconds since the epoch) in the finest calendar unit the
|
|
64
|
+
ticks need, with the date part common to them as the offset; ``fmt``: strftime codes
|
|
65
|
+
or a function of a ``datetime`` (no offset)."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, fmt: Any, tz: Any) -> None:
|
|
68
|
+
self.fmt, self.tz = fmt, tz
|
|
69
|
+
|
|
70
|
+
def __call__(self, values: list[float]) -> Formatted:
|
|
71
|
+
labels, offset = time_labels(values, self.tz, self.fmt)
|
|
72
|
+
return Formatted(labels, offset)
|
|
73
|
+
|
|
74
|
+
def ticks(self, lo: float, hi: float, most: int) -> tuple[list[tuple[float, Any]], Any]:
|
|
75
|
+
"""Automatic ticks between ``lo`` and ``hi``: ``[(value, label)]`` and the offset."""
|
|
76
|
+
return time_ticks(lo, hi, most, self.tz, self.fmt)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CategoryFormatter:
|
|
80
|
+
"""Labels of categories (by index): their names (a list is a name of several lines),
|
|
81
|
+
or numeric categories in ``fmt``, or each name through ``fmt``."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, categories: list[Any], fmt: Any, style: NumberStyle) -> None:
|
|
84
|
+
self.categories, self.fmt, self.style = categories, fmt, style
|
|
85
|
+
|
|
86
|
+
def __call__(self, index: list[int]) -> Formatted:
|
|
87
|
+
cats = [self.categories[i] for i in index]
|
|
88
|
+
fmt = self.fmt
|
|
89
|
+
if fmt != "auto":
|
|
90
|
+
if all(isinstance(c, (int, float)) and not isinstance(c, bool) for c in cats):
|
|
91
|
+
return Formatted(format_axis([float(c) for c in cats], fmt, self.style,
|
|
92
|
+
None)[0])
|
|
93
|
+
if callable(fmt):
|
|
94
|
+
return Formatted([fmt(c) for c in cats])
|
|
95
|
+
if fmt not in ("percent", "sci", "eng", "si", "int"):
|
|
96
|
+
return Formatted([apply_format(fmt, c) for c in cats])
|
|
97
|
+
return Formatted([str(c) if not isinstance(c, (list, str)) else c for c in cats])
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Genome tracks: signals, intervals, gene models, links and Hi-C triangles over a
|
|
2
|
+
region, and site matrices (a signal around many sites), read from gwseq_io's readers (or
|
|
3
|
+
any object with their methods) without depending on them; drawn on a genome x axis
|
|
4
|
+
(:mod:`~plot_io.genome.scale`), in a tracks figure (:func:`~plot_io.genome.layout.Tracks`)
|
|
5
|
+
or in any plot."""
|
|
6
|
+
|
|
7
|
+
from .signal import BigWig
|
|
8
|
+
from .combine import combine
|
|
9
|
+
from .intervals import Bed, Genes, Gtf
|
|
10
|
+
from .contacts import HiC
|
|
11
|
+
from .links import Links
|
|
12
|
+
from .layout import Track, Tracks
|
|
13
|
+
from .regions import Region, parse_region
|
|
14
|
+
from .sites import SignalMatrix, SiteHeatmap, SiteProfile
|
|
15
|
+
|
|
16
|
+
__all__ = ["Tracks", "Track", "BigWig", "combine", "Bed", "Genes", "Gtf", "HiC", "Links",
|
|
17
|
+
"SiteHeatmap", "SiteProfile", "SignalMatrix", "Region", "parse_region"]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Readers for what is not one: paths, opened with gwseq_io, and plain data, read from
|
|
2
|
+
memory. Each meets its protocol (:mod:`plot_io.genome.readers`).
|
|
3
|
+
|
|
4
|
+
* paths: ``.bed`` (``.bed12``, …), ``.narrowPeak``, ``.broadPeak`` (gzipped too) through
|
|
5
|
+
``gwseq_io.open_bed``; ``.tsv``, ``.csv``, ``.xlsx`` through ``gwseq_io.open_peaks``;
|
|
6
|
+
anything else through ``gwseq_io.open``, which tells the format by its bytes. The
|
|
7
|
+
formats gwseq_io has no reader for (bedGraph, BEDPE) are refused;
|
|
8
|
+
* :class:`Intervals`: BED columns (a gene table: BED12);
|
|
9
|
+
* :class:`Pairs`: BEDPE columns, the pairs with an anchor in the region;
|
|
10
|
+
* :class:`Signal`: ``chr``, ``start``, ``end``, ``value`` records, binned here;
|
|
11
|
+
* :class:`Matrix`: a square matrix over the region drawn, whatever it is.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import bisect
|
|
15
|
+
import importlib
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import weakref
|
|
20
|
+
from array import array
|
|
21
|
+
from collections.abc import Mapping, Sequence
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from . import sources
|
|
25
|
+
|
|
26
|
+
__all__ = ["check_path", "open_path", "close", "Deferred", "from_data", "Intervals", "Pairs",
|
|
27
|
+
"Signal", "Matrix", "REDUCTIONS"]
|
|
28
|
+
|
|
29
|
+
_BED = re.compile(r"\.(bed\d*|narrowpeak|broadpeak)(\.b?gz)?$", re.IGNORECASE)
|
|
30
|
+
_PEAKS = re.compile(r"\.(tsv|csv|xlsx)$", re.IGNORECASE)
|
|
31
|
+
#: Formats gwseq_io has no reader for, by suffix: their name and what to do instead.
|
|
32
|
+
_UNREAD = {
|
|
33
|
+
"bedgraph": ("bedGraph", "convert it to a bigWig (gwseq_io.convert_to_bigwig) or pass "
|
|
34
|
+
"its chr, start, end, value columns"),
|
|
35
|
+
"bdg": ("bedGraph", "convert it to a bigWig (gwseq_io.convert_to_bigwig) or pass its "
|
|
36
|
+
"chr, start, end, value columns"),
|
|
37
|
+
"bedpe": ("BEDPE", "pass its columns (chr1, start1, end1, chr2, start2, end2) as data"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _gwseq_io(path: str, what: str) -> Any:
|
|
42
|
+
try:
|
|
43
|
+
return importlib.import_module("gwseq_io")
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise TypeError(f"{what}: reading the file {path!r} needs gwseq_io, which is not "
|
|
46
|
+
f"installed; or pass an open reader or plain data") from None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_path(path: Any, what: str) -> None:
|
|
50
|
+
"""Refuse a path of a format gwseq_io has no reader for (by its suffix)."""
|
|
51
|
+
p = os.fsdecode(path)
|
|
52
|
+
name = p.lower()
|
|
53
|
+
for ext in (".gz", ".bgz"):
|
|
54
|
+
if name.endswith(ext):
|
|
55
|
+
name = name[:-len(ext)]
|
|
56
|
+
suffix = name.rsplit(".", 1)[-1] if "." in os.path.basename(name) else ""
|
|
57
|
+
if suffix in _UNREAD:
|
|
58
|
+
fmt, instead = _UNREAD[suffix]
|
|
59
|
+
raise TypeError(f"{what}: gwseq_io, which plot_io reads files with, has no {fmt} "
|
|
60
|
+
f"reader ({p!r}): {instead}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def open_path(path: Any, what: str) -> Any:
|
|
64
|
+
"""The gwseq_io reader of the file at ``path`` (see the module)."""
|
|
65
|
+
check_path(path, what)
|
|
66
|
+
p = os.fsdecode(path)
|
|
67
|
+
g = _gwseq_io(p, what)
|
|
68
|
+
if _BED.search(p):
|
|
69
|
+
return g.open_bed(p)
|
|
70
|
+
if _PEAKS.search(p):
|
|
71
|
+
return g.open_peaks(p)
|
|
72
|
+
try:
|
|
73
|
+
return g.open(p)
|
|
74
|
+
except g.InvalidFile as e:
|
|
75
|
+
raise TypeError(f"{what}: gwseq_io cannot read {p!r} ({e}); a BED or peaks file "
|
|
76
|
+
f"needs its suffix (.bed, .narrowPeak, .tsv, …)") from e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def close(reader: Any) -> None:
|
|
80
|
+
"""Close a reader plot_io opened (errors ignored: it is being let go)."""
|
|
81
|
+
f = getattr(reader, "close", None)
|
|
82
|
+
if f is not None:
|
|
83
|
+
try:
|
|
84
|
+
f()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Deferred:
|
|
90
|
+
"""A path's reader, opened when first used (at draw): once per figure, shared by the
|
|
91
|
+
series of the figure that read the same path, closed when the figure is collected (a
|
|
92
|
+
series without a figure: when this is). Every member is the reader's."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, path: Any, kinds: tuple[str, ...], what: str, owner: Any) -> None:
|
|
95
|
+
self._path, self._kinds, self._what = os.fsdecode(path), kinds, what
|
|
96
|
+
self._owner = None if owner is None else weakref.ref(owner)
|
|
97
|
+
self._reader: Any = None
|
|
98
|
+
|
|
99
|
+
def _figure(self) -> Any:
|
|
100
|
+
owner = None if self._owner is None else self._owner()
|
|
101
|
+
return getattr(getattr(owner, "plot", None), "figure", None)
|
|
102
|
+
|
|
103
|
+
def opened(self) -> Any:
|
|
104
|
+
"""The reader (opened now when it is not yet)."""
|
|
105
|
+
fig = self._figure()
|
|
106
|
+
cache = None if fig is None else fig.__dict__.setdefault("_genome_readers", {})
|
|
107
|
+
reader = self._reader if cache is None else cache.get(self._path)
|
|
108
|
+
if reader is None:
|
|
109
|
+
reader = open_path(self._path, self._what)
|
|
110
|
+
try:
|
|
111
|
+
sources.check_reader(reader, self._kinds, self._what)
|
|
112
|
+
except BaseException:
|
|
113
|
+
close(reader)
|
|
114
|
+
raise
|
|
115
|
+
if cache is None:
|
|
116
|
+
weakref.finalize(self, close, reader)
|
|
117
|
+
else:
|
|
118
|
+
cache[self._path] = reader
|
|
119
|
+
weakref.finalize(fig, close, reader)
|
|
120
|
+
else:
|
|
121
|
+
sources.check_reader(reader, self._kinds, self._what)
|
|
122
|
+
self._reader = reader
|
|
123
|
+
return reader
|
|
124
|
+
|
|
125
|
+
def __getattr__(self, name: str) -> Any:
|
|
126
|
+
if name.startswith("_"):
|
|
127
|
+
raise AttributeError(name)
|
|
128
|
+
return getattr(self.opened(), name)
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
return f"<reader of {self._path!r}>"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def from_data(value: Any, want: str, what: str) -> Any:
|
|
135
|
+
"""An in-memory reader of plain data for role ``want`` (see
|
|
136
|
+
:func:`plot_io.genome.readers.adapt`)."""
|
|
137
|
+
if want == "signal":
|
|
138
|
+
if callable(value): # f(chr, start, end, bins): values over the region
|
|
139
|
+
return value
|
|
140
|
+
return Signal(value, what)
|
|
141
|
+
if want in ("intervals", "annotation"):
|
|
142
|
+
return Intervals(value)
|
|
143
|
+
if want == "pairs":
|
|
144
|
+
return Pairs(value)
|
|
145
|
+
if want == "contacts":
|
|
146
|
+
return Matrix(value)
|
|
147
|
+
raise ValueError(f"unknown reader role {want!r}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _regions(regions: Any) -> list[tuple[str, int, int]]:
|
|
151
|
+
if isinstance(regions, tuple) and len(regions) == 3 and isinstance(regions[0], str):
|
|
152
|
+
regions = [regions]
|
|
153
|
+
return [(str(c), int(s), int(e)) for c, s, e in regions]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Intervals:
|
|
157
|
+
"""BED columns in memory (see :func:`~plot_io.genome.sources.entry_columns`): the
|
|
158
|
+
entries overlapping a region, on its chromosome when the data name one."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, data: Any) -> None:
|
|
161
|
+
self.columns = sources.entry_columns(data)
|
|
162
|
+
chrs = self.columns.get("chr")
|
|
163
|
+
#: the chromosomes the data name (``None``: any)
|
|
164
|
+
self.chr_names = None if chrs is None else frozenset(chrs)
|
|
165
|
+
|
|
166
|
+
def read_entries(self, regions: Any, columns: Any = None, **kw: Any) -> dict[str, list]:
|
|
167
|
+
cols = self.columns
|
|
168
|
+
chrs = cols.get("chr")
|
|
169
|
+
starts, ends = cols["start"], cols["end"]
|
|
170
|
+
keep: list[int] = []
|
|
171
|
+
for c, lo, hi in _regions(regions):
|
|
172
|
+
keep += [i for i in range(len(starts)) if ends[i] > lo and starts[i] < hi
|
|
173
|
+
and (chrs is None or chrs[i] == c)]
|
|
174
|
+
return {k: [v[i] for i in keep] for k, v in cols.items()}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Pairs:
|
|
178
|
+
"""BEDPE columns in memory (see :func:`~plot_io.genome.sources.pair_columns`): the
|
|
179
|
+
pairs with either anchor overlapping a region, each once, in the given order."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, data: Any) -> None:
|
|
182
|
+
self.columns = sources.pair_columns(data)
|
|
183
|
+
|
|
184
|
+
def read_entries(self, regions: Any, **kw: Any) -> dict[str, list]:
|
|
185
|
+
cols = self.columns
|
|
186
|
+
want = _regions(regions)
|
|
187
|
+
keep = []
|
|
188
|
+
for i in range(len(cols["start1"])):
|
|
189
|
+
for c, lo, hi in want:
|
|
190
|
+
if (cols["chr1"][i] == c and cols["end1"][i] > lo and cols["start1"][i] < hi) \
|
|
191
|
+
or (cols["chr2"][i] == c and cols["end2"][i] > lo
|
|
192
|
+
and cols["start2"][i] < hi):
|
|
193
|
+
keep.append(i)
|
|
194
|
+
break
|
|
195
|
+
return {k: [v[i] for i in keep] for k, v in cols.items()}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
#: The reductions :class:`Signal` computes.
|
|
199
|
+
REDUCTIONS = ("mean", "min", "max", "sum", "count")
|
|
200
|
+
_SIGNAL_KEYS = {"chr": ("chr", "chrom", "chromosome", "seqname"),
|
|
201
|
+
"start": ("start", "chromstart", "chrom_start"),
|
|
202
|
+
"end": ("end", "chromend", "chrom_end"),
|
|
203
|
+
"value": ("value", "score", "signal", "dataValue")}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _signal_columns(data: Any, what: str) -> dict[str, list]:
|
|
207
|
+
if hasattr(data, "to_dict") and hasattr(data, "columns"): # a DataFrame
|
|
208
|
+
data = {str(c): list(data[c]) for c in data.columns}
|
|
209
|
+
if isinstance(data, Mapping):
|
|
210
|
+
low = {str(k).lower(): v for k, v in data.items()}
|
|
211
|
+
cols = {}
|
|
212
|
+
for name, keys in _SIGNAL_KEYS.items():
|
|
213
|
+
got = next((low[k.lower()] for k in keys if k.lower() in low), None)
|
|
214
|
+
if got is None:
|
|
215
|
+
raise ValueError(f"{what}: a signal table needs chr, start, end and value "
|
|
216
|
+
f"columns; {name} is missing")
|
|
217
|
+
cols[name] = list(got.tolist() if hasattr(got, "tolist") else got)
|
|
218
|
+
return cols
|
|
219
|
+
if isinstance(data, (str, bytes)) or not isinstance(data, Sequence) and \
|
|
220
|
+
not hasattr(data, "__iter__"):
|
|
221
|
+
raise TypeError(f"{what} takes a signal reader, a path, a function f(chr, start, "
|
|
222
|
+
f"end, bins) or chr, start, end, value records; got "
|
|
223
|
+
f"{type(data).__name__}")
|
|
224
|
+
rows = list(data)
|
|
225
|
+
if rows and isinstance(rows[0], Mapping):
|
|
226
|
+
return _signal_columns({k: [r[k] for r in rows] for k in rows[0]}, what)
|
|
227
|
+
if any(isinstance(r, (str, bytes)) or not isinstance(r, Sequence) or len(r) != 4
|
|
228
|
+
for r in rows):
|
|
229
|
+
raise TypeError(f"{what} takes a signal reader, a path, a function f(chr, start, "
|
|
230
|
+
f"end, bins) or chr, start, end, value records (rows of 4); got "
|
|
231
|
+
f"{type(data).__name__}")
|
|
232
|
+
return {k: [r[i] for r in rows] for i, k in enumerate(("chr", "start", "end", "value"))}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Signal:
|
|
236
|
+
"""``chr``, ``start``, ``end``, ``value`` records in memory (columns, a DataFrame, or
|
|
237
|
+
rows), binned here as gwseq_io bins a bigWig: each bin the reduction (:data:`REDUCTIONS`)
|
|
238
|
+
of its bases, weighted by overlap, bases no record covers counting as ``def_value``
|
|
239
|
+
(left out when NaN)."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, data: Any, what: str = "BigWig") -> None:
|
|
242
|
+
cols = _signal_columns(data, what)
|
|
243
|
+
n = len(cols["chr"])
|
|
244
|
+
if any(len(v) != n for v in cols.values()):
|
|
245
|
+
raise ValueError(f"{what}: the signal columns have different lengths")
|
|
246
|
+
by: dict[str, list[tuple[int, int, float]]] = {}
|
|
247
|
+
for i in range(n):
|
|
248
|
+
s = sources._position(cols["start"][i], i, "start")
|
|
249
|
+
e = sources._position(cols["end"][i], i, "end")
|
|
250
|
+
if e < s:
|
|
251
|
+
raise ValueError(f"record {i + 1} ends before it starts ({s}-{e})")
|
|
252
|
+
v = cols["value"][i]
|
|
253
|
+
by.setdefault(str(cols["chr"][i]), []).append(
|
|
254
|
+
(s, e, math.nan if v is None else float(v)))
|
|
255
|
+
self._chrs: dict[str, tuple[array, array, array, int]] = {}
|
|
256
|
+
for c, recs in by.items():
|
|
257
|
+
recs.sort()
|
|
258
|
+
self._chrs[c] = (array("q", [r[0] for r in recs]), array("q", [r[1] for r in recs]),
|
|
259
|
+
array("d", [r[2] for r in recs]),
|
|
260
|
+
max((r[1] - r[0] for r in recs), default=0))
|
|
261
|
+
#: the chromosomes the records name
|
|
262
|
+
self.chr_names = frozenset(self._chrs)
|
|
263
|
+
|
|
264
|
+
def read_values(self, regions: Any, *, bin_count: int | None = None,
|
|
265
|
+
bin_size: int | None = None, bin_mode: str = "mean",
|
|
266
|
+
def_value: float = 0.0, full_bin: bool = False, **kw: Any) -> list[array]:
|
|
267
|
+
if bin_mode not in REDUCTIONS:
|
|
268
|
+
raise ValueError(f"bin_mode {bin_mode!r}: plain signal data are reduced by "
|
|
269
|
+
f"{', '.join(REDUCTIONS)}")
|
|
270
|
+
if kw.get("snap_to_grid") not in (None, False, "auto"):
|
|
271
|
+
raise ValueError("plain signal data are binned from the region's start "
|
|
272
|
+
"(snap_to_grid=False)")
|
|
273
|
+
out = []
|
|
274
|
+
for c, lo, hi in _regions(regions):
|
|
275
|
+
if bin_count is not None:
|
|
276
|
+
n = max(1, int(bin_count))
|
|
277
|
+
edges = [lo + (hi - lo) * i / n for i in range(n + 1)]
|
|
278
|
+
edges[-1] = float(hi)
|
|
279
|
+
else:
|
|
280
|
+
size = int(bin_size or 1)
|
|
281
|
+
n = (hi - lo) // size if not full_bin else -(-(hi - lo) // size)
|
|
282
|
+
edges = [float(lo + i * size) for i in range(n + 1)]
|
|
283
|
+
out.append(self._bin(c, edges, bin_mode, float(def_value)))
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
def _bin(self, chr: str, edges: list[float], mode: str, def_value: float) -> array:
|
|
287
|
+
n = len(edges) - 1
|
|
288
|
+
total = [0.0] * n # value × bases covered
|
|
289
|
+
covered = [0.0] * n # bases covered
|
|
290
|
+
lo_v = [math.inf] * n
|
|
291
|
+
hi_v = [-math.inf] * n
|
|
292
|
+
got = self._chrs.get(chr)
|
|
293
|
+
if got is not None and n > 0:
|
|
294
|
+
starts, ends, values, longest = got
|
|
295
|
+
a, b = edges[0], edges[-1]
|
|
296
|
+
i = bisect.bisect_left(starts, int(a) - longest)
|
|
297
|
+
j = bisect.bisect_left(starts, b)
|
|
298
|
+
for k in range(i, j):
|
|
299
|
+
s, e, v = starts[k], ends[k], values[k]
|
|
300
|
+
if e <= a or math.isnan(v):
|
|
301
|
+
continue
|
|
302
|
+
s0, e0 = max(float(s), a), min(float(e), b)
|
|
303
|
+
if e0 <= s0:
|
|
304
|
+
continue
|
|
305
|
+
first = max(0, bisect.bisect_right(edges, s0) - 1)
|
|
306
|
+
for m in range(first, n):
|
|
307
|
+
x0, x1 = max(s0, edges[m]), min(e0, edges[m + 1])
|
|
308
|
+
if x1 <= x0:
|
|
309
|
+
if edges[m] >= e0:
|
|
310
|
+
break
|
|
311
|
+
continue
|
|
312
|
+
total[m] += v * (x1 - x0)
|
|
313
|
+
covered[m] += x1 - x0
|
|
314
|
+
lo_v[m] = min(lo_v[m], v)
|
|
315
|
+
hi_v[m] = max(hi_v[m], v)
|
|
316
|
+
fill = not math.isnan(def_value)
|
|
317
|
+
out = array("d")
|
|
318
|
+
for m in range(n):
|
|
319
|
+
width = edges[m + 1] - edges[m]
|
|
320
|
+
gap = max(0.0, width - covered[m])
|
|
321
|
+
if mode == "count":
|
|
322
|
+
out.append(covered[m])
|
|
323
|
+
continue
|
|
324
|
+
if fill and gap > 1e-9:
|
|
325
|
+
total[m] += def_value * gap
|
|
326
|
+
lo_v[m], hi_v[m] = min(lo_v[m], def_value), max(hi_v[m], def_value)
|
|
327
|
+
weight = width
|
|
328
|
+
else:
|
|
329
|
+
weight = covered[m]
|
|
330
|
+
if weight <= 0:
|
|
331
|
+
out.append(math.nan)
|
|
332
|
+
elif mode == "mean":
|
|
333
|
+
out.append(total[m] / weight)
|
|
334
|
+
elif mode == "sum":
|
|
335
|
+
out.append(total[m])
|
|
336
|
+
else:
|
|
337
|
+
out.append(lo_v[m] if mode == "min" else hi_v[m])
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Matrix:
|
|
342
|
+
"""A square matrix of values over the region drawn (bins of ``span / n``): it has no
|
|
343
|
+
coordinates of its own, so it is the same whatever the region."""
|
|
344
|
+
|
|
345
|
+
bin_sizes: tuple[int, ...] = ()
|
|
346
|
+
normalizations = ("none",)
|
|
347
|
+
|
|
348
|
+
def __init__(self, data: Any) -> None:
|
|
349
|
+
h, w, k, flat = sources.matrix(data)
|
|
350
|
+
if h != w or k != 1 or not h:
|
|
351
|
+
raise ValueError(f"HiC needs a square matrix of values, got {h} × {w}")
|
|
352
|
+
self.n, self.values = h, flat
|
|
353
|
+
|
|
354
|
+
def read_values(self, region1: Any, region2: Any = None, **kw: Any) -> list[array]:
|
|
355
|
+
n, v = self.n, self.values
|
|
356
|
+
return [v[i * n:(i + 1) * n] for i in range(n)]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Two signals as one: :func:`combine` gives a signal reader whose values are those of two
|
|
2
|
+
readers read with the same arguments, combined bin by bin (a difference, a ratio, a log2
|
|
3
|
+
ratio or a function)."""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from vec_io.arrays import doubles
|
|
10
|
+
|
|
11
|
+
from ..validators import real
|
|
12
|
+
from . import adapters, readers
|
|
13
|
+
|
|
14
|
+
__all__ = ["combine", "Combined", "OPS"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ratio(a: float, b: float) -> float:
|
|
18
|
+
return a / b if b else math.nan
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _log2ratio(p: float) -> Callable[[float, float], float]:
|
|
22
|
+
def f(a: float, b: float) -> float:
|
|
23
|
+
a, b = a + p, b + p
|
|
24
|
+
return math.log2(a / b) if a > 0 and b > 0 else math.nan
|
|
25
|
+
return f
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: The operations by name: ``a - b``, ``a / b``, ``log2((a + p) / (b + p))``.
|
|
29
|
+
OPS = ("difference", "ratio", "log2ratio")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rows(v: Any) -> list[Any]:
|
|
33
|
+
"""The rows of a read (a 2-D buffer or a sequence of rows), as doubles."""
|
|
34
|
+
shape = getattr(v, "shape", None)
|
|
35
|
+
if shape is not None and len(shape) == 2:
|
|
36
|
+
return [doubles(v[i]) for i in range(shape[0])]
|
|
37
|
+
return [doubles(r) for r in v]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Combined:
|
|
41
|
+
"""A signal reader of two (see :func:`combine`)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, a: Any, b: Any, op: Any, pseudo_count: Any) -> None:
|
|
44
|
+
self.a = self._signal(a)
|
|
45
|
+
self.b = self._signal(b)
|
|
46
|
+
p = real(pseudo_count)
|
|
47
|
+
if p is None or not math.isfinite(p) or p < 0:
|
|
48
|
+
raise ValueError(f"combine: pseudo_count must be a number ≥ 0, got "
|
|
49
|
+
f"{pseudo_count!r}")
|
|
50
|
+
if isinstance(op, str):
|
|
51
|
+
if op not in OPS:
|
|
52
|
+
raise ValueError(f"combine: op must be one of {', '.join(OPS)} or a "
|
|
53
|
+
f"function, got {op!r}")
|
|
54
|
+
f: Callable[[float, float], float] = (
|
|
55
|
+
(lambda x, y: x - y) if op == "difference" else
|
|
56
|
+
(lambda x, y: _ratio(x + p, y + p)) if op == "ratio" else _log2ratio(p))
|
|
57
|
+
elif callable(op):
|
|
58
|
+
f = op
|
|
59
|
+
else:
|
|
60
|
+
raise TypeError(f"combine: op must be one of {', '.join(OPS)} or a function, "
|
|
61
|
+
f"got {op!r}")
|
|
62
|
+
self.op, self.pseudo_count, self._f = op, p, f
|
|
63
|
+
|
|
64
|
+
def __getattr__(self, name: str) -> Any:
|
|
65
|
+
"""``resolve_regions``: the first reader's, when both give it (read at draw)."""
|
|
66
|
+
if name == "resolve_regions" and "b" in self.__dict__ \
|
|
67
|
+
and hasattr(self.a, name) and hasattr(self.b, name):
|
|
68
|
+
return self.a.resolve_regions
|
|
69
|
+
raise AttributeError(name)
|
|
70
|
+
|
|
71
|
+
def _signal(self, v: Any) -> Any:
|
|
72
|
+
reader = readers.adapt(v, "signal", "combine", owner=self)
|
|
73
|
+
if not isinstance(reader, adapters.Deferred) and not hasattr(reader, "read_values"):
|
|
74
|
+
raise TypeError(f"combine needs signal readers or signal data, got {v!r}")
|
|
75
|
+
return reader
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
op = self.op if isinstance(self.op, str) else getattr(self.op, "__name__", "op")
|
|
79
|
+
return f"combine({self.a!r}, {self.b!r}, {op!r})"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def chr_sizes(self) -> Mapping[str, int]:
|
|
83
|
+
"""The sizes either reader gives (the larger)."""
|
|
84
|
+
sizes: dict[str, int] = {}
|
|
85
|
+
for r in (self.a, self.b):
|
|
86
|
+
got = getattr(r, "chr_sizes", None)
|
|
87
|
+
if isinstance(got, Mapping):
|
|
88
|
+
for k, n in got.items():
|
|
89
|
+
sizes[str(k)] = max(sizes.get(str(k), 0), int(n))
|
|
90
|
+
if not sizes:
|
|
91
|
+
raise AttributeError("neither reader gives chr_sizes")
|
|
92
|
+
return sizes
|
|
93
|
+
|
|
94
|
+
def read_values(self, regions: Any, **kw: Any) -> list[Any]:
|
|
95
|
+
"""Both readers read with these arguments, combined bin by bin: NaN where either
|
|
96
|
+
is NaN or the result is undefined (a ratio over 0, the log of a value ≤ 0)."""
|
|
97
|
+
ra, rb = _rows(self.a.read_values(regions, **kw)), _rows(self.b.read_values(regions, **kw))
|
|
98
|
+
if len(ra) != len(rb):
|
|
99
|
+
raise ValueError(f"combine: the readers gave {len(ra)} and {len(rb)} rows")
|
|
100
|
+
f = self._f
|
|
101
|
+
out = []
|
|
102
|
+
for x, y in zip(ra, rb):
|
|
103
|
+
if len(x) != len(y):
|
|
104
|
+
raise ValueError(f"combine: the readers gave rows of {len(x)} and {len(y)} "
|
|
105
|
+
f"bins (give both the same grid)")
|
|
106
|
+
row = [math.nan if math.isnan(a) or math.isnan(b) else float(f(a, b))
|
|
107
|
+
for a, b in zip(x, y)]
|
|
108
|
+
out.append(doubles(row))
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def combine(a: Any, b: Any, op: Any = "log2ratio", pseudo_count: Any = 0) -> Combined:
|
|
113
|
+
"""A signal reader of ``a`` and ``b`` (readers, paths or signal data, as ``bigwig``
|
|
114
|
+
takes them) read with the same arguments and combined bin by bin: ``"difference"``
|
|
115
|
+
(``a - b``), ``"ratio"`` (``a / b``), ``"log2ratio"`` (``log2((a + p) / (b + p))``,
|
|
116
|
+
``p`` the ``pseudo_count``; a ratio adds it too) or a function ``op(a, b)`` of two
|
|
117
|
+
numbers; NaN where either is NaN or the result is undefined. It is drawn wherever a
|
|
118
|
+
signal reader is: ``bigwig``, heat strips, the site matrix.
|
|
119
|
+
|
|
120
|
+
>>> fig.bigwig(plt.genome.combine("treat.bw", "input.bw", "log2ratio",
|
|
121
|
+
... pseudo_count=1))
|
|
122
|
+
"""
|
|
123
|
+
return Combined(a, b, op, pseudo_count)
|