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

ENH: Add a playback button to the notebook 3d backend #8741

Merged
merged 17 commits into from
May 3, 2021

Conversation

GuillaumeFavelier
Copy link
Contributor

@GuillaumeFavelier GuillaumeFavelier commented Jan 13, 2021

  1. This PR adds a set of ipywidgets buttons for playback. Here how it looks like:

image

output.mp4

Known issue

Does not depend on playback_speed but on the widget's interval value.

  1. I also tried backgroundjobsfrom ENH: Notebook 3d backend improvements (ipyvtklink) #8704 (comment) without success so far. I'll give it a try again later this afternoon.

It's an item of #8704.

@larsoner
Copy link
Member

Does not depend on playback_speed but on the widget's interval value.

It seems like you should only set "interval" to get the right refresh rate (e.g., 1/60 or 1/30), and the callback should be responsible for picking the correct time points. It seems like this should be the same for the Qt and IPywidgets backends actually -- and then as long as the callback tracks/knows 1) when the last frame was rendered (or if it's the first frame), 2) what the desired time dilation is, and 3) what the current time is (time.time()) and from this can calculate what time index/value it needs to change to, no? Such code would also adapt properly if we tried to get a framerate of 60 Hz but it renders at something slower or variable, which is nice.

@GuillaumeFavelier
Copy link
Contributor Author

Oh you're right! All this code is in _play() and _advance():

@safe_event
def _play(self):
if self.playback:
try:
self._advance()
except Exception:
self.toggle_playback(value=False)
raise
def _advance(self):
this_time = time.time()
delta = this_time - self._last_tick
self._last_tick = time.time()
time_data = self._data['time']
times = np.arange(self._n_times)
time_shift = delta * self.playback_speed
max_time = np.max(time_data)
time_point = min(self._current_time + time_shift, max_time)
# always use linear here -- this does not determine the data
# interpolation mode, it just finds where we are (in time) in
# terms of the time indices
idx = np.interp(time_point, time_data, times)
self.callbacks["time"](idx, update_widget=True)
if time_point == max_time:
self.toggle_playback(value=False)

@GuillaumeFavelier
Copy link
Contributor Author

The Play widget is limited to a range of int values. I increased the number of frames depending on the playback_speed but it won't be as smooth as the qt version since it does not reuse _advance() to synchronize the frame rate. Instead, it's a fixed to self.refresh_rate_ms.

@GuillaumeFavelier GuillaumeFavelier changed the title WIP: Add a playback button to the notebook 3d backend MRG: Add a playback button to the notebook 3d backend Jan 13, 2021
@GuillaumeFavelier
Copy link
Contributor Author

It's ready to go on my end 🚀

@larsoner
Copy link
Member

Works great! FWIW on my local Ubuntu-based 3.9 after pip installing the Jupyter widgets I needed to enable them:

jupyter-nbextension enable ipycanvas --py --user
jupyter nbextension enable ipyevents --py --user

Onto the minor issues:

  1. when clicking a time point there is a quick hop back to the old one before it continues on from the old one (and this does not happen with the standard Python plotting):

    Peek 2021-01-14 11-18

    Not a dealbreaker but if it's easy to fix we might as well.

  2. Changing the playback speed slider changes the time index even when no playback is ocurring

    Peek 2021-01-14 11-24

  3. Playback speed does not seem correct. The data is 0.25 sec so with speed == 0.1 playback should happen in 2.5 sec. Top is the HTML which goes maybe half as fast as it should, and bottom is the Qt/Python version which goes at the right speed:

    Peek 2021-01-14 11-26

    Peek 2021-01-14 11-28

    I suspect this has to do with relying somehow on the refresh rate (which probably does not actually get met by this backend) as opposed to making use of time.time()-based index selection.

@drammock
Copy link
Member

One nitpick from the peanut gallery: "toggle visibility" I think would be more clear if it said "toggle controls". Otherwise this is looking really nice.

@GuillaumeFavelier GuillaumeFavelier changed the title MRG: Add a playback button to the notebook 3d backend WIP: Add a playback button to the notebook 3d backend Jan 15, 2021
Base automatically changed from master to main January 23, 2021 18:27
@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Apr 23, 2021

This PR requires quite the update. I think the challenge now changed to finding a GUI-agnostic way (Qt/Ipywidget) to playback.

@larsoner
Copy link
Member

I think the challenge now changed to finding a GUI-agnostic way (Qt/Ipywidget) to playback.

Instead of making it agnostic I'd use our GUI abstraction layer to deal with the differences, and use a Qt timer and ipywidget object (Play?) as necessary in those backends

@GuillaumeFavelier
Copy link
Contributor Author

The test fails because Play is stored as an action but is technically not a Button.

@GuillaumeFavelier
Copy link
Contributor Author

Since I return it as a _Widget anyway, I think it's reasonable to remove it from the list of actions.

@GuillaumeFavelier
Copy link
Contributor Author

And this is how it looks when integrated in the toolbar:

image

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Apr 27, 2021

I chose the simplest approach which is to connect the playback buttons to the time slider bidirectionally.

The known issues:

  1. When the slider reaches the right end, the layout does not fit the dock (but I don't think it's related to this PR)

From:
image

To:
image

  1. The timeout is fixed (interval=500) so it's not realtime like the Qt solution
  2. The timesteps are fixed too (step=1). For this one, I tried implementing something similar to QFloatSlider and use a similar precision of 10000 but I could not manage to connect the slider in this case (or any callback actually).

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Apr 27, 2021

In my experiments, the bidirectional connection was not reliable. For example, using the playback to start, changing the slider and coming back to the playback, it will start from the old position:

output

I can mitigate by duplicating jsdlink():

        jsdlink((play, 'value'), (slider, 'value'))
        jsdlink((slider, 'value'), (play, 'value'))

But it brings lag when the slider is dragged (it snaps the value of the slider).

output.mp4

@GuillaumeFavelier
Copy link
Contributor Author

TL;DR: The realtime approach may not be possible considering the current limitations so I think the approach based on jsdlink is good enough. What do you think @larsoner, @agramfort ?

I took the time to fix 3) and the solution explained in jupyter-widgets/ipywidgets#1775 really helped. I shared the prototype in be61d64 for reference but I ended up reverting because I am not satisfied with the overall experience. The number of calls to set_time_point increases but the ViewInteractiveWidget just cannot keep up with the updates anyway causing a visible 'lag' (and I tested locally so it might be unusable with a remote connection).

@GuillaumeFavelier GuillaumeFavelier changed the title WIP: Add a playback button to the notebook 3d backend ENH: Add a playback button to the notebook 3d backend Apr 30, 2021
@larsoner
Copy link
Member

larsoner commented May 3, 2021

Some ability to play back is better than none, even if it's effectively a slideshow and the speed slider doesn't really reflect an accurate time dilation. We can always work on improving it later I guess. So if you're happy with how it works now @GuillaumeFavelier let me know and I'll look, test, and merge

@GuillaumeFavelier
Copy link
Contributor Author

You can have a look @larsoner, I'm ready for reviews.

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM

@@ -348,7 +363,7 @@ def show(self):
except RuntimeError:
# pyvista>=0.30.0
viewer = self.plotter.show(
jupyter_backend="ipyvtk_simple", return_viewer=True)
jupyter_backend="ipyvtklink", return_viewer=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a try/except as well?

Copy link
Contributor Author

@GuillaumeFavelier GuillaumeFavelier May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since 0.30.0 pyvista does not use ipyvtk_simple anymore

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but I'm thinking that it should also work for people with 0.29 if possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to bump/update those requirements in #9341

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's only a line or two to keep it working, it would be nice not to force people to upgrade

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if they have ipyvtk_simple and pyvista < 0.30 it should work, right? For example:

kwargs = dict()
if LooseVersion(pyvista.__version__) < LooseVersion('0.30'):
    kwargs['jupyter_backend' ] = 'ipyvtk_simple'
else:
    kwargs['jupyter_backend'] = 'ipyvtklink'
viewer = self.plotter.show(return_viewer=True, **kwargs)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice not to force people to upgrade

I understand

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rework this part to use LooseVersion instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API changed too, the following won't work:

self.plotter.show(jupyter_backend="ipyvtk_simple", return_viewer=True)

Comment on lines +1355 to +1362
try:
# pyvista<0.30.0
self.picked_renderer = \
self.plotter.iren.FindPokedRenderer(x, y)
except AttributeError:
# pyvista>=0.30.0
self.picked_renderer = \
self.plotter.iren.interactor.FindPokedRenderer(x, y)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this block works for >= 0.30 and < 0.30

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this one does not depend on another package.

If the user does not have ipyvtklink but has pyvista 0.30.0, it just won't work.

Copy link
Contributor Author

@GuillaumeFavelier GuillaumeFavelier May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I think bumping is the least painful solution

@larsoner larsoner merged commit 203d6fe into mne-tools:main May 3, 2021
@larsoner
Copy link
Member

larsoner commented May 3, 2021

Thanks @GuillaumeFavelier !

@GuillaumeFavelier GuillaumeFavelier deleted the enh/notebook_playback branch May 3, 2021 16:01
larsoner added a commit to rob-luke/mne-python that referenced this pull request May 6, 2021
* upstream/main:
  MRG, ENH: Make _get_hpi_info public (mne-tools#9369)
  ENH: Add a playback button to the notebook 3d backend (mne-tools#8741)
  better docs for permutation_cluster_test (mne-tools#9365)
  MRG: Add fNIRS to html output (mne-tools#9367)
  When plotting GFP comparison in Report, don't show sensor layout by default (mne-tools#9366)
  DOC: Update Mayavi troubleshooting section (mne-tools#9362)
  more tutorial tweaks (mne-tools#9359)
larsoner added a commit to agramfort/mne-python that referenced this pull request May 10, 2021
* upstream/main:
  FIX: make epoch cropping idempotent (mne-tools#9378)
  MRG, ENH: Add NIRSport support (mne-tools#9348)
  MRG, ENH: Make _get_hpi_info public (mne-tools#9369)
  ENH: Add a playback button to the notebook 3d backend (mne-tools#8741)
  better docs for permutation_cluster_test (mne-tools#9365)
  MRG: Add fNIRS to html output (mne-tools#9367)
  When plotting GFP comparison in Report, don't show sensor layout by default (mne-tools#9366)
  DOC: Update Mayavi troubleshooting section (mne-tools#9362)
  more tutorial tweaks (mne-tools#9359)
  MRG, MAINT: Use native GitHub Actions skip (mne-tools#9361)
  MAINT: Clean up crufty code [circle front] (mne-tools#9358)
  API: Complete deprecations (mne-tools#9356)
  Add qdarkstyle, darkdetect to environment.yml [circle full] (mne-tools#9357)
  FIX: Fix
  FIX: Add
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants