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

Implemented decorator :func:.manimation for building a scene from a construct method #1901

Open
wants to merge 280 commits into
base: main
Choose a base branch
from

Conversation

behackl
Copy link
Member

@behackl behackl commented Aug 12, 2021

Overview: What does this pull request change?

As a follow-up from a recent Discord discussion, this PR is a proof of concept for a decorator that allows to remove one level of indentation when writing your animations. Whether or not we go ahead with this idea is subject to discussion.

Motivation and Explanation: Why and how do your changes improve the library?

This is a demo script which, when running python on it, renders the scene with the specified temporary config options.

from manim import *

@manimation  # equivalent to @manimation(scene_class=Scene)
def square_to_circle(scene: Scene):
    s = Square()
    c = Circle()
    scene.play(Create(s))
    scene.play(Transform(s, c))
    scene.play(FadeOut(*scene.mobjects))

with tempconfig({'frame_rate': 30}):
    square_to_circle.render()

Links to added or changed documentation pages

https://manimce--1901.org.readthedocs.build/en/1901/reference/manim.scene.scene.html#module-manim.scene.scene

Further Information and Comments

The current version is not compatible with the CLI (yet), but I think it would not be too hard to change this.

Note that the interface originally described in this description also included overriding the __call__ of a scene; this has since been removed from the proposal.

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

@behackl behackl added new feature Enhancement specifically adding a new feature (feature request should be used for issues instead) needs discussion Things which needs to be discussed before implemented. labels Aug 12, 2021
Copy link
Member

@huguesdevimeux huguesdevimeux left a comment

Choose a reason for hiding this comment

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

I must say that I disagree with that design (although It's pythonically interesting), and for mainly two reasons :

  • I dislike the design. Here, SquareToCircle is declared as a function, but intended to be used as it was a class, and called as such. This makes it unclear for the end user about what they are supposed to do and what is this SquareToCircle object. I find better to use a render method instead of a direct instantiation, as we have currently, thing that (at first glance) should not be possible to use with this decorator approach.

  • I strongly think that the config options you're quoting should not be set from a python file for clarity and separation sake, but rather use the config that have been made specifically for that, since these config options are more project-wide than scene-wide (i.e a user will likely not want a different resolution for two scenes of a single video). Quoting @leotrs (the man behind config system) here : I propose that we leave inside constants.py only those variables X such that the answer to the following question is "no": Would the user expect X to change from project to project? If the user has two entirely different projects for which they are using manim, and they would expect X to have the exact same value in both projects, then X should go in constants.py, and everything else should go in the config dict

In fact, I don't really approve the whole idea of having function to define a scene (I know, that's what I did for tests but .. meh that's tests there wasn't much choice). The problem is that the function will necessarily have parameter scene containing the scene reference, which is … is exactly what python uses for class method, self. So, the function would be a class method without being declared as such, which I find being a bad design.

But still, I don't want to be rude or whatever, that's really a great and interesting idea, with which I unfortunately disagree.

@behackl
Copy link
Member Author

behackl commented Aug 13, 2021

I dislike the design.

That's fair. I am used to the current way of writing scenes as well, but people have been pretty vocal about it in our Discord, so I thought I'd draft a proposal for it.

I find better to use a render method instead of a direct instantiation, as we have currently, thing that (at first glance) should not be possible to use with this decorator approach.

That's also perfectly fair, sure, and on second thought I also pretty much agree – making the class callable doesn't really provide a lot of benefit. However, it is easily possible to adapt this to the decorator approach as well. The @manim_scene decorator turns a function into a scene object, meaning that after defining the function as above, SquareToCircle is an initialized Scene-object. Calling SquareToCircle.render() works.

config options you're quoting should not be set from a python file for clarity and separation sake

For large projects with multiple scenes I agree, users should use a config file to store their settings. But this syntax isn't intended for large projects, it's for quick-and-dirty scenes where you might want to quickly animate a concept and don't want to spend a whole lot of time setting the configuration up. When developing and reviewing, I write such scenes literally all the time, and I feel it would help my workflow if I can make these changes locally in an easy way.

If I had to decide between

config.frame_rate = 42
config.frame_width = ...,
config.frame_height = ...,
config.background_color = ...
scene = MyScene()
scene.render()

and

MyScene.render(frame_rate=42, frame_width=..., frame_height=..., background_color=...),

I think that the second version expresses what I am doing in a cleaner way. Besides, I don't want to circumvent the config system, not at all -- in the current version of this PR, a tempconfig with the passed options is created in which the scene is rendered, thus temporarily overwriting the preexisting config options with the settings that are explicitly passed to render.

I don't want to be rude or whatever

I don't think your response or your stance are rude at all. This is something that has to be discussed, and I'm happy you contributed your point of view. :-)

@huguesdevimeux
Copy link
Member

For large projects with multiple scenes I agree, users should use a config file to store their settings. But this syntax isn't intended for large projects, it's for quick-and-dirty scenes where you might want to quickly animate a concept and don't want to spend a whole lot of time setting the configuration up. When developing and reviewing, I write such scenes literally all the time, and I feel it would help my workflow if I can make these changes locally in an easy way.

In fact, at the beginning, the idea was to set all the config content primarily (exclusively) from a .cfg file, and pretty much never from a python script, so, the two cases you're pointing out are ... not theoretically encouraged. However, there is theory and the real world, and as you said, it can be annoying to have to write a new .cfg. This is something to discuss.

a tempconfig with the passed options is created in which the scene is rendered, thus temporarily overwriting the preexisting config options with the settings that are explicitly passed to render.

Since all the stuff in config are thought to be project-dependent, this case shouldn't happen, no ? I mean, if you want to have a different frame rate, it's very likely that you are working on another project, so you should therefore work on another python file/another cwd and .cfg.

@behackl
Copy link
Member Author

behackl commented Aug 13, 2021

As I've said, I don't see this as an addition for people that are actually working on a coherent project. I see it as syntax that allows to quickly test something. I definitely share the point of view that for projects, a dedicated .cfg should be the (single) source of truth for configuration options.

But let me put it differently: I see some arguments (convenience for quick testing and prototyping) in favor of allowing to pass configuration options to the render method of a scene, but not really any downsides. I even believe that allowing to pass rendering options makes it easier to use manim from other libraries (where you mainly want to work with the Python interface, and not worry about .cfg files or using the CLI).

As a thought experiment (and I admit, this does derail this discussion a bit), imagine if matplotlib could only be used like manim: your plot output settings have to be saved to a separate matplotlib.cfg file beforehand, and while it would be possible to hack something together from your script, the encouraged way of compiling would be using a matplotlib command line interface. Sure, it's not fair to compare the libraries in that way – but I can't keep myself from wondering whether a more library- and scripting-centered interface wouldn't be more Pythonic.

@huguesdevimeux
Copy link
Member

huguesdevimeux commented Aug 13, 2021

But let me put it differently: I see some arguments (convenience for quick testing and prototyping) in favor of allowing to pass configuration options to the render method of a scene, but not really any downsides. I even believe that allowing to pass rendering options makes it easier to use manim from other libraries (where you mainly want to work with the Python interface, and not worry about .cfg files or using the CLI).

Well, it's just a question of code organization and practices. The main idea behind using config (and not putting everything in constant.py) was to establish a sort of separation between scene settigns. The idea was to improve code clarity by letting only pure-manim within a python file. For example, I would be against implementing some sort of control of caching in the public API ; I think that this is something related to the rendering process, that should be separated from the pure-manim stuff, so set though either config and CLI.

That being said, if one want to quickly hack something, one uses CLI flags, they provide largely enough configuration to span any quick need. The real problem is that we are heading toward a no-CLI manim, which wasn't the case when the config/constants separation was established. But my point still stands, I strongly think that this separation is necessary.

Anyway, that's not the debate, but this is nevertheless a very fair one that will be imposed to us soon.

Concerning this approach, I disapprove it (even with the config debate being aside) and I'm in favor of a Matplotlib like design (no indentation, scene = Scene() \ scene.play(Create(..)), but I would love to hear other's point of view.

@kolibril13
Copy link
Member

I would be also in favor of a matplotlib-like behavior, e.g.

scn = Scene()
d= Dot()
scn.add(d)
scn.render()

That would have the following benefits in my opinion:

  • It is easier to write documentation, as less indention is needed
  • It is easier to write documentation as one does not have to give names for the scenes, e.g. CircleExample
  • When copy and pasting from the documentation examples, it would be great to get a building block that is ready to use, without further adjustment. (e.g. now one has to remove the CircleExample class name and getting the indention right.)
  • it feels more like other python modules like e.g. matplotlib, and I think ecspecially for jupyter notebooks, that would be a really great feature (see issue: Getting rid of indentations for manim im jupyter #1707)

@behackl
Copy link
Member Author

behackl commented Aug 13, 2021

Just to state this explicitly:

  • I don't want to deprecate our CLI, but I think the CLI should only set the config options passed to it (plus instantiating the requested class + call SelectedClass.render()). I'm not sure to which degree this is currently true.
  • I don't want to change the default way how we write scenes, at least not with this PR. This is exclusively about introducing an alternative way of writing scenes, and whether we should make this alternative available to users or not.
  • The matplotlib-way of doing things (import manim as mn; scene = mn.Scene(); scene.play(mn.Create(mn.Square())) etc.) feels a bit odd to me, because Manim's users need to access way more Manim classes than in matplotlib's case. I think it's a good idea to move more in that direction, but at the moment I don't see how this script-oriented paradigm combines well with keeping the current state of config and not allowing to pass / override parameters in a more convenient way directly from the script.
  • The discussion of whether we should work on making it easier to set config options (e.g., by passing them to the render method) can be entirely separate from this PR. It is loosely connected, but the decorator works just as well without being able to pass config options.

@hydrobeam
Copy link
Member

Personally, I'm a fan of the decorator syntax. It removes a layer of bloat from having to write: def construct(self): and the extra layer of indentation. Also I'm a very big fan of being able to pass in config options easily in the file. When drafting many small scenes (either for a project, or for testing), having this kind of flexibility is incredibly useful, and circumvents having to write:

class MySc(Scene):
  config.background_color = WHITE
  config.frame_width = 20

which applies the config changes to every scene within the file. For unified projects and very large scenes, this isn't a big concern, but for ease of use and quick drafting/testing, attaching config to a specific scene makes a lot more sense than project/manim-wide configurations. If you wanted to have two scenes in a project with different settings (i.e a lightmode/darkmode scene), this would be very challenging with what we currently have.

As for the matplotlib-esque approach, it feels a little off to me. Manim's currently based on distinct scenes, so having them grouped/organized via a level of indentation makes sense to me. It might be useful if you're sharing lots of mobjects across several scenes? But I'm not sure how common of a use-case this is.

@huguesdevimeux
Copy link
Member

I would be also in favor of a matplotlib-like behavior, e.g.

scn = Scene()
d= Dot()
scn.add(d)
scn.render()

This Matplotlib like approach is close to the Builder design pattern, except it build is called render.
I'm afraid it won't work with manim, because the methods play, etc. are linked to a timeline, and therefore must be executed directly during their initial calls (i.e, you can't "store" their calls and call them after, when the scene is built). You can't remove the .render at the end because Manim needs an end-scene marker, to e.g. combine the partial movie files.

A fix would be to use the context manager-like approach, which is also great in my eyes.

@leleogere
Copy link
Contributor

I like the possibility to be able to directly run the python file to render the scene without the need to use the command line. Most IDEs provide a simple way to run a python file. I know that they often propose to configure that shortcut to something else that would run the appropriate manim command, but I feel like it's a bit overkilling for quick tests and debugging.

@behackl
Copy link
Member Author

behackl commented Aug 30, 2021

I'm not sure whether we have reached a conclusion about this. Just to make sure we are on the same page, the latest version of this proposal is an interface like

from manim import *

@manim_scene
def SquareToCircle(scene):
    s = Square()
    c = Circle()
    scene.play(Create(s))
    scene.play(Transform(s, c))
    scene.play(FadeOut(*scene.mobjects))

# SquareToCircle is *usually* just a function, but the manim_scene decorator
# turns it directly into a Scene object

SquareToCircle.render()

This PR is only about this interface; while personally I would like to be able to pass config options for rendering as kwargs to Scene.render, this can be discussed completely separately. :-)

@behackl
Copy link
Member Author

behackl commented Oct 2, 2021

I'm closing this for now. Should the community be interested in such a feature at some point in the future, we can come back to it I guess.

@behackl behackl closed this Oct 2, 2021
@behackl behackl deleted the manim-scene-decorator branch May 16, 2022 09:16
@behackl behackl restored the manim-scene-decorator branch May 18, 2022 09:12
@behackl
Copy link
Member Author

behackl commented May 18, 2022

I'd like to revisit this. My central thought behind it is that I feel the library should be flexible enough to support the full range between CLI-based rendering (which, at this point, I am sort of satisfied with -- the CLI itself needs some improvements and cleanups, but generally speaking the concept behind it is solid and works well) and script-based rendering.

The pattern in which we write and render scenes from within a script via

class MySuperScene(Scene):
    def construct(self):
        # animation code

scene_instance = MySuperScene()
scene_instance.render()

feels bloated in contrast to the decorator-based approach from above where we have

@manimation
def my_super_scene(scene):
    # animation code

my_super_scene.render()

(Note: forget about the __call__ idea and passing additional config options to render that were discussed earlier in this PR, this is purely about the decorator that turns a specified function into an object of a class where the decorated method is used as construct.)

While yes, it might be unintuitive that the decorator returns an object rather than another function, I don't really feel that this is bad design -- the behavior just needs to be documented properly.

Any thoughts?

@behackl behackl reopened this May 18, 2022
@MrDiver MrDiver linked an issue Jun 16, 2022 that may be closed by this pull request
@behackl behackl changed the title Proof of concept: Decorator for building a scene from a construct method Implemented decorator :func:.manimation for building a scene from a construct method Jul 10, 2022
@behackl behackl marked this pull request as ready for review July 10, 2022 07:26
Copy link
Collaborator

@MrDiver MrDiver left a comment

Choose a reason for hiding this comment

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

I think it is time that we finally merge this! Because it's just a great addition.

Im thinking if that would be possible to integrate in to the tests ? and maybe specify renderers in the decorator at some point? That would simplify the process a little bit i suppose!

But anyway great addition and i think config options and all that stuff should just be added after some triage of the community.

And in no way i feel this is bad design. It actually seems pretty intuitive that it returns an object, at least i never questioned that it does that.

@huguesdevimeux
Copy link
Member

huguesdevimeux commented Nov 17, 2022

Hello and thanks for the review request.

I was out of the loop for some time (this issue is more than one year old).

I'm still not really in favour of this syntax change, and for two reasons :

  • The main one is that I'm not sure if we can reach an equilibrium. I doubt we will be able in the future to equally maintain the two versions, and at some point either any dev will spend twice as much time or we will end up fostering one approach rather than the other.
  • I highly doubt about the forward compatibility of such change : it will likely bring down some good ideas, just because they won't be compatible with the two syntaxes.
  • Such change has to be documented somewhere, and if MC wants to have a good documentation, it's likely that we will have to include two syntaxes in every example.

To put all in one, I'm not especially against the new syntax - It's a not a really bad design from my POV. I'm just against oscillating between two dramatically different syntaxes, because it will create confusion, additional work if we want to make things thoroughly, and most importantly, it will make MC appears like without having a clear direction of development.

As to chose, I like better the decorator approach (I know, I changed my mind if you read above messages ^^), but I also think the construct approach is way more coherent given how manim is built, and, well, works well.

PS : I don't want to make my opinion blocking on this topic

@MrDiver MrDiver added this to the v0.18.0 milestone Dec 18, 2022
@MrDiver MrDiver removed the request for review from huguesdevimeux December 26, 2022 16:41
manim/scene/scene.py Fixed Show fixed Hide fixed
manim/scene/scene.py Fixed Show fixed Hide fixed
christopher-hampson and others added 25 commits November 4, 2024 14:13
* feat: added dvipsnames color palette

* added svgnames color palette

* fixed docstrings

* Commit changes requested by JasonGrace2282

* Double backticks in xcolor

---------

Co-authored-by: Francisco Manríquez <[email protected]>
Co-authored-by: Francisco Manríquez Novoa <[email protected]>
Co-authored-by: Aarush Deshpande <[email protected]>
* Add type hints to cli

* Import click.Parameter instead of non existing cloup.Parameter

* Stop ignoring manim.cli errors in mypy.ini

* Fix mypy errors

* Remove unused TypeVar C

* click.Command.command -> cloup.Group.command

* Address Jason's requested changes

* exit() -> sys.exit()
* Add a time property to scene

* fix mistaken search/replace

* Add typehint
* Add typings to __main__.py

* Remove `Unused parameter.` from docstrings
…LI) set to 0 to end after first animation (ManimCommunity#4013)

* make checks involving from/upto_animation_number more explicit

* add test for flag -n 0,0 for only rendering first animation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Aarush Deshpande <[email protected]>
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2.
- [Changelog](https://github.com/tornadoweb/tornado/blob/v6.4.2/docs/releases.rst)
- [Commits](tornadoweb/tornado@v6.4.1...v6.4.2)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.0 → v0.8.0](astral-sh/ruff-pre-commit@v0.7.0...v0.8.0)
- [github.com/pre-commit/mirrors-mypy: v1.12.1 → v1.13.0](pre-commit/mirrors-mypy@v1.12.1...v1.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Aarush Deshpande <[email protected]>
Co-authored-by: Aarush Deshpande <[email protected]>

from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.color import *
from manim.mobject.types.vectorized_mobject import VGroup, VMobject

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'VGroup' may not be defined if module
manim.mobject.types.vectorized_mobject
is imported before module
manim.mobject.geometry.arc
, as the
definition
of VGroup occurs after the cyclic
import
of manim.mobject.geometry.arc.
'VGroup' may not be defined if module
manim.mobject.types.vectorized_mobject
is imported before module
manim.mobject.geometry.arc
, as the
definition
of VGroup occurs after the cyclic
import
of manim.mobject.geometry.arc.

from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.color import *
from manim.mobject.types.vectorized_mobject import VGroup, VMobject

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'VMobject' may not be defined if module
manim.mobject.types.vectorized_mobject
is imported before module
manim.mobject.geometry.arc
, as the
definition
of VMobject occurs after the cyclic
import
of manim.mobject.geometry.arc.
'VMobject' may not be defined if module
manim.mobject.types.vectorized_mobject
is imported before module
manim.mobject.geometry.arc
, as the
definition
of VMobject occurs after the cyclic
import
of manim.mobject.geometry.arc.
from collections.abc import Iterable
from typing import Any

import manim.mobject.geometry.tips as tips

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
manim.mobject.geometry.tips
begins an import cycle.
else:
self.radius = math.inf
self.radius = np.inf

Check warning

Code scanning / CodeQL

Overwriting attribute in super-class or sub-class Warning

Assignment overwrites attribute radius, which was previously defined in superclass
Arc
.
from manim.typing import InternalPoint3D, Point2D, Point3D, Vector3D
from manim.utils.color import ParsableManimColor

from ..matrix import Matrix # Avoid circular import

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'Matrix' may not be defined if module
manim.mobject.matrix
is imported before module
manim.mobject.geometry.line
, as the
definition
of Matrix occurs after the cyclic
import
of manim.mobject.geometry.line.
'Matrix' may not be defined if module
manim.mobject.matrix
is imported before module
manim.mobject.geometry.line
, as the
definition
of Matrix occurs after the cyclic
import
of manim.mobject.geometry.line.
'Matrix' may not be defined if module
manim.mobject.matrix
is imported before module
manim.mobject.geometry.line
, as the
definition
of Matrix occurs after the cyclic
import
of manim.mobject.geometry.line.
'Matrix' may not be defined if module
manim.mobject.matrix
is imported before module
manim.mobject.geometry.line
, as the
definition
of Matrix occurs after the cyclic
import
of manim.mobject.geometry.line.
'Matrix' may not be defined if module
manim.mobject.matrix
is imported before module
manim.mobject.geometry.line
, as the
definition
of Matrix occurs after the cyclic
import
of manim.mobject.geometry.line.
@@ -2360,7 +2686,7 @@
part.match_style(vmobject)
self.add(part)

def point_from_proportion(self, alpha: float) -> np.ndarray:
def point_from_proportion(self, alpha: float) -> Point3D:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.
from manim.mobject.opengl.opengl_mobject import OpenGLPoint

from .. import config, logger
from ..animation.animation import Animation, Wait, prepare_animation
from .._config import tempconfig

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.
'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.
'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.
'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.
'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.
'Camera' may not be defined if module
manim.camera.camera
is imported before module
manim.scene.scene
, as the
definition
of Camera occurs after the cyclic
import
of manim.scene.scene.


@overload
def interpolate(start: float, end: float, alpha: float) -> float: ...

Check notice

Code scanning / CodeQL

Statement has no effect Note

This statement has no effect.


@overload
def interpolate(start: Point3D, end: Point3D, alpha: float) -> Point3D: ...

Check notice

Code scanning / CodeQL

Statement has no effect Note

This statement has no effect.
cls,
color: Sequence[ParsableManimColor],
alpha: float = ...,
) -> list[Self]: ...

Check notice

Code scanning / CodeQL

Statement has no effect Note

This statement has no effect.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs discussion Things which needs to be discussed before implemented. new feature Enhancement specifically adding a new feature (feature request should be used for issues instead)
Projects
Status: 🔖 Ready for Review
Development

Successfully merging this pull request may close these issues.

Getting rid of indentations for manim im jupyter