From 72d20404d6d7a7ed943a1f28970cd1bb9e236b23 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 05:24:32 -0400 Subject: [PATCH 1/7] gridplot controllers kwarg is back, other improvements to gp --- fastplotlib/layouts/_gridplot.py | 318 +++++++++++++++++++------------ 1 file changed, 200 insertions(+), 118 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 5f7f3086d..28893db37 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,6 +1,6 @@ from itertools import product, chain import numpy as np -from typing import Literal +from typing import Literal, Iterable from inspect import getfullargspec from warnings import warn @@ -21,16 +21,16 @@ def __init__( shape: tuple[int, int], cameras: ( Literal["2d", "3d"] - | list[Literal["2d", "3d"]] - | list[pygfx.PerspectiveCamera] - | np.ndarray + | Iterable[Iterable[Literal["2d", "3d"]]] + | pygfx.PerspectiveCamera + | Iterable[Iterable[pygfx.PerspectiveCamera]] ) = "2d", controller_types: ( - Literal["panzoom", "fly", "trackball", "orbit"] - | list[Literal["panzoom", "fly", "trackball", "orbit"]] - | np.ndarray + Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]] + | Iterable[Literal["panzoom", "fly", "trackball", "orbit"]] ) = None, - controller_ids: str | list[int] | np.ndarray | list[list[str]] = None, + controller_ids: Literal["sync"] | Iterable[int] | Iterable[Iterable[int]] | Iterable[Iterable[str]] = None, + controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | WgpuCanvasBase | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, size: tuple[int, int] = (500, 300), @@ -44,19 +44,18 @@ def __init__( shape: (int, int) (n_rows, n_cols) - cameras: "2d", "3", list of "2d" | "3d", list of camera instances, or np.ndarray of "2d" | "3d", optional + cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots - | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot - | list/array of pygfx.PerspectiveCamera instances + | Iterable/list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | Iterable/list/array of pygfx.PerspectiveCamera instances - controller_types: str, list or np.ndarray, optional - list or array that specifies the controller type for each subplot, or list/array of - pygfx.Controller instances. Valid controller types: "panzoom", "fly", "trackball", "orbit". + controller_types: str, Iterable, optional + list/array that specifies the controller type for each subplot. + Valid controller types: "panzoom", "fly", "trackball", "orbit". If not specified a default controller is chosen based on the camera type. Orthographic projections, i.e. "2d" cameras, use a "panzoom" controller by default. Perspective projections with a FOV > 0, i.e. "3d" cameras, use a "fly" controller by default. - controller_ids: str, list of int, np.ndarray of int, or list with sublists of subplot str names, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller @@ -70,6 +69,11 @@ def __init__( | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b, subplot_e], [subplot_c, subplot_d]] | this syncs subplot_a, subplot_b and subplot_e together; syncs subplot_c and subplot_d together + controllers: pygfx.Controller | list[pygfx.Controller] | np.ndarray[pygfx.Controller], optional + directly provide pygfx.Controller instances(s). Useful if you want to use a controller from an existing + plot/subplot. Other controller kwargs, i.e. ``controller_Types`` and ``controller_ids`` are ignored if + ``controllers`` are provided. + canvas: WgpuCanvas, optional Canvas for drawing @@ -83,23 +87,23 @@ def __init__( subplot names """ - self.shape = shape + self._shape = shape if names is not None: - if len(list(chain(*names))) != self.shape[0] * self.shape[1]: + if len(list(chain(*names))) != len(self): raise ValueError( "must provide same number of subplot `names` as specified by gridplot shape" ) - self.names = np.asarray(names).reshape(self.shape) + subplot_names = np.asarray(names).reshape(self.shape) else: - self.names = None + subplot_names = None canvas, renderer = make_canvas_and_renderer(canvas, renderer) if isinstance(cameras, str): # create the array representing the views for each subplot in the grid - cameras = np.array([cameras] * self.shape[0] * self.shape[1]).reshape( + cameras = np.array([cameras] * len(self)).reshape( self.shape ) @@ -110,127 +114,175 @@ def __init__( raise ValueError("Number of cameras does not match the number of subplots") # create the cameras - self._cameras = np.empty(self.shape, dtype=object) + subplot_cameras = np.empty(self.shape, dtype=object) for i, j in product(range(self.shape[0]), range(self.shape[1])): - self._cameras[i, j] = create_camera(camera_type=cameras[i, j]) + subplot_cameras[i, j] = create_camera(camera_type=cameras[i, j]) - if controller_ids is None: - # individual controller for each subplot - controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape( - self.shape - ) + # if controller instances have been specified for each subplot + if controllers is not None: + + # one controller for all subplots + if isinstance(controllers, pygfx.Controller): + controllers = [controllers] * len(self) + # subplot_controllers[:] = controllers + # # subplot_controllers = np.asarray([controllers] * len(self), dtype=object) - elif isinstance(controller_ids, str): - if controller_ids == "sync": - controller_ids = np.zeros(self.shape, dtype=int) + # individual controller instance specified for each subplot else: + # I found that this is better than list(*chain()) because chain doesn't give the right + # result we want for arrays + for item in controllers: + if isinstance(item, pygfx.Controller): + pass + elif all(isinstance(c, pygfx.Controller) for c in item): + pass + else: + raise TypeError( + "controllers argument must be a single pygfx.Controller instance of a Iterable of " + "pygfx.Controller instances" + ) + + try: + controllers = np.asarray(controllers).reshape(shape) + except ValueError: raise ValueError( - f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " - f"integer ids. See the docstring for more details." - ) + f"number of controllers passed must be the same as the number of subplots specified " + f"by shape: {self.shape}. You have passed: <{controllers.size}> controllers" + ) from None - # list controller_ids - elif isinstance(controller_ids, (list, np.ndarray)): - ids_flat = list(chain(*controller_ids)) + subplot_controllers: np.ndarray[pygfx.Controller] = np.empty(self.shape, dtype=object) - # list of str of subplot names, convert this to integer ids - if all([isinstance(item, str) for item in ids_flat]): - if self.names is None: - raise ValueError( - "must specify subplot `names` to use list of str for `controller_ids`" - ) + for i, j in product(range(self.shape[0]), range(self.shape[1])): + subplot_controllers[i, j] = controllers[i, j] + subplot_controllers[i, j].add_camera(subplot_cameras[i, j]) - # make sure each controller_id str is a subplot name - if not all([n in self.names for n in ids_flat]): - raise KeyError( - f"all `controller_ids` strings must be one of the subplot names" - ) + # parse controller_ids and controller_types to make desired controller for each supblot + else: + if controller_ids is None: + # individual controller for each subplot + controller_ids = np.arange(len(self)).reshape( + self.shape + ) - if len(ids_flat) > len(set(ids_flat)): + elif isinstance(controller_ids, str): + if controller_ids == "sync": + # this will eventually make one controller for all subplots + controller_ids = np.zeros(self.shape, dtype=int) + else: raise ValueError( - "id strings must not appear twice in `controller_ids`" + f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " + f"integer ids. See the docstring for more details." ) - # initialize controller_ids array - ids_init = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + # list controller_ids + elif isinstance(controller_ids, (list, np.ndarray)): + ids_flat = list(chain(*controller_ids)) - # set id based on subplot position for each synced sublist - for i, sublist in enumerate(controller_ids): - for name in sublist: - ids_init[self.names == name] = -( - i + 1 - ) # use negative numbers because why not + # list of str of subplot names, convert this to integer ids + if all([isinstance(item, str) for item in ids_flat]): + if subplot_names is None: + raise ValueError( + "must specify subplot `names` to use list of str for `controller_ids`" + ) - controller_ids = ids_init + # make sure each controller_id str is a subplot name + if not all([n in subplot_names for n in ids_flat]): + raise KeyError( + f"all `controller_ids` strings must be one of the subplot names" + ) - # integer ids - elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): - controller_ids = np.asarray(controller_ids).reshape(self.shape) + if len(ids_flat) > len(set(ids_flat)): + raise ValueError( + "id strings must not appear twice in `controller_ids`" + ) - else: - raise TypeError( - f"list argument to `controller_ids` must be a list of `str` or `int`, " - f"you have passed: {controller_ids}" - ) + # initialize controller_ids array + ids_init = np.arange(len(self)).reshape(self.shape) - if controller_ids.shape != self.shape: - raise ValueError( - "Number of controller_ids does not match the number of subplots" - ) + # set id based on subplot position for each synced sublist + for i, sublist in enumerate(controller_ids): + for name in sublist: + ids_init[subplot_names == name] = -( + i + 1 + ) # use negative numbers because why not - if controller_types is None: - # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array( - ["default"] * self.shape[0] * self.shape[1] - ).reshape(self.shape) - - # validate controller types - types_flat = list(chain(*controller_types)) - # str controller_type or pygfx instances - valid_str = list(valid_controller_types.keys()) + ["default"] - valid_instances = tuple(valid_controller_types.values()) - - # make sure each controller type is valid - for controller_type in types_flat: - if controller_type is None: - continue - - if (controller_type not in valid_str) and ( - not isinstance(controller_type, valid_instances) - ): - raise ValueError( - f"You have passed an invalid controller type, valid controller_types arguments are:\n" - f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" - ) + controller_ids = ids_init + + # integer ids + elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): + controller_ids = np.asarray(controller_ids).reshape(self.shape) - controller_types = np.asarray(controller_types).reshape(self.shape) + else: + raise TypeError( + f"list argument to `controller_ids` must be a list of `str` or `int`, " + f"you have passed: {controller_ids}" + ) - # make the real controllers for each subplot - self._controllers = np.empty(shape=self.shape, dtype=object) - for cid in np.unique(controller_ids): - cont_type = controller_types[controller_ids == cid] - if np.unique(cont_type).size > 1: + if controller_ids.shape != self.shape: raise ValueError( - "Multiple controller types have been assigned to the same controller id. " - "All controllers with the same id must use the same type of controller." + "Number of controller_ids does not match the number of subplots" ) - cont_type = cont_type[0] + if controller_types is None: + # `create_controller()` will auto-determine controller for each subplot based on defaults + controller_types = np.array( + ["default"] * len(self) + ).reshape(self.shape) - # get all the cameras that use this controller - cams = self._cameras[controller_ids == cid].ravel() + elif isinstance(controller_types, str): + if controller_types not in valid_controller_types.keys(): + raise ValueError( + f"invalid controller_types argument, you may pass either a single controller type as a str, or an" + f"iterable of controller types from the selection: {valid_controller_types.keys()}" + ) + + # valid controller types + types_flat = list(chain(*controller_types)) + # str controller_type or pygfx instances + valid_str = list(valid_controller_types.keys()) + ["default"] + valid_instances = tuple(valid_controller_types.values()) + + # make sure each controller type is valid + for controller_type in types_flat: + if controller_type is None: + continue + + if (controller_type not in valid_str) and ( + not isinstance(controller_type, valid_instances) + ): + raise ValueError( + f"You have passed an invalid controller type, valid controller_types arguments are:\n" + f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + ) + + controller_types: np.ndarray[pygfx.Controller] = np.asarray(controller_types).reshape(self.shape) + + # make the real controllers for each subplot + subplot_controllers = np.empty(shape=self.shape, dtype=object) + for cid in np.unique(controller_ids): + cont_type = controller_types[controller_ids == cid] + if np.unique(cont_type).size > 1: + raise ValueError( + "Multiple controller types have been assigned to the same controller id. " + "All controllers with the same id must use the same type of controller." + ) - if cont_type == "default": - # hacky fix for now because of how `create_controller()` works - cont_type = None - _controller = create_controller(controller_type=cont_type, camera=cams[0]) + cont_type = cont_type[0] - self._controllers[controller_ids == cid] = _controller + # get all the cameras that use this controller + cams = subplot_cameras[controller_ids == cid].ravel() - # add the other cameras that go with this controller - if cams.size > 1: - for cam in cams[1:]: - _controller.add_camera(cam) + if cont_type == "default": + # hacky fix for now because of how `create_controller()` works + cont_type = None + _controller = create_controller(controller_type=cont_type, camera=cams[0]) + + subplot_controllers[controller_ids == cid] = _controller + + # add the other cameras that go with this controller + if cams.size > 1: + for cam in cams[1:]: + _controller.add_camera(cam) self._canvas = canvas self._renderer = renderer @@ -243,11 +295,11 @@ def __init__( for i, j in self._get_iterator(): position = (i, j) - camera = self._cameras[i, j] - controller = self._controllers[i, j] + camera = subplot_cameras[i, j] + controller = subplot_controllers[i, j] - if self.names is not None: - name = self.names[i, j] + if subplot_names is not None: + name = subplot_names[i, j] else: name = None @@ -272,6 +324,11 @@ def __init__( RecordMixin.__init__(self) Frame.__init__(self) + @property + def shape(self) -> tuple[int, int]: + """[n_rows, n_cols]""" + return self._shape + @property def canvas(self) -> WgpuCanvasBase: """The canvas associated to this GridPlot""" @@ -282,6 +339,27 @@ def renderer(self) -> pygfx.WgpuRenderer: """The renderer associated to this GridPlot""" return self._renderer + @property + def controllers(self) -> np.ndarray[pygfx.Controller]: + """controllers, read-only array, access individual subplots to change a controller""" + controllers = np.asarray([subplot.controller for subplot in self], dtype=object).reshape(self.shape) + controllers.flags.writeable = False + return controllers + + @property + def cameras(self) -> np.ndarray[pygfx.Camera]: + """cameras, read-only array, access individual subplots to change a camera""" + cameras = np.asarray([subplot.camera for subplot in self], dtype=object).reshape(self.shape) + cameras.flags.writeable = False + return cameras + + @property + def names(self) -> np.ndarray[str]: + """subplot names, read-only array, access individual subplots to change a name""" + names = np.asarray([subplot.name for subplot in self]).reshape(self.shape) + names.flags.writeable = False + return names + def __getitem__(self, index: tuple[int, int] | str) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): @@ -389,6 +467,10 @@ def __next__(self) -> Subplot: pos = self._current_iter.__next__() return self._subplots[pos] + def __len__(self): + """number of subplots""" + return self.shape[0] * self.shape[1] + def __str__(self): return f"{self.__class__.__name__} @ {hex(id(self))}" From 9f4e8f78e6eb8a7c46e4da7ead247ac6680b9f1c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 05:34:23 -0400 Subject: [PATCH 2/7] gridplot manipulation tests --- examples/tests/test_gridplot.py | 142 ++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 examples/tests/test_gridplot.py diff --git a/examples/tests/test_gridplot.py b/examples/tests/test_gridplot.py new file mode 100644 index 000000000..0f2910fec --- /dev/null +++ b/examples/tests/test_gridplot.py @@ -0,0 +1,142 @@ +import numpy as np +import pytest + +import fastplotlib as fpl +import pygfx + +data = np.arange(10) + + +def test_cameras_controller_properties(): + cameras = [ + ["2d", "3d", "3d"], + ["3d", "3d", "3d"] + ] + + controller_types = [ + ["panzoom", "panzoom", "fly"], + ["orbit", "trackball", "panzoom"] + ] + + gp = fpl.GridPlot( + shape=(2, 3), + cameras=cameras, + controller_types=controller_types + ) + + subplot_cameras = [subplot.camera for subplot in gp] + subplot_controllers = [subplot.controller for subplot in gp] + + for c1, c2 in zip(subplot_cameras, gp.cameras.ravel()): + assert c1 is c2 + + for c1, c2 in zip(subplot_controllers, gp.controllers.ravel()): + assert c1 is c2 + + for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), gp.cameras.ravel()): + if camera_type == "2d": + assert subplot_camera.fov == 0 + else: + assert subplot_camera.fov == 50 + + for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), gp.controllers.ravel()): + match controller_type: + case "panzoom": + assert isinstance(subplot_controller, pygfx.PanZoomController) + case "fly": + assert isinstance(subplot_controller, pygfx.FlyController) + case "orbit": + assert isinstance(subplot_controller, pygfx.OrbitController) + case "trackball": + assert isinstance(subplot_controller, pygfx.TrackballController) + + # check changing cameras + gp[0, 0].camera = "3d" + assert gp[0, 0].camera.fov == 50 + gp[1, 0].camera = "2d" + assert gp[1, 0].camera.fov == 0 + + # test changing controller + gp[1, 1].controller = "fly" + assert isinstance(gp[1, 1].controller, pygfx.FlyController) + assert gp[1, 1].controller is gp.controllers[1, 1] + gp[0, 2].controller = "panzoom" + assert isinstance(gp[0, 2].controller, pygfx.PanZoomController) + assert gp[0, 2].controller is gp.controllers[0, 2] + + +def test_gridplot_controller_ids_int(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids) + + assert gp[0, 0].controller is gp[1, 0].controller + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert gp[1, 1].controller is gp[2, 2].controller + + +def test_gridplot_controller_ids_int_change_controllers(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + cameras = [ + ["2d", "3d", "3d"], + ["2d", "3d", "2d"], + ["3d", "3d", "3d"] + ] + + gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids) + + assert isinstance(gp[0, 1].controller, pygfx.FlyController) + + # changing controller when id matches should change the others too + gp[0, 1].controller = "panzoom" + assert isinstance(gp[0, 1].controller, pygfx.PanZoomController) + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} + + # change to orbit + gp[0, 1].controller = "orbit" + assert isinstance(gp[0, 1].controller, pygfx.OrbitController) + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} + + +def test_set_gridplot_controllers_from_existing_controllers(): + gp = fpl.GridPlot(shape=(3, 3)) + gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers) + + assert gp.controllers[:-1].size == 6 + with pytest.raises(ValueError): + gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1]) + + for sp_gp, sp_gp2 in zip(gp, gp2): + assert sp_gp.controller is sp_gp2.controller + + cameras = [ + [pygfx.PerspectiveCamera(), "3d"], + ["3d", "2d"] + ] + + controllers = [ + [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], + [pygfx.OrbitController(), pygfx.PanZoomController()] + ] + + gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers) + + assert gp[0, 0].controller is controllers[0][0] + assert gp[0, 1].controller is controllers[0][1] + assert gp[1, 0].controller is controllers[1][0] + assert gp[1, 1].controller is controllers[1][1] + + assert gp[0, 0].camera is cameras[0][0] + + assert gp[0, 1].camera.fov == 50 From 93a503fb192ce710bd5d046a4116c4b9f8c124d5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 05:52:33 -0400 Subject: [PATCH 3/7] black --- fastplotlib/layouts/_gridplot.py | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 28893db37..472d3dd2e 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -29,7 +29,12 @@ def __init__( Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]] | Iterable[Literal["panzoom", "fly", "trackball", "orbit"]] ) = None, - controller_ids: Literal["sync"] | Iterable[int] | Iterable[Iterable[int]] | Iterable[Iterable[str]] = None, + controller_ids: ( + Literal["sync"] + | Iterable[int] + | Iterable[Iterable[int]] + | Iterable[Iterable[str]] + ) = None, controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | WgpuCanvasBase | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, @@ -103,9 +108,7 @@ def __init__( if isinstance(cameras, str): # create the array representing the views for each subplot in the grid - cameras = np.array([cameras] * len(self)).reshape( - self.shape - ) + cameras = np.array([cameras] * len(self)).reshape(self.shape) # list -> array if necessary cameras = np.asarray(cameras).reshape(self.shape) @@ -150,7 +153,9 @@ def __init__( f"by shape: {self.shape}. You have passed: <{controllers.size}> controllers" ) from None - subplot_controllers: np.ndarray[pygfx.Controller] = np.empty(self.shape, dtype=object) + subplot_controllers: np.ndarray[pygfx.Controller] = np.empty( + self.shape, dtype=object + ) for i, j in product(range(self.shape[0]), range(self.shape[1])): subplot_controllers[i, j] = controllers[i, j] @@ -160,9 +165,7 @@ def __init__( else: if controller_ids is None: # individual controller for each subplot - controller_ids = np.arange(len(self)).reshape( - self.shape - ) + controller_ids = np.arange(len(self)).reshape(self.shape) elif isinstance(controller_ids, str): if controller_ids == "sync": @@ -225,9 +228,7 @@ def __init__( if controller_types is None: # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array( - ["default"] * len(self) - ).reshape(self.shape) + controller_types = np.array(["default"] * len(self)).reshape(self.shape) elif isinstance(controller_types, str): if controller_types not in valid_controller_types.keys(): @@ -255,7 +256,9 @@ def __init__( f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" ) - controller_types: np.ndarray[pygfx.Controller] = np.asarray(controller_types).reshape(self.shape) + controller_types: np.ndarray[pygfx.Controller] = np.asarray( + controller_types + ).reshape(self.shape) # make the real controllers for each subplot subplot_controllers = np.empty(shape=self.shape, dtype=object) @@ -275,7 +278,9 @@ def __init__( if cont_type == "default": # hacky fix for now because of how `create_controller()` works cont_type = None - _controller = create_controller(controller_type=cont_type, camera=cams[0]) + _controller = create_controller( + controller_type=cont_type, camera=cams[0] + ) subplot_controllers[controller_ids == cid] = _controller @@ -342,14 +347,18 @@ def renderer(self) -> pygfx.WgpuRenderer: @property def controllers(self) -> np.ndarray[pygfx.Controller]: """controllers, read-only array, access individual subplots to change a controller""" - controllers = np.asarray([subplot.controller for subplot in self], dtype=object).reshape(self.shape) + controllers = np.asarray( + [subplot.controller for subplot in self], dtype=object + ).reshape(self.shape) controllers.flags.writeable = False return controllers @property def cameras(self) -> np.ndarray[pygfx.Camera]: """cameras, read-only array, access individual subplots to change a camera""" - cameras = np.asarray([subplot.camera for subplot in self], dtype=object).reshape(self.shape) + cameras = np.asarray( + [subplot.camera for subplot in self], dtype=object + ).reshape(self.shape) cameras.flags.writeable = False return cameras From a43e5fe25897964d42c7e7012930a04a1497f99f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 05:53:19 -0400 Subject: [PATCH 4/7] done wtih gp tests --- examples/tests/test_gridplot.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/examples/tests/test_gridplot.py b/examples/tests/test_gridplot.py index 0f2910fec..518838de6 100644 --- a/examples/tests/test_gridplot.py +++ b/examples/tests/test_gridplot.py @@ -4,8 +4,6 @@ import fastplotlib as fpl import pygfx -data = np.arange(10) - def test_cameras_controller_properties(): cameras = [ @@ -109,6 +107,27 @@ def test_gridplot_controller_ids_int_change_controllers(): assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} +def test_gridplot_controller_ids_str(): + names = [ + ["a", "b", "c"], + ["d", "e", "f"] + ] + + controller_ids = [ + ["a", "f"], + ["b", "d", "e"] + ] + + gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names) + + assert gp[0, 0].controller is gp[1, 2].controller is gp["a"].controller is gp["f"].controller + assert gp[0, 1].controller is gp[1, 0].controller is gp[1, 1].controller is gp["b"].controller is gp["d"].controller is gp["e"].controller + + # make sure subplot c is unique + exclude_c = [gp[n].controller for n in ["a", "b", "d", "e", "f"]] + assert gp["c"] not in exclude_c + + def test_set_gridplot_controllers_from_existing_controllers(): gp = fpl.GridPlot(shape=(3, 3)) gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers) From af64f8ca548b357fdfa3506ce3737800b9f79fda Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 06:10:07 -0400 Subject: [PATCH 5/7] move non example tests to new tests dir --- .github/workflows/ci.yml | 2 ++ {examples/tests => tests}/test_gridplot.py | 7 +++++++ 2 files changed, 9 insertions(+) rename {examples/tests => tests}/test_gridplot.py (97%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53e88bdf8..66518138c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | + pytest -v tests/ pytest -v examples FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 @@ -145,6 +146,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | + pytest -v tests/ pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} diff --git a/examples/tests/test_gridplot.py b/tests/test_gridplot.py similarity index 97% rename from examples/tests/test_gridplot.py rename to tests/test_gridplot.py index 518838de6..53dc037d9 100644 --- a/examples/tests/test_gridplot.py +++ b/tests/test_gridplot.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -5,6 +7,11 @@ import pygfx +@pytest.fixture(scope="session", autouse=True) +def set_env(): + os.environ["WGPU_FORCE_OFFSCREEN"] = "true" + + def test_cameras_controller_properties(): cameras = [ ["2d", "3d", "3d"], From 81240eacda3bd2109149b4567bd66122041aa677 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 06:22:02 -0400 Subject: [PATCH 6/7] force offscreen canvas --- tests/test_gridplot.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/test_gridplot.py b/tests/test_gridplot.py index 53dc037d9..3814664d7 100644 --- a/tests/test_gridplot.py +++ b/tests/test_gridplot.py @@ -1,5 +1,3 @@ -import os - import numpy as np import pytest @@ -7,11 +5,6 @@ import pygfx -@pytest.fixture(scope="session", autouse=True) -def set_env(): - os.environ["WGPU_FORCE_OFFSCREEN"] = "true" - - def test_cameras_controller_properties(): cameras = [ ["2d", "3d", "3d"], @@ -26,9 +19,12 @@ def test_cameras_controller_properties(): gp = fpl.GridPlot( shape=(2, 3), cameras=cameras, - controller_types=controller_types + controller_types=controller_types, + canvas="offscreen" ) + print(gp.canvas) + subplot_cameras = [subplot.camera for subplot in gp] subplot_controllers = [subplot.controller for subplot in gp] @@ -77,7 +73,7 @@ def test_gridplot_controller_ids_int(): [4, 1, 2] ] - gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids) + gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids, canvas="offscreen") assert gp[0, 0].controller is gp[1, 0].controller assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller @@ -97,7 +93,7 @@ def test_gridplot_controller_ids_int_change_controllers(): ["3d", "3d", "3d"] ] - gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids) + gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") assert isinstance(gp[0, 1].controller, pygfx.FlyController) @@ -125,7 +121,7 @@ def test_gridplot_controller_ids_str(): ["b", "d", "e"] ] - gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names) + gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") assert gp[0, 0].controller is gp[1, 2].controller is gp["a"].controller is gp["f"].controller assert gp[0, 1].controller is gp[1, 0].controller is gp[1, 1].controller is gp["b"].controller is gp["d"].controller is gp["e"].controller @@ -136,12 +132,12 @@ def test_gridplot_controller_ids_str(): def test_set_gridplot_controllers_from_existing_controllers(): - gp = fpl.GridPlot(shape=(3, 3)) - gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers) + gp = fpl.GridPlot(shape=(3, 3), canvas="offscreen") + gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers, canvas="offscreen") assert gp.controllers[:-1].size == 6 with pytest.raises(ValueError): - gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1]) + gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1], canvas="offscreen") for sp_gp, sp_gp2 in zip(gp, gp2): assert sp_gp.controller is sp_gp2.controller @@ -156,7 +152,7 @@ def test_set_gridplot_controllers_from_existing_controllers(): [pygfx.OrbitController(), pygfx.PanZoomController()] ] - gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers) + gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") assert gp[0, 0].controller is controllers[0][0] assert gp[0, 1].controller is controllers[0][1] From b57243f1243fe2ac8a45f5b8cce5ca889acd0380 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 7 Apr 2024 06:32:49 -0400 Subject: [PATCH 7/7] force offscreen canvas with env var for now --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66518138c..7db694d01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pytest -v tests/ + WGPU_FORCE_OFFSCREEN=1 pytest -v tests/ pytest -v examples FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 @@ -146,7 +146,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pytest -v tests/ + WGPU_FORCE_OFFSCREEN=1 pytest -v tests/ pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }}