Skip to content

Commit ed4d82e

Browse files
authored
Improve detection of array names in visualize (#835)
* Find arrays in Xarray datasets * Fix bug where cubed_xarray wasn't being considered * Filter out variable names starting with _ * Show literal value of scalar arrays
1 parent 044c303 commit ed4d82e

4 files changed

Lines changed: 62 additions & 28 deletions

File tree

cubed/array_api/creation_functions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ def asarray(
7272
name = gensym()
7373
target = virtual_in_memory(a, chunks=chunksize)
7474

75-
plan = Plan._new(name, "asarray", target)
75+
scalar_value = str(a) if a.shape == () else None
76+
plan = Plan._new(name, "asarray", target, scalar_value=scalar_value)
7677
return Array(name, target, spec, plan)
7778

7879

cubed/core/ops.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ def blockwise(
395395
op.target_array,
396396
op,
397397
False,
398+
None,
398399
*source_arrays,
399400
)
400401
from cubed.array_api import Array
@@ -555,6 +556,7 @@ def _general_blockwise(
555556
op.target_array,
556557
op,
557558
False,
559+
None,
558560
*source_arrays,
559561
)
560562
from cubed.array_api import Array
@@ -975,6 +977,7 @@ def rechunk(x, chunks, *, target_store=None, min_mem=None, use_new_impl=True):
975977
op.target_array,
976978
op,
977979
False,
980+
None,
978981
x,
979982
)
980983
return Array(name, op.target_array, spec, plan)
@@ -987,6 +990,7 @@ def rechunk(x, chunks, *, target_store=None, min_mem=None, use_new_impl=True):
987990
op1.target_array,
988991
op1,
989992
False,
993+
None,
990994
x,
991995
)
992996
x_int = Array(name_int, op1.target_array, spec, plan1)
@@ -998,6 +1002,7 @@ def rechunk(x, chunks, *, target_store=None, min_mem=None, use_new_impl=True):
9981002
op2.target_array,
9991003
op2,
10001004
False,
1005+
None,
10011006
x_int,
10021007
)
10031008
return Array(name, op2.target_array, spec, plan2)

cubed/core/plan.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from cubed.storage.zarr import LazyZarrArray, open_if_lazy_zarr_array
2121
from cubed.utils import (
2222
chunk_memory,
23+
extract_array_names_from_stack_summaries,
2324
extract_stack_summaries,
2425
is_local_path,
2526
itemsize,
@@ -90,6 +91,7 @@ def _new(
9091
target,
9192
primitive_op=None,
9293
hidden=False,
94+
scalar_value=None,
9395
*source_arrays,
9496
):
9597
# create an empty DAG or combine from sources
@@ -137,6 +139,7 @@ def _new(
137139
type="array",
138140
target=target,
139141
hidden=hidden,
142+
scalar_value=scalar_value,
140143
)
141144
dag.add_edge(op_name_unique, name)
142145
else:
@@ -582,21 +585,17 @@ def visualize(
582585
dag.graph["node"] = {"fontname": "helvetica", "shape": "box", "fontsize": "10"}
583586

584587
# do an initial pass to extract array variable names from stack summaries
585-
array_display_names = {}
588+
stacks = []
586589
for _, d in dag.nodes(data=True):
587590
if "stack_summaries" in d:
588591
stack_summaries = d["stack_summaries"]
589-
first_cubed_i = min(
590-
i for i, s in enumerate(stack_summaries) if s.is_cubed()
591-
)
592-
caller_summary = stack_summaries[first_cubed_i - 1]
593-
array_display_names.update(caller_summary.array_names_to_variable_names)
592+
stacks.append(stack_summaries)
594593
# add current stack info
595-
frame = inspect.currentframe().f_back # go back one in the stack
594+
# go back one in the stack to the caller of 'visualize'
595+
frame = inspect.currentframe().f_back
596596
stack_summaries = extract_stack_summaries(frame, limit=10)
597-
first_cubed_i = min(i for i, s in enumerate(stack_summaries) if s.is_cubed())
598-
caller_summary = stack_summaries[first_cubed_i - 1]
599-
array_display_names.update(caller_summary.array_names_to_variable_names)
597+
stacks.append(stack_summaries)
598+
array_display_names = extract_array_names_from_stack_summaries(stacks)
600599

601600
# now set node attributes with visualization info
602601
for n, d in dag.nodes(data=True):
@@ -680,6 +679,10 @@ def visualize(
680679
var_name = array_display_names[n]
681680
label = f"{n}\n{var_name}"
682681
tooltip += f"variable: {var_name}\n"
682+
if "scalar_value" in d and d["scalar_value"] is not None:
683+
scalar_value = d["scalar_value"]
684+
label += f"\n{scalar_value}"
685+
tooltip += f"value: {scalar_value}\n"
683686
tooltip += f"shape: {target.shape}\n"
684687
tooltip += f"chunks: {target.chunks}\n"
685688
tooltip += f"dtype: {target.dtype}\n"

cubed/utils.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
import sys
77
import sysconfig
88
import traceback
9-
from collections.abc import Iterator
9+
from collections.abc import Iterable, Iterator
1010
from dataclasses import dataclass
1111
from functools import partial
1212
from itertools import islice
1313
from math import prod
1414
from operator import add, mul
1515
from pathlib import Path
1616
from posixpath import join
17+
from types import FrameType
1718
from typing import Dict, Optional, Tuple, Union, cast
1819
from urllib.parse import quote, unquote, urlsplit, urlunsplit
1920

@@ -169,22 +170,24 @@ class StackSummary:
169170
module: str
170171
array_names_to_variable_names: dict
171172

172-
def is_cubed(self):
173+
def is_cubed(self) -> bool:
173174
"""Return True if this stack frame is a Cubed call."""
174-
return self.module.startswith("cubed.") and not self.module.startswith(
175-
"cubed.tests."
176-
)
175+
return (
176+
self.module.startswith("cubed.") or self.module.startswith("cubed_xarray.")
177+
) and not self.module.startswith("cubed.tests.")
177178

178-
def is_on_python_lib_path(self):
179+
def is_on_python_lib_path(self) -> bool:
179180
"""Return True if this stack frame is from a library on Python's library path."""
180181
python_lib_path = sysconfig.get_path("purelib")
181182

182183
return self.filename.startswith(python_lib_path)
183184

184185

185-
def extract_stack_summaries(frame, limit=None):
186+
def extract_stack_summaries(
187+
frame: Optional[FrameType], limit: Optional[int] = None
188+
) -> list[StackSummary]:
186189
"""Like Python's ``StackSummary.extract``, but returns module information."""
187-
frame_gen = traceback.walk_stack(frame)
190+
frame_gen: Iterable[tuple[FrameType, int]] = traceback.walk_stack(frame)
188191

189192
# from StackSummary.extract
190193
if limit is None:
@@ -214,24 +217,46 @@ def extract_stack_summaries(frame, limit=None):
214217
return stack_summaries
215218

216219

217-
def extract_array_names(frame):
220+
def extract_array_names(frame: FrameType) -> dict[str, str]:
218221
"""Look for Cubed arrays in local variables to create a mapping from (internally generated) array names to variable names."""
219222

220223
from cubed import Array
221224

222-
array_names_to_variable_names = {}
223-
for var, arr in frame.f_locals.items():
224-
if isinstance(arr, Array):
225-
array_names_to_variable_names[arr.name] = var
225+
array_names_to_variable_names: dict[str, str] = {}
226+
for name, obj in frame.f_locals.items():
227+
if name.startswith("_"):
228+
# ignore previous output variables like _, __, and _1 (IPython)
229+
continue
230+
if isinstance(obj, Array):
231+
array_names_to_variable_names[obj.name] = name
232+
elif (
233+
type(obj).__module__.split(".")[0] == "xarray"
234+
and obj.__class__.__name__ == "DataArray"
235+
):
236+
if isinstance(obj.data, Array):
237+
array_names_to_variable_names[obj.data.name] = name
226238
elif (
227-
type(arr).__module__.split(".")[0] == "xarray"
228-
and arr.__class__.__name__ == "DataArray"
239+
type(obj).__module__.split(".")[0] == "xarray"
240+
and obj.__class__.__name__ == "Dataset"
229241
):
230-
if isinstance(arr.data, Array):
231-
array_names_to_variable_names[arr.data.name] = arr.name
242+
for var in obj.data_vars:
243+
da = obj[var]
244+
if isinstance(da.data, Array):
245+
array_names_to_variable_names[da.data.name] = f"{name}.{var}"
232246
return array_names_to_variable_names
233247

234248

249+
def extract_array_names_from_stack_summaries(
250+
stacks: list[list[StackSummary]],
251+
) -> dict[str, str]:
252+
array_display_names: dict[str, str] = {}
253+
for stack_summaries in stacks:
254+
first_cubed_i = min(i for i, s in enumerate(stack_summaries) if s.is_cubed())
255+
caller_summary = stack_summaries[first_cubed_i - 1]
256+
array_display_names.update(caller_summary.array_names_to_variable_names)
257+
return array_display_names
258+
259+
235260
def convert_to_bytes(size: Union[int, float, str]) -> int:
236261
"""
237262
Converts the input data size to bytes.

0 commit comments

Comments
 (0)