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

Implement a Jupyter cell magic %%manim #895

Closed
behackl opened this issue Jan 1, 2021 · 7 comments · Fixed by #943
Closed

Implement a Jupyter cell magic %%manim #895

behackl opened this issue Jan 1, 2021 · 7 comments · Fixed by #943
Labels
new feature Enhancement specifically adding a new feature (feature request should be used for issues instead)

Comments

@behackl
Copy link
Member

behackl commented Jan 1, 2021

Description of proposed feature

The package jupyter_manim allows a neat integration of (3b1b) Manim in Jupyter notebooks. At some point, we discussed sending a PR to make jupyter_manim compatible with the community version -- but actually, as has been pointed out in krassowski/jupyter-manim#26, it would be a much better solution to ship the corresponding functionality as a part of our community edition.

How can the new feature be used?

See the README in https://github.com/krassowski/jupyter-manim -- just like that.

@behackl behackl added the new feature Enhancement specifically adding a new feature (feature request should be used for issues instead) label Jan 1, 2021
@andreyuhai
Copy link

andreyuhai commented Jan 6, 2021

I've actually been trying to create a package for that but since there is an open issue I would like to share what I've tried so far.
I've read jupyter manim and as mentioned, it's a workaround because you can not really call manimlib.__init__.main() with the arguments, the same applies to ManimCommunity/manim. Moreover, there is no way to know where the output file is created without checking the output string.

What I've done so far is to create a temporary .py file with the cell content and call manimce within a subprocess with the temporary file but then I was not able to get the content of other cells . What's been done in jupyter_manim is to pickle all the globals from the stack and then unpickle them from within the temporary .py file but I've not tried that yet.

Here is the code:

from IPython.core.display import display, HTML
from IPython import get_ipython
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic, line_cell_magic
from IPython.core.magic import needs_local_scope
from pathlib import Path
from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile
import IPython.display
import os
import re

def find_path(output_string):
    output_string = output_string.decode('utf-8')
    output_string = re.sub(' ', '', ''.join(output_string.split('\n')))
    if m := re.search('(?<=Filereadyat).+(?=INFO)', output_string):
        return m.group(0)

@magics_class
class ManimceMagic(Magics):
    @cell_magic
    def manimce(self, line, cell):
        get_ipython().ex(cell)
        manimce_args = line.split()
        f = NamedTemporaryFile('r+', suffix='.py', delete=False)
        try:
            f.write(cell)
            f.close()

            args = ['manimce', f.name, *manimce_args]
            p = Popen(args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
            output, err = p.communicate()
            path = find_path(output)
            display(IPython.display.Code(output.decode('utf-8'), language='python3'))
            # display(IPython.display.Code(err.decode('utf-8'), language='python3'))
            display(p.returncode)
        finally:
            os.remove(f.name)

        if path:
            path = Path(path)
            relative_path = path.relative_to(Path.cwd())

            if '-i' in manimce_args:
                return IPython.display.Image(relative_path, width=854, height=480)
            else:
                return IPython.display.Video(relative_path, width=854, height=480, html_attributes='controls loop autoplay')

def load_ipython_extension(ipython):
    ipython.register_magics(ManimceMagic)

I've also tried to change jupyter_manim so that it supports manimce as well but there is an issue with the ouput that I couldn't figure out. Basically with manimce the output is directly written to jupyter notebook and I couldn't figure out why that happens.

@behackl
Copy link
Member Author

behackl commented Jan 6, 2021

Thank you for your report!

A while ago, I was working on getting jupyter_manim compatible with manimce and got it up and running eventually, but it did require some additional work (I can't recall precisely what I did, but I faintly remember having to fix setting config options and making sure that the output file is actually where it is supposed to be; something along these lines).

Technically, I wouldn't spawn a subprocess and run manimce there, but rather let Python do the work for us: we don't need to have a file containing the code for an animation. It would be more straightforward to have, given a cell

%%manim MyAwesomeScene

from manim import Scene, Square, ShowCreation

class MyAwesomeScene(Scene):
    def construct(self):
        sq = Square()
        self.play(ShowCreation(sq))

simply call MyAwesomeScene().render() to produce the corresponding output.

The biggest problem with this approch is, I feel, providing a clean interface for setting config parameters. While it is possible to simply set the corresponding options using manim.config['option'] = value or so directly in the cell, it would be a good idea to support passing the CLI flags anyways, in an ideal world even by utilizing the existing code for parsing and setting these options.

@kolibril13
Copy link
Member

I am really excited about this integration idea!

Before going into deep details, I have some conceptional thoughts of what this manim jupyter feature should be used for.

I could see two approaches:

  1. We keep all the manim functionality that is also available in the current manim when run from a .py file with terminal, and also include all the CLI flags.

  2. We develop manim functionality that is focused on the platform jupyter which means that some behaviours might change.

To give some examples of my idea:

  • At the moment, the -s flag saves the last output image to an .png -> In a jupyter notebook, it will show the image in the cell output instead of saving it.
  • With the -q l , -q m flag, one can change the quality of the output video. -> in a jupyter notebook this will be useful as well, but additionally, it would be nice to change the size of the output videowindow as well. Maybe with a new jupyter flag -windowsize m
  • Some other flags could be removed: the -p would be unnecessary, and also the -i could be removed.

I would rather go with approach 2 then approach 1.

Design idea:

As

%%manim MyAwesomeScene

from manim import Scene, Square, ShowCreation

class MyAwesomeScene(Scene):
    def construct(self):
        sq = Square()
        self.play(ShowCreation(sq))

is a bit clunky.
Maybe it would be also possible to go with something like this:

%%manim -q m

from manim import ManimScene, Scene, Square, ShowCreation

manim = ManimScene(Scene):
sq = Square()
manim.play(ShowCreation(sq))

Furhter notes:

I think that from IPython.display import display can be useful for us, I already experimented with manim videos in jupyter notebooks here: https://github.com/kolibril13/jupyter_video_presentation

@behackl
Copy link
Member Author

behackl commented Jan 11, 2021

Personally, I would keep the magic cell interface as close as possible to the command line interface as possible: it would seem weird to me, from a UX perspective, to introduce a completely different way how Manim code should be written and run.

The advantage I see for writing the exact same code in Jupyter cells as in a normal python file is precisely that: that Jupyter can be used to experiment and for rapid prototyping, and once you are satisfied you can just copy+paste your code in a python file.

I don't really understand what you are doing in your last code block; that seems syntactically broken to me.

I'd definitely include and display the videos in the jupyter notebook -- just like the current behavior of the jupyter_manim package!

@kolibril13
Copy link
Member

Personally, I would keep the magic cell interface as close as possible to the command line interface as possible: it would seem weird to me, from a UX perspective, to introduce a completely different way how Manim code should be written and run.

Yep, it definitely should stay similar.
However, some things will have some different behaviors.
E.g. the -s flag, will it save the image to an png?
Or will it only show the image in a jupyter output cell?

The advantage I see for writing the exact same code in Jupyter cells as in a normal python file is precisely that: that Jupyter can be used to experiment and for rapid prototyping, and once you are satisfied you can just copy+paste your code in a python file.
I don't really understand what you are doing in your last code block; that seems syntactically broken to me.

Yep, I think so too! That was only a brainstorming idea, to get the indention level a bit more to the right, but the copy+paste possibility is also a good point.

@krassowski
Copy link

krassowski commented Feb 1, 2021

Happy to see that it was merged! I was going to comment that maybe the Scene objects could have _repr_html method for even better jupyter integration, see https://ipython.readthedocs.io/en/stable/config/integrating.html#integrating-your-objects-with-ipython

I am away from keyboard now but will add a mention of your implementation to help users find what they are looking for. Is there a documentation section with examples of the magic usage that I should point to?

Edit: sorry I meant to comment on the issue in my repository but I'm on mobile and didn't notice. Anyways I guess it works here too :)

@behackl
Copy link
Member Author

behackl commented Feb 1, 2021

Happy to see that it was merged! I was going to comment that maybe the Scene objects could have _repr_html method for even better jupyter integration, see https://ipython.readthedocs.io/en/stable/config/integrating.html#integrating-your-objects-with-ipython

That's an interesting idea, thanks for sharing! I do think that something like that makes sense; maybe we'll wait with implementing this until we switch to the new webgl renderer; then we might be able to profit even more of custom html representations.

And of course: thank you again for bringing up the idea of including IPython capabilities directly!

I am away from keyboard now but will add a mention of your implementation to help users find what they are looking for. Is there a documentation section with examples of the magic usage that I should point to?

We did setup a very simple playground on binder, accessible at https://mybinder.org/v2/gist/behackl/725d956ec80969226b7bf9b4aef40b78/HEAD?filepath=basic%20example%20scenes.ipynb -- eventually, I'd like to provide supplementary notebooks to all of our tutorials. I'm not sure yet about the best way of documenting the magic (ideally such that it combines well with Sphinx), but I didn't investigate too much so far.

Edit: sorry I meant to comment on the issue in my repository but I'm on mobile and didn't notice. Anyways I guess it works here too :)

It does. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature Enhancement specifically adding a new feature (feature request should be used for issues instead)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants