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

Handle ipywidget sliders in Controls.save_animation #181

Closed
redeboer opened this issue Apr 26, 2021 · 5 comments · Fixed by #182
Closed

Handle ipywidget sliders in Controls.save_animation #181

redeboer opened this issue Apr 26, 2021 · 5 comments · Fixed by #182

Comments

@redeboer
Copy link
Contributor

redeboer commented Apr 26, 2021

Bug report

Hi @ianhi and contributors, thanks a lot for creating this nice interface to matplotlib!

Following https://mpl-interactions.readthedocs.io/en/0.17.8/gallery/mpl-sliders-same-figure.html, I've been trying to use FloatSliders from ipywidgets as controllers, because I find them more convenient than Matplotlib sliders. Not sure if mpl-interactions is supposed to work like that, but it worked well so far, up to saving an animation.

Bug summary

Controls.save_animation cannot handle FloatSlider or IntSlider from ipywidgets.

Code for reproduction

Modified from basic example:

# required because ipywidgets_slider = True
%matplotlib widget

import matplotlib.pyplot as plt
import numpy as np
from ipywidgets.widgets.widget_float import FloatSlider
from ipywidgets.widgets.widget_int import IntSlider

import mpl_interactions.controller as Controls
import mpl_interactions.ipyplot as iplt

x = np.linspace(0, np.pi, 100)
beta = FloatSlider(min=1, max=10, step=1e-1, value=2, description=R"$\beta$")
tau = IntSlider(min=1, max=10, description=R"$\tau$")

def f1(x, tau, beta):
    return np.sin(x * tau) * x * beta

def f2(x, tau, beta):
    return np.sin(x * beta) * x * tau

fig, ax = plt.subplots()
controls = iplt.plot(x, f1, tau=tau, beta=beta)
iplt.plot(x, f2, controls=controls)

# This causes the bug:
controls.save_animation("animation-beta.gif", fig, "beta")
beta.value = 2  # example of what I like about ipywidget sliders
controls.save_animation("animation-tau.gif", fig, "tau", interval=300)

Actual outcome

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-3-dfc566a4ee9c> in <module>
----> 1 controls.save_animation("animation-beta.gif", fig, "beta")
      2 beta.value = 2
      3 controls.save_animation("animation-tau.gif", fig, "tau", interval=300)

**/mpl_interactions/controller.py in save_animation(self, filename, fig, param, interval, func_anim_kwargs, N_frames, **kwargs)
    302 
    303         repeat = func_anim_kwargs.pop("repeat", False)
--> 304         anim = FuncAnimation(fig, f, frames=N, interval=interval, repeat=repeat, **func_anim_kwargs)
    305         # draw then stop necessary to prevent an extra loop after finished saving
    306         # see https://discourse.matplotlib.org/t/how-to-prevent-funcanimation-looping-a-single-time-after-save/21680/2

UnboundLocalError: local variable 'N' referenced before assignment

Expected outcome

After a small bug fix (see #182), these are the resulting gifs:

beta (FloatSlider) tau (IntSlider)
animation-beta animation-tau

Version Info

  • Operating system: Ubuntu 20.04
  • mpl-interactions version: 0.17.8
  • Matplotlib version: 3.4.1
  • Matplotlib backend: ipympl
  • Python version: 3.8.8
  • Jupyter version: jupyterlab==3.0.14
@ianhi
Copy link
Collaborator

ianhi commented Apr 26, 2021

Hi @redeboer thanks for the excellent bug report and quick fix!

I've been trying to use FloatSliders from ipywidgets as controllers, because I find them more convenient than Matplotlib sliders. Not sure if mpl-interactions is supposed to work like that, but it worked well so far, up to saving an animation.

It most definitely is supposed to work with those! In fact originally those are all that were supported. Is there a reason you are manually creating the sliders? A big benefit for me of this package is that it will autocreate the sliders for you based on the shorthands of tuples or passing in arrays.

So instead of defining tau and beta as sliders you can do:

tau = np.arange(1,10, 1e-1) or np.linspace(1,10, 1000) or (1,10,1000). Although then you would have a more difficult time giving them latex labels.

If you create them with a controls object then you can still directly access the sliders by:

controls.controls['tau'] which should directly return the slider (controls.controls is a dict of all the control objects).

All that aside I think you're right that a fix is need and your fix looks good on quick glance, I can review it tonight.


After writing the above I wonder if it might be nice to be to directly set the values of the parameters by accessing the controls objeect. Something like controls.beta = 2. That would also make it easier for users to abstract over mpl vs ipywidgets sliders. This would require some sort of nifty properties trickery

@ianhi
Copy link
Collaborator

ianhi commented Apr 26, 2021

controls.controls['tau'] which should directly return the slider (controls.controls is a dict of all the control objects).

That isn't quite true :(. For ipywidgets if mpl-interactions auto creates the sliders for you then it actually makes an HBox for which the slider is the first child (This is a workaround to have better custom formatting of the slider values). So you would need:

controls.controls['tau'].children[0] which is pretty awful.... I should make that easier

@ianhi
Copy link
Collaborator

ianhi commented Apr 26, 2021

This would require some sort of nifty properties trickery

(for future me) I think the way here is to make Controls inherit from HasTraits so that you can use add_traits this will work for an instance of the class which is nice because basic properties only work for the full class, rather than an instance (https://stackoverflow.com/questions/1325673/how-to-add-property-to-a-class-dynamically). idk what traitlets did to solve this, but they did something because this works:

from traitlets import HasTraits, Int

class controls(HasTraits):
    whatever = Int(30)
beep = controls()
boop = controls()
beep.add_traits(blarg = Int())
beep.blarg = 2
boop.blarg # AttributeError

@redeboer
Copy link
Contributor Author

You're welcome! I'm quite new to widgets etc. and avoided them so far, but mpl-interactions makes it much easier to use them. So thanks again :)

Is there a reason you are manually creating the sliders? A big benefit for me of this package is that it will autocreate the sliders for you based on the shorthands of tuples or passing in arrays.

That's true, originally I also followed the 'auto-create style' from the documentation. But indeed, as you note, it's currently a bit tedious to programmatically modify slider values. For instance, controls.controls["beta"].children[0].value = 2.0 won't work, because the auto-generated children[0] is an IntSlider for the (I assume) indices of the underlying numpy.array.

All that aside I think you're right that a fix is need and your fix looks good on quick glance, I can review it tonight.

So it could be that I misunderstood some things, particularly how to work with the generated HBox structure for each control, and whether/how this should work if you don't use ipympl as backend.

After writing the above I wonder if it might be nice to be to directly set the values of the parameters by accessing the controls objeect. Something like controls.beta = 2. That would also make it easier for users to abstract over mpl vs ipywidgets sliders. This would require some sort of nifty properties trickery

That syntax would be nice indeed! Personally, I have my reservations though about adding too many convenient short-cuts. So in this example, I would prefer something more object-oriented, like

slider = controls.controls["beta"]
slider.label = "\\beta"
slider.min = 2.2
slider.set_range(0, 2.5)
slider.n_points = 10

(or some variation thereof). Reason is that such methods and attributes (can) document themselves in an API and make it easier for the developer to understand the codebase (or for a user who has some particular requirements). Convenience often comes at the cost of maintainability and can make the framework restricted to particular use cases.


Background: I was writing some functions that generate interactive plots from arbitrary sympy expressions (remaining free symbols become sliders, etc.). That's why I wanted to set latex labels for the sliders and initialize the sliders with some suggested parameter value.

@ianhi
Copy link
Collaborator

ianhi commented Apr 26, 2021

That syntax would be nice indeed! Personally, I have my reservations though about adding too many convenient short-cuts. So in this example, I would prefer something more object-oriented, like

Part of what I'm trying achieve is to this library handle all the complexity of the differences between ipywidgets and maptlotlib widgets. Then you can use this in a function and not have to worry about a different API depending on if the user is running your code from a notebook or not. So I guess that any solution will need to both:

  1. Provide a convenient way that is independent of widget backend
  2. Allow for direct access to the controls if you are willing to deal with this maybe being in a notebook or maybe being pure mpl

I'll think about this more and open an issue with potential options - your thoughts and/or suggestions are of course welcome.

So it could be that I misunderstood some things, particularly how to work with the generated HBox structure for each control, and whether/how this should work if you don't use ipympl as backend.

Nah I think you did it right. The HBox structure is only relevant if the sliders are autogenerated which is captured in the first if statement (which I should add a comment explaining). That HBox is there because autogenerated sliders use a widgets Label as a custom readout to hide the fact that they are actually IntSliders https://github.com/ianhi/mpl-interactions/blob/99e1552266b8179c03583e585ad262c2670daa63/mpl_interactions/helpers.py#L374-L379

Background: I was writing some functions that generate interactive plots from arbitrary sympy expressions (remaining free symbols become sliders, etc.). That's why I wanted to set latex labels for the sliders and initialize the sliders with some suggested parameter value.

That sounds very cool! If it's not already I'd definitely encourage you to make this available as a standalone package (there's also https://github.com/matplotlib/matplotlib-extension-cookiecutter if you need a start point)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants