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.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. 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}