arrscope 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {arrscope-0.1.0 → arrscope-0.2.0}/PKG-INFO +1 -1
- {arrscope-0.1.0 → arrscope-0.2.0}/main.py +24 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/pyproject.toml +1 -1
- arrscope-0.2.0/src/arrscope/_api.py +207 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/_config.py +2 -1
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/adapters/_core.py +4 -1
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/renderers/html.py +47 -2
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/renderers/terminal.py +40 -2
- arrscope-0.1.0/src/arrscope/_api.py +0 -131
- {arrscope-0.1.0 → arrscope-0.2.0}/.gitignore +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/README.md +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/docs/ADR-001-visual-grammar.md +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/docs/ADR-002-architecture.md +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/docs/ADR-003-api-design.md +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/docs/GLOSSARY.md +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/__init__.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/__main__.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/_format.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/_layout.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/_types.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/adapters/__init__.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/src/arrscope/renderers/__init__.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/test.ipynb +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/tests/test_adapter.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/tests/test_format.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/tests/test_layout.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/tests/test_stats.py +0 -0
- {arrscope-0.1.0 → arrscope-0.2.0}/uv.lock +0 -0
|
@@ -65,4 +65,28 @@ print(scope(np.random.rand(2, 3, 4), mode="heatmap"))
|
|
|
65
65
|
print("\n1️⃣3️⃣ STATS OFF")
|
|
66
66
|
print(scope(np.eye(5), stats=False))
|
|
67
67
|
|
|
68
|
+
print("\n1️⃣4️⃣ MOSAIC — 3×4×5 tiled (no tree)")
|
|
69
|
+
arr = np.arange(60).reshape(3, 4, 5).astype(float)
|
|
70
|
+
print(scope(arr, render_style="mosaic"))
|
|
71
|
+
|
|
72
|
+
print("\n1️⃣5️⃣ MOSAIC — 4D with named axes tiled side by side")
|
|
73
|
+
print(
|
|
74
|
+
scope(
|
|
75
|
+
np.random.rand(2, 3, 6, 6),
|
|
76
|
+
axes=["batch", "heads", "h", "w"],
|
|
77
|
+
grid=["h", "w"],
|
|
78
|
+
title="Mosaic: 2×3 attention heads",
|
|
79
|
+
render_style="mosaic",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print("\n1️⃣6️⃣ MOSAIC + HEATMAP — tiled grids with diverging colors")
|
|
84
|
+
print(
|
|
85
|
+
scope(
|
|
86
|
+
np.random.rand(2, 4, 5, 5),
|
|
87
|
+
render_style="mosaic",
|
|
88
|
+
mode="heatmap",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
68
92
|
print("\n Done!")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from arrscope._config import config
|
|
6
|
+
from arrscope._format import format_value
|
|
7
|
+
from arrscope._layout import build_layout, compute_stats
|
|
8
|
+
from arrscope._types import VizOutput
|
|
9
|
+
from arrscope.adapters import to_numpy
|
|
10
|
+
from arrscope.renderers import render_html, render_terminal
|
|
11
|
+
|
|
12
|
+
Mode = Literal["dtype", "heatmap", "sparsity"]
|
|
13
|
+
RenderStyle = Literal["tree", "mosaic"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def scope(
|
|
17
|
+
arr,
|
|
18
|
+
*,
|
|
19
|
+
axes: list[str] | None = None,
|
|
20
|
+
grid: list[str | int] | None = None,
|
|
21
|
+
title: str | None = None,
|
|
22
|
+
max_width: int | None = None,
|
|
23
|
+
max_height: int | None = None,
|
|
24
|
+
fmt: str | None = None,
|
|
25
|
+
color: bool | None = None,
|
|
26
|
+
style: Literal["auto", "terminal", "html"] = "auto",
|
|
27
|
+
mode: Mode | None = None,
|
|
28
|
+
render_style: RenderStyle | None = None,
|
|
29
|
+
stats: bool | None = None,
|
|
30
|
+
) -> VizOutput:
|
|
31
|
+
"""Visualize an n-dimensional array in the terminal and/or Jupyter.
|
|
32
|
+
|
|
33
|
+
Renders a structural tree view with 2D value grids at the leaves.
|
|
34
|
+
Works with NumPy, PyTorch, TensorFlow, JAX, and tinygrad arrays.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
arr: The array to visualize. Accepts np.ndarray, torch.Tensor,
|
|
38
|
+
tf.Tensor, jax.Array, tinygrad.Tensor, or any object
|
|
39
|
+
implementing ``__array__``.
|
|
40
|
+
axes: Names for each dimension, e.g. ``["batch", "heads", "h", "w"]``.
|
|
41
|
+
Used in headers and for ``grid`` axis selection by name.
|
|
42
|
+
grid: Which dimensions form the leaf 2D grid. Given as axis indices
|
|
43
|
+
(``[0, 1]``), axis names (``["h", "w"]``), or omitted to
|
|
44
|
+
default to the last two dimensions.
|
|
45
|
+
title: Optional heading displayed above the visualization.
|
|
46
|
+
max_height: Max tree rows before truncation. The first half and
|
|
47
|
+
last half of children are shown with ``... (N more)`` in between.
|
|
48
|
+
max_width: Max character width (terminal only). Not yet enforced.
|
|
49
|
+
fmt: Format spec for float values, e.g. ``".4f"``, ``".2e"``.
|
|
50
|
+
Auto-detected from value range when not set.
|
|
51
|
+
color: Enable or disable ANSI / CSS color output.
|
|
52
|
+
Defaults to ``True``.
|
|
53
|
+
style: Output target. ``"auto"`` detects Jupyter and renders
|
|
54
|
+
HTML there, ANSI elsewhere. ``"terminal"`` forces ANSI,
|
|
55
|
+
``"html"`` forces HTML.
|
|
56
|
+
mode: Colorization strategy:
|
|
57
|
+
|
|
58
|
+
- ``"dtype"`` — Semantic colors by data type (blue=float,
|
|
59
|
+
green=int, magenta=bool, yellow=str, red=complex).
|
|
60
|
+
Negative values tinted red, positive tinted blue.
|
|
61
|
+
- ``"heatmap"`` — Diverging colormap (red → light → blue)
|
|
62
|
+
scaled to the array's global min/max range. Best for
|
|
63
|
+
spotting value patterns.
|
|
64
|
+
- ``"sparsity"`` — Zeros rendered as dim ``·``, non-zero
|
|
65
|
+
values in bold. Best for inspecting sparse or masked arrays.
|
|
66
|
+
|
|
67
|
+
render_style: Visual layout style:
|
|
68
|
+
|
|
69
|
+
- ``"tree"`` (default) — Hierarchical branch view with 2D
|
|
70
|
+
grids nested at the leaves. Each depth level gets a
|
|
71
|
+
distinct branch color for readability.
|
|
72
|
+
- ``"mosaic"`` — All 2D sub-slices tiled side by side in a
|
|
73
|
+
contact-sheet grid. Best for comparing slices in 3D/4D
|
|
74
|
+
arrays (batches, attention heads, channels).
|
|
75
|
+
|
|
76
|
+
stats: Show summary statistics below the tree (min, max, mean,
|
|
77
|
+
std, zeros %, NaN count). Defaults to ``True``.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A ``VizOutput`` object with ``.ansi`` (str) and ``.html`` (str)
|
|
81
|
+
attributes. In Jupyter it auto-displays via ``_repr_html_``.
|
|
82
|
+
In the terminal, ``print()`` or ``str()`` renders the ANSI output.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
>>> import numpy as np
|
|
86
|
+
>>> from arrscope import scope
|
|
87
|
+
>>> scope(np.random.rand(3, 4, 5))
|
|
88
|
+
|
|
89
|
+
>>> scope(np.eye(5), title="Identity", mode="sparsity")
|
|
90
|
+
|
|
91
|
+
>>> scope(
|
|
92
|
+
... np.random.rand(2, 8, 32, 32),
|
|
93
|
+
... axes=["batch", "heads", "h", "w"],
|
|
94
|
+
... mode="heatmap",
|
|
95
|
+
... )
|
|
96
|
+
"""
|
|
97
|
+
arr_np = to_numpy(arr)
|
|
98
|
+
|
|
99
|
+
color = config.color if color is None else color
|
|
100
|
+
max_width = max_width or config.max_width
|
|
101
|
+
max_height = max_height or config.max_height
|
|
102
|
+
mode = mode or config.mode
|
|
103
|
+
rs = render_style or config.render_style
|
|
104
|
+
show_stats = config.stats if stats is None else stats
|
|
105
|
+
|
|
106
|
+
if mode not in ("dtype", "heatmap", "sparsity"):
|
|
107
|
+
raise ValueError(f"Unknown mode '{mode}'. Expected one of: dtype, heatmap, sparsity")
|
|
108
|
+
|
|
109
|
+
if rs not in ("tree", "mosaic"):
|
|
110
|
+
raise ValueError(f"Unknown render_style '{rs}'. Expected one of: tree, mosaic")
|
|
111
|
+
|
|
112
|
+
grid_dims = _resolve_grid_dims(arr_np, axes, grid)
|
|
113
|
+
|
|
114
|
+
node = build_layout(
|
|
115
|
+
arr_np,
|
|
116
|
+
grid_dims=grid_dims,
|
|
117
|
+
max_height=max_height,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
global_stats = compute_stats(arr_np) if (show_stats or mode == "heatmap") else None
|
|
121
|
+
if global_stats and show_stats:
|
|
122
|
+
node.stats = global_stats
|
|
123
|
+
|
|
124
|
+
use_html = _use_html(style)
|
|
125
|
+
use_terminal = _use_terminal(style)
|
|
126
|
+
|
|
127
|
+
output = VizOutput()
|
|
128
|
+
|
|
129
|
+
if use_html:
|
|
130
|
+
output.html = _wrap_html(
|
|
131
|
+
render_html(node, color=color, mode=mode, global_stats=global_stats, render_style=rs), title
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if use_terminal:
|
|
135
|
+
output.ansi = _wrap_ansi(
|
|
136
|
+
render_terminal(node, color=color, mode=mode, global_stats=global_stats, render_style=rs), title
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return output
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _resolve_grid_dims(
|
|
143
|
+
arr: np.ndarray,
|
|
144
|
+
axes: list[str] | None,
|
|
145
|
+
grid: list[str | int] | None,
|
|
146
|
+
) -> list[int] | None:
|
|
147
|
+
if grid is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
if axes is None:
|
|
151
|
+
return [g if isinstance(g, int) else arr.ndim - 2 + (list(axes or []).index(g))
|
|
152
|
+
for g in grid]
|
|
153
|
+
|
|
154
|
+
name_to_idx = {name: i for i, name in enumerate(axes)}
|
|
155
|
+
resolved: list[int] = []
|
|
156
|
+
for g in grid:
|
|
157
|
+
if isinstance(g, str):
|
|
158
|
+
if g not in name_to_idx:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Axis '{g}' not found in axes={axes}. "
|
|
161
|
+
f"Available axes: {list(name_to_idx.keys())}"
|
|
162
|
+
)
|
|
163
|
+
resolved.append(name_to_idx[g])
|
|
164
|
+
else:
|
|
165
|
+
resolved.append(g)
|
|
166
|
+
return resolved
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _use_html(style: str) -> bool:
|
|
170
|
+
if style == "html":
|
|
171
|
+
return True
|
|
172
|
+
if style == "terminal":
|
|
173
|
+
return False
|
|
174
|
+
return _in_jupyter()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _use_terminal(style: str) -> bool:
|
|
178
|
+
if style == "terminal":
|
|
179
|
+
return True
|
|
180
|
+
if style == "html":
|
|
181
|
+
return False
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _in_jupyter() -> bool:
|
|
186
|
+
try:
|
|
187
|
+
from IPython import get_ipython
|
|
188
|
+
|
|
189
|
+
shell = get_ipython()
|
|
190
|
+
return shell is not None and "IPKernelApp" in shell.config
|
|
191
|
+
except (ImportError, AttributeError):
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _wrap_html(body: str, title: str | None) -> str:
|
|
196
|
+
if title:
|
|
197
|
+
return f'<h3 style="font-family: sans-serif; margin: 4px 0;">{title}</h3>\n{body}'
|
|
198
|
+
return body
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _wrap_ansi(body: str, title: str | None) -> str:
|
|
202
|
+
if title:
|
|
203
|
+
from rich.text import Text
|
|
204
|
+
|
|
205
|
+
t = Text(title, style="bold")
|
|
206
|
+
return f"{t}\n{body}"
|
|
207
|
+
return body
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
@dataclass
|
|
@@ -8,6 +8,7 @@ class Config:
|
|
|
8
8
|
color: bool = True
|
|
9
9
|
precision: int | None = None
|
|
10
10
|
mode: str = "dtype"
|
|
11
|
+
render_style: str = "tree"
|
|
11
12
|
stats: bool = True
|
|
12
13
|
|
|
13
14
|
|
|
@@ -23,11 +23,14 @@ def to_numpy(arr) -> np.ndarray:
|
|
|
23
23
|
if "jax" in module:
|
|
24
24
|
return np.asarray(arr)
|
|
25
25
|
|
|
26
|
+
if "tinygrad" in module:
|
|
27
|
+
return arr.numpy()
|
|
28
|
+
|
|
26
29
|
try:
|
|
27
30
|
return np.asarray(arr)
|
|
28
31
|
except (TypeError, ValueError) as e:
|
|
29
32
|
raise TypeError(
|
|
30
33
|
f"Cannot convert {type(arr).__name__} to numpy array. "
|
|
31
34
|
f"Supported types: np.ndarray, torch.Tensor, tf.Tensor, jax.Array, "
|
|
32
|
-
f"and objects implementing __array__."
|
|
35
|
+
f"tinygrad.Tensor, and objects implementing __array__."
|
|
33
36
|
) from e
|
|
@@ -13,9 +13,14 @@ def render_html(
|
|
|
13
13
|
color: bool = True,
|
|
14
14
|
mode: str = "dtype",
|
|
15
15
|
global_stats: dict | None = None,
|
|
16
|
+
render_style: str = "tree",
|
|
16
17
|
) -> str:
|
|
17
18
|
css = _css()
|
|
18
|
-
|
|
19
|
+
|
|
20
|
+
if render_style == "mosaic":
|
|
21
|
+
body = _render_mosaic_html(node, mode, global_stats)
|
|
22
|
+
else:
|
|
23
|
+
body = _render_node_html(node, 0, mode, global_stats)
|
|
19
24
|
|
|
20
25
|
stats_html = ""
|
|
21
26
|
if global_stats and node.stats:
|
|
@@ -28,6 +33,32 @@ def render_html(
|
|
|
28
33
|
</div>"""
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
def _collect_leaves(node: VizNode) -> list[VizNode]:
|
|
37
|
+
if node.grid_data is not None:
|
|
38
|
+
return [node]
|
|
39
|
+
leaves: list[VizNode] = []
|
|
40
|
+
for child in node.children:
|
|
41
|
+
leaves.extend(_collect_leaves(child))
|
|
42
|
+
return leaves
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _render_mosaic_html(
|
|
46
|
+
node: VizNode,
|
|
47
|
+
mode: str,
|
|
48
|
+
global_stats: dict | None,
|
|
49
|
+
) -> str:
|
|
50
|
+
leaves = _collect_leaves(node)
|
|
51
|
+
if not leaves:
|
|
52
|
+
return "<span>(empty)</span>"
|
|
53
|
+
|
|
54
|
+
tiles: list[str] = []
|
|
55
|
+
for leaf in leaves:
|
|
56
|
+
if leaf.grid_data is not None:
|
|
57
|
+
tiles.append(_render_grid_html(leaf.grid_data, leaf.label, mode, global_stats))
|
|
58
|
+
|
|
59
|
+
return '<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start;">' + "".join(tiles) + "</div>"
|
|
60
|
+
|
|
61
|
+
|
|
31
62
|
def _css() -> str:
|
|
32
63
|
return """
|
|
33
64
|
.arrscope-wrapper {
|
|
@@ -63,6 +94,13 @@ def _css() -> str:
|
|
|
63
94
|
}
|
|
64
95
|
.arrscope-row { display: flex; flex-wrap: wrap; gap: 12px; }
|
|
65
96
|
.arrscope-tree { margin-left: 16px; }
|
|
97
|
+
.arrscope-level-0 { }
|
|
98
|
+
.arrscope-level-1 { border-left: 2px solid #00bcd4; padding-left: 8px; }
|
|
99
|
+
.arrscope-level-2 { border-left: 2px solid #4caf50; padding-left: 8px; }
|
|
100
|
+
.arrscope-level-3 { border-left: 2px solid #ffc107; padding-left: 8px; }
|
|
101
|
+
.arrscope-level-4 { border-left: 2px solid #e91e63; padding-left: 8px; }
|
|
102
|
+
.arrscope-level-5 { border-left: 2px solid #9c27b0; padding-left: 8px; }
|
|
103
|
+
.arrscope-level-6 { border-left: 2px solid #2196f3; padding-left: 8px; }
|
|
66
104
|
.arrscope-ellipsis { color: #999; font-style: italic; margin: 2px 0; }
|
|
67
105
|
.arrscope-header {
|
|
68
106
|
font-weight: 600;
|
|
@@ -95,6 +133,12 @@ details.arrscope-details[open] > summary {
|
|
|
95
133
|
.arrscope-shape { color: #777; }
|
|
96
134
|
.arrscope-stats { color: #999; }
|
|
97
135
|
.arrscope-header { border-bottom-color: #444; }
|
|
136
|
+
.arrscope-level-1 { border-left-color: #26c6da; }
|
|
137
|
+
.arrscope-level-2 { border-left-color: #66bb6a; }
|
|
138
|
+
.arrscope-level-3 { border-left-color: #ffd54f; }
|
|
139
|
+
.arrscope-level-4 { border-left-color: #f06292; }
|
|
140
|
+
.arrscope-level-5 { border-left-color: #ab47bc; }
|
|
141
|
+
.arrscope-level-6 { border-left-color: #42a5f5; }
|
|
98
142
|
}
|
|
99
143
|
"""
|
|
100
144
|
|
|
@@ -140,7 +184,8 @@ def _render_node_html(
|
|
|
140
184
|
parts.append(
|
|
141
185
|
f"<summary>{html_mod.escape(node.label or _format_header(node))}</summary>"
|
|
142
186
|
)
|
|
143
|
-
|
|
187
|
+
level_class = f"arrscope-level-{(depth + 1) % 7}"
|
|
188
|
+
parts.append(f'<div class="arrscope-tree {level_class}">')
|
|
144
189
|
|
|
145
190
|
for child in node.children:
|
|
146
191
|
parts.append(_render_node_html(child, depth + 1, mode, global_stats))
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from rich.box import SQUARE
|
|
5
5
|
from rich.color import Color
|
|
6
|
+
from rich.columns import Columns
|
|
6
7
|
from rich.style import Style
|
|
7
8
|
from rich.table import Table
|
|
8
9
|
from rich.text import Text
|
|
@@ -11,19 +12,25 @@ from rich.tree import Tree
|
|
|
11
12
|
from arrscope._format import format_value
|
|
12
13
|
from arrscope._types import VizNode
|
|
13
14
|
|
|
15
|
+
BRANCH_COLORS = ["grey50", "cyan", "green", "yellow", "magenta", "blue"]
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
def render_terminal(
|
|
16
19
|
node: VizNode,
|
|
17
20
|
color: bool = True,
|
|
18
21
|
mode: str = "dtype",
|
|
19
22
|
global_stats: dict | None = None,
|
|
23
|
+
render_style: str = "tree",
|
|
20
24
|
) -> str:
|
|
21
25
|
from rich.console import Console
|
|
22
26
|
|
|
23
27
|
console = Console(width=120, color_system="truecolor" if color else None)
|
|
24
28
|
parts: list = []
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
if render_style == "mosaic":
|
|
31
|
+
body = _render_mosaic(node, color, mode, global_stats)
|
|
32
|
+
else:
|
|
33
|
+
body = _render_node(node, 0, color, mode, global_stats)
|
|
27
34
|
parts.append(body)
|
|
28
35
|
|
|
29
36
|
if global_stats and node.stats:
|
|
@@ -35,6 +42,34 @@ def render_terminal(
|
|
|
35
42
|
return capture.get()
|
|
36
43
|
|
|
37
44
|
|
|
45
|
+
def _collect_leaves(node: VizNode) -> list[VizNode]:
|
|
46
|
+
if node.grid_data is not None:
|
|
47
|
+
return [node]
|
|
48
|
+
leaves: list[VizNode] = []
|
|
49
|
+
for child in node.children:
|
|
50
|
+
leaves.extend(_collect_leaves(child))
|
|
51
|
+
return leaves
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _render_mosaic(
|
|
55
|
+
node: VizNode,
|
|
56
|
+
color: bool,
|
|
57
|
+
mode: str,
|
|
58
|
+
global_stats: dict | None,
|
|
59
|
+
) -> Columns | Text:
|
|
60
|
+
leaves = _collect_leaves(node)
|
|
61
|
+
if not leaves:
|
|
62
|
+
return Text("(empty)")
|
|
63
|
+
|
|
64
|
+
tables: list[Table] = []
|
|
65
|
+
for leaf in leaves:
|
|
66
|
+
if leaf.grid_data is not None:
|
|
67
|
+
t = _render_grid(leaf.grid_data, leaf.label, color, mode, global_stats)
|
|
68
|
+
tables.append(t)
|
|
69
|
+
|
|
70
|
+
return Columns(tables, equal=True, expand=False)
|
|
71
|
+
|
|
72
|
+
|
|
38
73
|
def _stats_text(stats: dict, color: bool) -> Text:
|
|
39
74
|
dim = Style(dim=True, color="grey50") if color else Style(dim=True)
|
|
40
75
|
t = Text()
|
|
@@ -76,9 +111,12 @@ def _render_node(
|
|
|
76
111
|
t.stylize(Style(dim=True))
|
|
77
112
|
return t
|
|
78
113
|
|
|
114
|
+
guide_color = BRANCH_COLORS[(depth) % len(BRANCH_COLORS)]
|
|
115
|
+
guide = Style(dim=True, color=guide_color) if color else Style(dim=True)
|
|
116
|
+
|
|
79
117
|
tree = Tree(
|
|
80
118
|
_make_header(node, color),
|
|
81
|
-
guide_style=
|
|
119
|
+
guide_style=guide,
|
|
82
120
|
)
|
|
83
121
|
for child in node.children:
|
|
84
122
|
child_renderable = _render_node(child, depth + 1, color, mode, global_stats)
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from arrscope._config import config
|
|
4
|
-
from arrscope._format import format_value
|
|
5
|
-
from arrscope._layout import build_layout, compute_stats
|
|
6
|
-
from arrscope._types import VizOutput
|
|
7
|
-
from arrscope.adapters import to_numpy
|
|
8
|
-
from arrscope.renderers import render_html, render_terminal
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def scope(
|
|
12
|
-
arr,
|
|
13
|
-
*,
|
|
14
|
-
axes: list[str] | None = None,
|
|
15
|
-
grid: list[str | int] | None = None,
|
|
16
|
-
title: str | None = None,
|
|
17
|
-
max_width: int | None = None,
|
|
18
|
-
max_height: int | None = None,
|
|
19
|
-
fmt: str | None = None,
|
|
20
|
-
color: bool | None = None,
|
|
21
|
-
style: str = "auto",
|
|
22
|
-
mode: str | None = None,
|
|
23
|
-
stats: bool | None = None,
|
|
24
|
-
) -> VizOutput:
|
|
25
|
-
arr_np = to_numpy(arr)
|
|
26
|
-
|
|
27
|
-
color = config.color if color is None else color
|
|
28
|
-
max_width = max_width or config.max_width
|
|
29
|
-
max_height = max_height or config.max_height
|
|
30
|
-
mode = mode or config.mode
|
|
31
|
-
show_stats = config.stats if stats is None else stats
|
|
32
|
-
|
|
33
|
-
if mode not in ("dtype", "heatmap", "sparsity"):
|
|
34
|
-
raise ValueError(f"Unknown mode '{mode}'. Expected one of: dtype, heatmap, sparsity")
|
|
35
|
-
|
|
36
|
-
grid_dims = _resolve_grid_dims(arr_np, axes, grid)
|
|
37
|
-
|
|
38
|
-
node = build_layout(
|
|
39
|
-
arr_np,
|
|
40
|
-
grid_dims=grid_dims,
|
|
41
|
-
max_height=max_height,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
global_stats = compute_stats(arr_np) if (show_stats or mode == "heatmap") else None
|
|
45
|
-
if global_stats and show_stats:
|
|
46
|
-
node.stats = global_stats
|
|
47
|
-
|
|
48
|
-
use_html = _use_html(style)
|
|
49
|
-
use_terminal = _use_terminal(style)
|
|
50
|
-
|
|
51
|
-
output = VizOutput()
|
|
52
|
-
|
|
53
|
-
if use_html:
|
|
54
|
-
output.html = _wrap_html(
|
|
55
|
-
render_html(node, color=color, mode=mode, global_stats=global_stats), title
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if use_terminal:
|
|
59
|
-
output.ansi = _wrap_ansi(
|
|
60
|
-
render_terminal(node, color=color, mode=mode, global_stats=global_stats), title
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return output
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _resolve_grid_dims(
|
|
67
|
-
arr: np.ndarray,
|
|
68
|
-
axes: list[str] | None,
|
|
69
|
-
grid: list[str | int] | None,
|
|
70
|
-
) -> list[int] | None:
|
|
71
|
-
if grid is None:
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
if axes is None:
|
|
75
|
-
return [g if isinstance(g, int) else arr.ndim - 2 + (list(axes or []).index(g))
|
|
76
|
-
for g in grid]
|
|
77
|
-
|
|
78
|
-
name_to_idx = {name: i for i, name in enumerate(axes)}
|
|
79
|
-
resolved: list[int] = []
|
|
80
|
-
for g in grid:
|
|
81
|
-
if isinstance(g, str):
|
|
82
|
-
if g not in name_to_idx:
|
|
83
|
-
raise ValueError(
|
|
84
|
-
f"Axis '{g}' not found in axes={axes}. "
|
|
85
|
-
f"Available axes: {list(name_to_idx.keys())}"
|
|
86
|
-
)
|
|
87
|
-
resolved.append(name_to_idx[g])
|
|
88
|
-
else:
|
|
89
|
-
resolved.append(g)
|
|
90
|
-
return resolved
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _use_html(style: str) -> bool:
|
|
94
|
-
if style == "html":
|
|
95
|
-
return True
|
|
96
|
-
if style == "terminal":
|
|
97
|
-
return False
|
|
98
|
-
return _in_jupyter()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _use_terminal(style: str) -> bool:
|
|
102
|
-
if style == "terminal":
|
|
103
|
-
return True
|
|
104
|
-
if style == "html":
|
|
105
|
-
return False
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _in_jupyter() -> bool:
|
|
110
|
-
try:
|
|
111
|
-
from IPython import get_ipython
|
|
112
|
-
|
|
113
|
-
shell = get_ipython()
|
|
114
|
-
return shell is not None and "IPKernelApp" in shell.config
|
|
115
|
-
except (ImportError, AttributeError):
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _wrap_html(body: str, title: str | None) -> str:
|
|
120
|
-
if title:
|
|
121
|
-
return f'<h3 style="font-family: sans-serif; margin: 4px 0;">{title}</h3>\n{body}'
|
|
122
|
-
return body
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _wrap_ansi(body: str, title: str | None) -> str:
|
|
126
|
-
if title:
|
|
127
|
-
from rich.text import Text
|
|
128
|
-
|
|
129
|
-
t = Text(title, style="bold")
|
|
130
|
-
return f"{t}\n{body}"
|
|
131
|
-
return body
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|