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() }} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy