Skip to content

Commit 1355733

Browse files
committed
basic minimal ndw orchestration working
1 parent 777a1d5 commit 1355733

7 files changed

Lines changed: 266 additions & 17 deletions

File tree

fastplotlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
else:
2020
from .layouts import Figure
2121

22-
from .widgets import ImageWidget
22+
from .widgets import NDWidget, ImageWidget
2323
from .utils import config, enumerate_adapters, select_adapter, print_wgpu_report
2424

2525

fastplotlib/widgets/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .nd_widget import NDWidget
12
from .image_widget import ImageWidget
23

3-
__all__ = ["ImageWidget"]
4+
__all__ = ["NDWidget", "ImageWidget"]
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1-
from .processor_base import NDProcessor
2-
from ._nd_positions import NDPositions, NDPositionsProcessor, ndp_extras
3-
from ._nd_image import NDImageProcessor, NDImage
1+
from ...layouts import IMGUI
2+
3+
if IMGUI:
4+
from .base import NDProcessor
5+
from ._nd_positions import NDPositions, NDPositionsProcessor, ndp_extras
6+
from ._nd_image import NDImageProcessor, NDImage
7+
from .ndwidget import NDWidget
8+
else:
9+
class NDWidget:
10+
def __init__(self, *args, **kwargs):
11+
raise ModuleNotFoundError(
12+
"NDWidget requires `imgui-bundle` to be installed.\n"
13+
"pip install imgui-bundle"
14+
)

fastplotlib/widgets/nd_widget/_nd_image.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77

88
from ...utils import subsample_array, ArrayProtocol, ARRAY_LIKE_ATTRS
99
from ...graphics import ImageGraphic, ImageVolumeGraphic
10-
from .processor_base import NDProcessor
11-
12-
# must take arguments: array-like, `axis`: int, `keepdims`: bool
13-
WindowFuncCallable = Callable[[ArrayLike, int, bool], ArrayLike]
10+
from .base import NDProcessor, NDGraphic, WindowFuncCallable
1411

1512

1613
class NDImageProcessor(NDProcessor):
@@ -526,7 +523,7 @@ def _recompute_histogram(self):
526523
self._histogram = np.histogram(sub_real, bins=100)
527524

528525

529-
class NDImage:
526+
class NDImage(NDGraphic):
530527
def __init__(
531528
self,
532529
data: Any,
@@ -538,6 +535,7 @@ def __init__(
538535
index_mappings: tuple[Callable[[Any], int] | None] | None = None,
539536
graphic_kwargs: dict = None,
540537
processor_kwargs: dict = None,
538+
name: str = None,
541539
):
542540
if processor_kwargs is None:
543541
processor_kwargs = dict()
@@ -557,6 +555,8 @@ def __init__(
557555

558556
self._create_graphic()
559557

558+
self._name = name
559+
560560
@property
561561
def processor(self) -> NDImageProcessor:
562562
return self._processor

fastplotlib/widgets/nd_widget/_nd_positions/core.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
ScatterGraphic,
1717
ScatterCollection,
1818
)
19-
from ..processor_base import NDProcessor, WindowFuncCallable
19+
from ..base import NDProcessor, NDGraphic, WindowFuncCallable
2020

2121

2222
# TODO: Maybe get rid of n_display_dims in NDProcessor,
@@ -210,7 +210,8 @@ def _get_dw_slices(self, indices) -> tuple[slice] | tuple[slice, slice]:
210210
if index_p_start >= index_p_stop:
211211
index_p_stop = index_p_start + 1
212212

213-
slices = [slice(index_p_start, index_p_stop)]
213+
# round to the nearest integer since to use as arra indices
214+
slices = [slice(round(index_p_start), round(index_p_stop))]
214215

215216
if self.multi:
216217
slices.insert(0, slice(None))
@@ -225,19 +226,18 @@ def get(self, indices: tuple[Any, ...]):
225226
index for each dimension. Slices are not allowed, therefore __getitem__ is not suitable here.
226227
"""
227228
# apply any slider index mappings
228-
indices = tuple([m(i) for m, i in zip(self.index_mappings, indices)])
229+
array_indices = tuple([m(i) for m, i in zip(self.index_mappings, indices)])
229230

230-
if len(indices) > 1:
231+
if len(array_indices) > 1:
231232
# there are dims in addition to the n_datapoints dim
232233
# apply window funcs
233234
# window_output array should be of shape [n_datapoints, 2 | 3]
234-
window_output = self._apply_window_functions(indices[:-1]).squeeze()
235+
window_output = self._apply_window_functions(array_indices[:-1]).squeeze()
235236
else:
236237
window_output = self.data
237238

238-
# TODO: window function on the `p` n_datapoints dimension
239-
240239
if self.display_window is not None:
240+
# display_window is in reference units
241241
slices = self._get_dw_slices(indices)
242242

243243
# if self.display_window is not None:

fastplotlib/widgets/nd_widget/processor_base.py renamed to fastplotlib/widgets/nd_widget/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from numpy.typing import ArrayLike
77

88
from ...utils import subsample_array, ArrayProtocol
9+
from ...graphics import Graphic
910

1011
# must take arguments: array-like, `axis`: int, `keepdims`: bool
1112
WindowFuncCallable = Callable[[ArrayLike, int, bool], ArrayLike]
@@ -249,3 +250,25 @@ def _validate_index_mappings(self, maps):
249250

250251
def __getitem__(self, item: tuple[Any, ...]) -> ArrayProtocol:
251252
pass
253+
254+
255+
class NDGraphic:
256+
@property
257+
def name(self) -> str:
258+
return self._name
259+
260+
@property
261+
def processor(self) -> NDProcessor:
262+
raise NotImplementedError
263+
264+
@property
265+
def graphic(self) -> Graphic:
266+
raise NotImplementedError
267+
268+
@property
269+
def indices(self) -> tuple[Any]:
270+
raise NotImplementedError
271+
272+
@indices.setter
273+
def indices(self, new: tuple):
274+
raise NotImplementedError
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from dataclasses import dataclass
2+
import os
3+
from time import perf_counter
4+
from typing import Any, Sequence
5+
6+
from imgui_bundle import imgui, icons_fontawesome_6 as fa
7+
import numpy as np
8+
9+
from ...layouts import ImguiFigure, Subplot
10+
from ...graphics import ScatterCollection, LineCollection, LineStack, ImageGraphic
11+
from ...ui import EdgeWindow
12+
from .base import NDGraphic, NDProcessor
13+
from ._nd_image import NDImage, NDImageProcessor
14+
from ._nd_positions import NDPositions, NDPositionsProcessor
15+
16+
17+
@dataclass
18+
class ReferenceRangeContinuous:
19+
start: int | float
20+
stop: int | float
21+
step: int | float
22+
unit: str
23+
24+
def __getitem__(self, index: int):
25+
"""return the value at the index w.r.t. the step size"""
26+
# if index is negative, turn to positive index
27+
if index < 0:
28+
raise ValueError("negative indexing not supported")
29+
30+
val = self.start + (self.step * index)
31+
if not self.start <= val <= self.stop:
32+
raise IndexError(f"index: {index} value: {val} out of bounds: [{self.start}, {self.stop}]")
33+
34+
return val
35+
36+
37+
@dataclass
38+
class ReferenceRangeDiscrete:
39+
options: Sequence[Any]
40+
unit: str
41+
42+
def __getitem__(self, index: int):
43+
if index > len(self.options):
44+
raise IndexError
45+
46+
return self.options[index]
47+
48+
def __len__(self):
49+
return len(self.options)
50+
51+
52+
class NDWSubplot:
53+
def __init__(self, ndw, subplot: Subplot):
54+
self.ndw = ndw
55+
self._subplot = subplot
56+
57+
self._nd_graphics = list()
58+
59+
@property
60+
def nd_graphics(self) -> list[NDGraphic]:
61+
return self._nd_graphics
62+
63+
def __getitem__(self, key):
64+
if isinstance(key, (int, np.integer)):
65+
return self.nd_graphics[key]
66+
67+
for g in self.nd_graphics:
68+
if g.name == key:
69+
return g
70+
71+
else:
72+
raise KeyError(f"NDGraphc with given key not found: {key}")
73+
74+
def add_nd_image(self, *args, **kwargs):
75+
nd = NDImage(*args, **kwargs)
76+
self._nd_graphics.append(nd)
77+
self._subplot.add_graphic(nd.graphic)
78+
return nd
79+
80+
def add_nd_scatter(self, *args, **kwargs):
81+
nd = NDPositions(*args, graphic=ScatterCollection, multi=True, **kwargs)
82+
self._nd_graphics.append(nd)
83+
self._subplot.add_graphic(nd.graphic)
84+
85+
return nd
86+
87+
def add_nd_timeseries(self, *args, graphic: type[LineCollection | LineStack | ImageGraphic] = LineStack, **kwargs):
88+
nd = NDPositions(*args, graphic=LineStack, multi=True, **kwargs)
89+
self._nd_graphics.append(nd)
90+
self._subplot.add_graphic(nd.graphic)
91+
# TODO: think about auto-xrange for subplot camera
92+
return nd
93+
94+
def add_nd_lines(self, *args, **kwargs):
95+
nd = NDPositions(*args, graphic=LineCollection, multi=True, **kwargs)
96+
self._nd_graphics.append(nd)
97+
self._subplot.add_graphic(nd.graphic)
98+
return nd
99+
100+
# def __repr__(self):
101+
# return "NDWidget Subplot"
102+
#
103+
# def __str__(self):
104+
# return "NDWidget Subplot"
105+
106+
107+
class NDWSliders(EdgeWindow):
108+
def __init__(self, figure, size, ndwidget):
109+
super().__init__(figure=figure, size=size, title="NDWidget controls", location="bottom")
110+
self._ndwidget = ndwidget
111+
112+
# n_sliders = self._image_widget.n_sliders
113+
#
114+
# # whether or not a dimension is in play mode
115+
# self._playing: list[bool] = [False] * n_sliders
116+
#
117+
# # approximate framerate for playing
118+
# self._fps: list[int] = [20] * n_sliders
119+
#
120+
# # framerate converted to frame time
121+
# self._frame_time: list[float] = [1 / 20] * n_sliders
122+
#
123+
# # last timepoint that a frame was displayed from a given dimension
124+
# self._last_frame_time: list[float] = [perf_counter()] * n_sliders
125+
#
126+
# # loop playback
127+
# self._loop = False
128+
#
129+
# # auto-plays the ImageWidget's left-most dimension in docs galleries
130+
# if "DOCS_BUILD" in os.environ.keys():
131+
# if os.environ["DOCS_BUILD"] == "1":
132+
# self._playing[0] = True
133+
# self._loop = True
134+
#
135+
# self.pause = False
136+
137+
def update(self):
138+
indices_changed = False
139+
140+
for dim_index, (current_index, refr) in enumerate(zip(self._ndwidget.indices, self._ndwidget.ref_ranges)):
141+
if isinstance(refr, ReferenceRangeContinuous):
142+
changed, val = imgui.slider_float(
143+
v=current_index,
144+
v_min=refr.start,
145+
v_max=refr.stop,
146+
label=refr.unit
147+
)
148+
149+
if changed:
150+
new_indices = list(self._ndwidget.indices)
151+
new_indices[dim_index] = val
152+
153+
indices_changed = True
154+
155+
if indices_changed:
156+
self._ndwidget.indices = tuple(new_indices)
157+
158+
159+
class NDWidget:
160+
def __init__(self, ref_ranges: list[tuple], **kwargs):
161+
self._ref_ranges = list()
162+
163+
for r in ref_ranges:
164+
if len(r) == 4:
165+
# assume start, stop, step, unit
166+
refr = ReferenceRangeContinuous(*r)
167+
elif len(r) == 2:
168+
refr = ReferenceRangeDiscrete(*r)
169+
else:
170+
raise ValueError
171+
172+
self._ref_ranges.append(refr)
173+
174+
self._figure = ImguiFigure(**kwargs)
175+
176+
self._subplots: dict[Subplot, NDWSubplot] = dict()
177+
for subplot in self.figure:
178+
self._subplots[subplot] = NDWSubplot(self, subplot)
179+
180+
# starting index for all dims
181+
self._indices = tuple(refr[0] for refr in self.ref_ranges)
182+
183+
# hard code the expected height so that the first render looks right in tests, docs etc.
184+
ui_size = 57 + (50 * len(self.indices))
185+
186+
self._sliders_ui = NDWSliders(self.figure, ui_size, self)
187+
self.figure.add_gui(self._sliders_ui)
188+
189+
@property
190+
def figure(self) -> ImguiFigure:
191+
return self._figure
192+
193+
@property
194+
def ref_ranges(self) -> tuple[ReferenceRangeContinuous | ReferenceRangeDiscrete]:
195+
return tuple(self._ref_ranges)
196+
197+
@property
198+
def indices(self) -> tuple:
199+
return self._indices
200+
201+
@indices.setter
202+
def indices(self, new_indices: tuple[Any]):
203+
for subplot in self._subplots.values():
204+
for ndg in subplot.nd_graphics:
205+
ndg.indices = new_indices
206+
207+
self._indices = new_indices
208+
209+
def __getitem__(self, key):
210+
subplot = self.figure[key]
211+
return self._subplots[subplot]
212+
213+
def show(self, **kwargs):
214+
return self.figure.show(**kwargs)

0 commit comments

Comments
 (0)