Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Points, vispy] Traceback when toggling Points layer to 3D #6925

Closed
psobolewskiPhD opened this issue May 21, 2024 · 6 comments · Fixed by #7004
Closed

[Points, vispy] Traceback when toggling Points layer to 3D #6925

psobolewskiPhD opened this issue May 21, 2024 · 6 comments · Fixed by #7004
Labels
bug Something isn't working as expected priority:high High priority issue severity:medium Impairs users from using napari to do what they need but napari is not crashing or segfaulti
Milestone

Comments

@psobolewskiPhD
Copy link
Member

🐛 Bug Report

First reported here:
https://napari.zulipchat.com/#narrow/stream/212875-general/topic/vispy.20.22Last.20dimension.20should.20be.202.20not.203.22.20error

Creating a points layer and toggling to 3D results in a traceback:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/Dev/vispy/vispy/app/backends/_qt.py:928, in CanvasBackendDesktop.paintGL(self=<vispy.app.backends._qt.CanvasBackendDesktop object>)
    926 # (0, 0, self.width(), self.height()))
    927 self._vispy_canvas.set_current()
--> 928 self._vispy_canvas.events.draw(region=None)
        self._vispy_canvas = <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>
        self._vispy_canvas.events.draw = <vispy.util.event.EventEmitter object at 0x28a227770>
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x28a296c10>
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x28a2278c0>
    930 # Clear the alpha channel with QOpenGLWidget (Qt >= 5.4), otherwise the
    931 # window is translucent behind non-opaque objects.
    932 # Reference:  MRtrix3/mrtrix3#266
    933 if QT5_NEW_API or PYSIDE6_API or PYQT6_API:

File ~/Dev/vispy/vispy/util/event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'region': None})
    450 if self._emitting > 1:
    451     raise RuntimeError('EventEmitter loop detected!')
--> 453 self._invoke_callback(cb, event)
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
        self = <vispy.util.event.EventEmitter object at 0x28a227770>
        cb = <bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>>
    454 if event.blocked:
    455     break

File ~/Dev/vispy/vispy/util/event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6)>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,
        self = <vispy.util.event.EventEmitter object at 0x28a227770>
        cb = <bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
        (cb, event) = (<bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>>, <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    472                       self.print_callback_errors,
    473                       self, cb_event=(cb, event))

File ~/Dev/vispy/vispy/util/event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6)>>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    467 def _invoke_callback(self, cb, event):
    468     try:
--> 469         cb(event)
        cb = <bound method SceneCanvas.on_draw of <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>>
        event = <DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File ~/Dev/vispy/vispy/scene/canvas.py:219, in SceneCanvas.on_draw(self=<NapariSceneCanvas (PyQt6)>, event=<DrawEvent blocked=False handled=False native=None region=None source=None sources=[] type=draw>)
    216 # Now that a draw event is going to be handled, open up the
    217 # scheduling of further updates
    218 self._update_pending = False
--> 219 self._draw_scene()
        self = <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>

File ~/Dev/vispy/vispy/scene/canvas.py:278, in SceneCanvas._draw_scene(self=<NapariSceneCanvas (PyQt6)>, bgcolor=<class 'numpy.ndarray'> (4,) float32)
    276     bgcolor = self._bgcolor
    277 self.context.clear(color=bgcolor, depth=True)
--> 278 self.draw_visual(self.scene)
        self = <NapariSceneCanvas (PyQt6) at 0x16e6ffbf0>

File ~/Dev/vispy/vispy/scene/canvas.py:316, in SceneCanvas.draw_visual(self=<NapariSceneCanvas (PyQt6)>, visual=<SubScene>, event=None)
    314         else:
    315             if hasattr(node, 'draw'):
--> 316                 node.draw()
        node = <PointsVisual at 0x29327d460>
    317                 prof.mark(str(node))
    318 else:

File ~/Dev/vispy/vispy/scene/visuals.py:106, in VisualNode.draw(self=<PointsVisual>)
    104 if self.picking and not self.interactive:
    105     return
--> 106 self._visual_superclass.draw(self)
        self = <PointsVisual at 0x29327d460>
        self._visual_superclass = <class 'vispy.visuals.visual.CompoundVisual'>

File ~/Dev/vispy/vispy/visuals/visual.py:668, in CompoundVisual.draw(self=<PointsVisual>)
    666 for v in self._subvisuals:
    667     if v.visible:
--> 668         v.draw()
        v = <Line at 0x293050da0>

File ~/Dev/vispy/vispy/scene/visuals.py:106, in VisualNode.draw(self=<Line>)
    104 if self.picking and not self.interactive:
    105     return
--> 106 self._visual_superclass.draw(self)
        self = <Line at 0x293050da0>
        self._visual_superclass = <class 'vispy.visuals.line.line.LineVisual'>

File ~/Dev/vispy/vispy/visuals/visual.py:668, in CompoundVisual.draw(self=<Line>)
    666 for v in self._subvisuals:
    667     if v.visible:
--> 668         v.draw()
        v = <vispy.visuals.line.line._GLLineVisual object at 0x2932e5f40>

File ~/Dev/vispy/vispy/visuals/visual.py:505, in Visual.draw(self=<vispy.visuals.line.line._GLLineVisual object>)
    503 if not self.visible:
    504     return
--> 505 if self._prepare_draw(view=self) is False:
        self = <vispy.visuals.line.line._GLLineVisual object at 0x2932e5f40>
    506     return
    508 if self._vshare.draw_mode is None:

File ~/Dev/vispy/vispy/visuals/line/line.py:331, in _GLLineVisual._prepare_draw(self=<vispy.visuals.line.line._GLLineVisual object>, view=<vispy.visuals.line.line._GLLineVisual object>)
    329     return False
    330 pos = np.ascontiguousarray(self._parent._pos, dtype=np.float32)
--> 331 self._pos_vbo.set_data(pos)
        pos = <class 'numpy.ndarray'> (1, 3) float32
        self._pos_vbo = <VertexBuffer size=1 last_dim=2>
        self = <vispy.visuals.line.line._GLLineVisual object at 0x2932e5f40>
    332 self._program.vert['position'] = self._pos_vbo
    333 self._program.vert['to_vec4'] = self._ensure_vec4_func(pos.shape[-1])

File ~/Dev/vispy/vispy/gloo/buffer.py:189, in DataBuffer.set_data(self=<VertexBuffer size=1 last_dim=2>, data=<class 'numpy.ndarray'> (1, 3) float32, copy=False, **kwargs={})
    175 def set_data(self, data, copy=False, **kwargs):
    176     """Set data (deferred operation)
    177
    178     Parameters
   (...)
    187         Additional arguments.
    188     """
--> 189     data = self._prepare_data(data, **kwargs)
        data = <class 'numpy.ndarray'> (1, 3) float32
        self = <VertexBuffer size=1 last_dim=2>
        kwargs = {}
    190     self._dtype = data.dtype
    191     # This works around some strange NumPy bug where a float32 array
    192     # of shape (155407, 1) was said to have strides
    193     # (4, 9223372036854775807), which is crazy

File ~/Dev/vispy/vispy/gloo/buffer.py:446, in VertexBuffer._prepare_data(self=<VertexBuffer size=1 last_dim=2>, data=<class 'numpy.ndarray'> (1, 3) float32, convert=False)
    444     c = 1
    445 if self._last_dim and c != self._last_dim:
--> 446     raise ValueError('Last dimension should be %s not %s'
        'Last dimension should be %s not %s'
                                 % (self._last_dim, c) = 'Last dimension should be 2 not 3'
        c = 3
        self._last_dim = 2
        self = <VertexBuffer size=1 last_dim=2>
        (self._last_dim, c) = (2, 3)
    447                      % (self._last_dim, c))
    448 dtype_def = ('f0', data.dtype.base)
    449 if c != 1:
    450     # numpy dtypes with size 1 are ambiguous, only add size if it is greater than 1

ValueError: Last dimension should be 2 not 3

The error is spammed, even upon return to 2D, and results in significant performance degradation.

💡 Steps to Reproduce

The following script will reproduce:

import numpy as np
import napari

data =  np.random.rand(3, 2)
viewer = napari.Viewer()

viewer.add_points(data)
viewer.dims.ndisplay = 3

napari.run()

Alternately just launch napari, add points layer, and toggle to 3D.

💡 Expected Behavior

No error.

🌎 Environment

napari: 0.5.0a2.dev649+g2d2cac676
Platform: macOS-13.3.1-arm64-arm-64bit
System: MacOS 13.3.1
Python: 3.12.2 | packaged by conda-forge | (main, Feb 16 2024, 20:54:21) [Clang 16.0.6 ]
Qt: 6.6.1
PyQt6:
NumPy: 1.26.4
SciPy: 1.12.0
Dask: 2024.3.1
VisPy: 0.14.3.dev3
magicgui: 0.8.2
superqt: 0.6.2
in-n-out: 0.2.0
app-model: 0.2.5
npe2: 0.7.4

OpenGL:

  • GL version: 2.1 Metal - 83.1
  • MAX_TEXTURE_SIZE: 16384
  • GL_MAX_3D_TEXTURE_SIZE: 2048

Screens:

  • screen 1: resolution 1680x1050, scale 2.0

Optional:

  • numba: 0.59.0
  • triangle not installed

Settings path:

  • /Users/piotrsobolewski/Library/Application Support/napari/napari-dev_a1eb8b76ba95fa16ad06e26097b46b8455dfbf0b/settings.yaml
    Plugins:
  • napari: 0.5.0a2.dev604+gfe4740ef3.d20240319 (81 contributions)
  • napari-console: 0.0.9 (0 contributions)
  • napari-svg: 0.1.10 (2 contributions)
  • napari-threedee: 0.0.20.dev1+g65d1b01 (22 contributions)
  • zarpaint: 0.3.1 (14 contributions)

💡 Additional Context

The root cause is here:
https://github.com/napari/napari/pull/6896/files#r1602264036

But the question is how are all tests passing and what can we do about it.

@psobolewskiPhD psobolewskiPhD added bug Something isn't working as expected priority:high High priority issue severity:medium Impairs users from using napari to do what they need but napari is not crashing or segfaulti labels May 21, 2024
@psobolewskiPhD psobolewskiPhD added this to the 0.5.0 milestone May 21, 2024
@andy-sweet andy-sweet self-assigned this Jun 12, 2024
@andy-sweet
Copy link
Member

I keep running into this, so assigned myself to spend a little time thinking about how to write a test that fails on main.

@andy-sweet
Copy link
Member

Quickly unassigning because I failed to write any test that fails on main ...

I started with some that don't require the whole viewer, but they all pass.

def test_2d_to_3d_vispy_layer():
    """See https://github.com/napari/napari/issues/6925"""
    layer = Points(np.zeros((4, 2)))
    visual = VispyPointsLayer(layer)
    dims = Dims(ndim=3, ndisplay=2)
    layer._slice_dims(dims)

    dims.ndisplay = 3
    layer._slice_dims(dims)


def test_2d_to_3d_viewer_model():
    """See https://github.com/napari/napari/issues/6925"""
    viewer = ViewerModel()
    layer = viewer.add_points(np.zeros((4, 2)))
    visual = VispyPointsLayer(layer)

    viewer.dims.ndisplay = 3


def test_2d_to_3d_vispy_canvas():
    """See https://github.com/napari/napari/issues/6925"""
    viewer = ViewerModel()
    layer = viewer.add_points(np.zeros((4, 2)))
    canvas = VispyCanvas(viewer, KeymapHandler())
    visual = VispyPointsLayer(layer)
    canvas.add_layer_visual_mapping(layer, visual)

    viewer.dims.ndisplay = 3

There may be some state that could be inspected/asserted to cause a failure (e.g. looking at the width of the line visual), but that may assume too much about the implementation details.

Then I tried the higher level test suggested on Zulip and even that passes:

def test_points_2d_to_3d(make_napari_viewer):
    """See https://github.com/napari/napari/issues/6925"""
    viewer = make_napari_viewer(ndisplay=2, show=True)
    layer = viewer.add_points()
    viewer.dims.ndisplay = 3

I then tried run the reproducer above as a script and even that was fine!

But I can manually reproduce by clicking the 2/3D display button.

@andy-sweet
Copy link
Member

Ah, maybe this needs QApplication::processEvents?

Adding that does make this reproducible for me:

def test_points_2d_to_3d(make_napari_viewer):
    """See https://github.com/napari/napari/issues/6925"""
    viewer = make_napari_viewer(ndisplay=2, show=True)
    viewer.add_points()
    QApplication.processEvents()
    viewer.dims.ndisplay = 3
    QApplication.processEvents()

@andy-sweet andy-sweet removed their assignment Jun 12, 2024
@andy-sweet
Copy link
Member

Also note that while using a Points layer with no data is enough to create the issue for me (as above), the test we add should have some data.

@psobolewskiPhD
Copy link
Member Author

Interesting, the script from the OP does reproduce with no additional input for me -- fresh pull from main: eab7661

Thanks for investigating!

@brisvag
Copy link
Contributor

brisvag commented Jun 19, 2024

I bumped into this again... I put together the suggestions from above in a PR: #7004.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working as expected priority:high High priority issue severity:medium Impairs users from using napari to do what they need but napari is not crashing or segfaulti
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants