Skip to content

Added Function of Generating Animation of DFA and NFA Reading Strings #252

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

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

dsr20030703
Copy link

I have added a animate_reding_input function in DFA and NFA to automatically generate the animation of the DFA or NFA reading the input using manim 0.19.0.

I have also added a Jupyter Notebook for the funciton in the example_notebooks/ folder to show the generated animation.

As there're some problems in the type lint of manim and pygraphviz, which includes:

  1. Argument color to manim.FadeToColor expected str, but it also accepts type ManimColor;
  2. The docstring of pygraphviz.Node and pygraphviz.Edge says each node or edge has attributes that can be directly accessed through the attr dictionary, but they aren't recognized by mypy.

It comes with some errors in typechecking run with mypy. But there aren't any functional bugs (as far as I have checked).

Another problem I have encountered is that I don't know how to add the tests. As manim is an animation engine, they have developed a unique test framework based on pytest, which is different from this project. So I haven't added any test. (But at least the examples in the Jupyter Notebook all works, except the last of which)

Added the docstring for animate_reading_input function.
Removed the FAAnimation class.
Handle the case when the input is empty in animate_reading_input function.
@eliotwrobson
Copy link
Collaborator

@dsr20030703 thanks for this contribution! I'm a bit busy this week, but I'll try and get a review in this weekend 👍

Copy link
Collaborator

@eliotwrobson eliotwrobson left a comment

Choose a reason for hiding this comment

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

Several comments. My main reservation is having certain class definitions behind if statements, but the code itself seems solid. Should consult with @caleb531 to see what the best way to organize these changes is. Overall looks good though, thanks again for this contribution!!

.gitignore Outdated
@@ -24,3 +24,6 @@ nosetests.xml

# MyPy
.mypy_cache

# manim generated files
media/
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should have a newline at the end of the file.

Copy link
Author

Choose a reason for hiding this comment

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

I've added a new line at the end of this file in the newest commit (6149742). And Git at local also has recorded the change. But I don't know why it's missed when uploading to the GitHub. 😕❓
I've added a new line
Git at local also has recorded the change

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you have to configure VSCode to add newlines when saving any file, otherwise you might see weird behavior like this.


DEFAULT_COLOR = manim.WHITE
HIGHLIGHT_COLOR = manim.RED
manim.Circle.set_default(color=DEFAULT_COLOR)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this change a global property of manim? Is there a way to do this local to what is animated from this class?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it changes the default color of manim.Circle from manim.RED to manim.WHITE globally. A way to do this local to what is animated from this class is by specifying color=Animate.DEFAULT_COLOR in every manim.MObject created, which is what I did at first. After that, I thought it may be too troublesome. As most of manim.MObject except manim.Circle uses manim.WHITE as default color, I chose to change the default color of manim.Circle to manim.WHITE globally.

Is the way of specifying color=Animate.DEFAULT_COLOR in every manim.MObject created suggested? If so, I'll revise the code in that way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the way of specifying color=Animate.DEFAULT_COLOR in every manim.MObject created suggested? If so, I'll revise the code in that way.

I think this should be possible through inheritance or a helper function? You could have an object creation function that creates and returns the objects with these defaults set.

Copy link
Author

Choose a reason for hiding this comment

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

Is a helper function like this OK? (partial is from functools)

Suggested change
manim.Circle.set_default(color=DEFAULT_COLOR)
M = TypeVar("M", bound=manim.Mobject)
@classmethod
def default_init(cls, mobject_class: type[M]):
return partial(mobject_class, color=cls.DEFAULT_COLOR)

It can handle all subclasses of manim.Mobject like manim.Circle, manim.Text, etc. In that case, we would use Animate.default_init(/*a manim.Mobject class*/) to replace the default init function of that class. For example, manim.Text(node.name, font_size=float(node.attr["fontsize"])) will be replaced by Animate.default_init(manim.Text)(node.name, font_size=float(node.attr["fontsize"])), and manim.Circle() will be repalced by Animate.default_init(manim.Circle)() (seems that the latter () are easy to forget though).

Copy link
Collaborator

@eliotwrobson eliotwrobson Mar 28, 2025

Choose a reason for hiding this comment

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

I think that seems reasonable! My main motivation for this comment is to avoid side effects that can cause other animation code to behave unexpectedly, and I think this is a solid way of doing that.

EDIT: Be sure to annotate the return type here as well (I think it is also type[M]).

shape: manim.Dot | manim.Circle | manim.VGroup
label: Optional[manim.Text]

def __init__(self, node: pgv.Node):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
def __init__(self, node: pgv.Node):
def __init__(self, node: pgv.Node) -> None:

self.add(self.shape)
self.label = manim.Text(node.name, font_size=float(node.attr["fontsize"]))
self.add(self.label)
x, y = (float(pt) / 72 for pt in node.attr["pos"].split(","))
Copy link
Collaborator

Choose a reason for hiding this comment

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

72 is a magic number here, could this be set at the class level or at the top of the file?

Copy link
Author

Choose a reason for hiding this comment

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

72 is the ratio between inch and point (actually, big boint). Graphviz measures some attributes of Graph, Node and Edge in points, while some attibutes are measured in inches.

Is adding a constant like _POINTS_IN_INCH = 72 at the top of the file, and replacing all "72" with "_POINTS_IN_INCH" OK?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think that's a good strategy 👍🏽

string are separated with each character is generated to a `Text` object, so that
you can get each character simply with `[]` operator."""

def __init__(self, text: str):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
def __init__(self, text: str):
def __init__(self, text: str) -> None:


if fa._visual_imports:

@dataclass(eq=False)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we put this in the animation file, or make a separate folder for just animations? I'm not a fan of having an entire class definition behind an if statement like this, but not 100% sure what the best way to do this is. @caleb531 any thoughts on this?

Copy link
Author

Choose a reason for hiding this comment

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

As _DFAAnimation and _NFAAnimation requires DFA and NFA respectively, and the animate_reading_input function in DFA and NFA needs to call the initialize function of _DFAAnimation and _NFAAnimation respectively, it seems that circular dependencies can't be avoided. :( If we put them into other files, like a new automata/fa/animation.py file, then we have to put the import statement inside the animate_reading_input function to prevent circular import, like this:

class DFA:
    ...
    def animate_reading_input(self, input_str: str, preview: bool = False) -> None:
        if not fa._visual_imports:
            raise ImportError(...)
        from automata.fa.animation import _DFAAnimation

        _DFAAnimation(self, input_str).render(preview)

Is that acceptable?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let me think about this some and get back to you. Circular dependencies like these can usually be resolved by changing the design somehow.

Also, I think the circular dependency may only be because of the DFA / NFA type annotation? In that case, you can put those imports behind an if TYPE_CHECKING guard, see here for discussion.

Copy link
Author

Choose a reason for hiding this comment

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

Also, I think the circular dependency may only be because of the DFA / NFA type annotation? In that case, you can put those imports behind an if TYPE_CHECKING guard, see here for discussion.

It works! Now we don't need to import the module when calling.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for adding the example notebook!

if not fa._visual_imports:
raise ImportError(
"Missing visualization packages; "
"please install pygraphviz, coloraide, and manim."
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you still need to add manim to the visual imports section in fa.py. I think it would also be good to add a helper function that checks for the visual imports and raises an exception if they are not present (i.e. just put this if block into a helper function in fa.py).

Copy link
Author

Choose a reason for hiding this comment

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

In spite of that manim isn't used in fa.py? Plus, I don't think that checking for the visual imports every time when calling them is a good idea, as the environment won't change when the program is running. Perhaps setting a variable recording the result of checking for the visual imports using importlib in the automata.base.configs would be better? In that case, we don't need to "try-import-except ImportError" to check visual imports every time, just coding

if config._visual_imports:
    import ...

is OK. Plus, we could let the _visual_imports to be a Optional[ImportError] object. When visual imports exists, its value is None; while when visual imports doesn't exist, its value is ImportError("Missing visualization packages; please install ...") (while in that case, it may be named as miss_visual_imports). So that we don't need to change all the notes of ImportErrors when we want to change the visualization packages.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@dsr20030703 I think that idea makes sense. My suggestion was mainly to try to avoid repeating this message, so using the optional import error would accomplish this 👍🏽

@eliotwrobson
Copy link
Collaborator

@dsr20030703 the overall structure of the code looks good now, but it looks like tests are failing. If you can get the tests passing, then I can do a full review 👍🏽

@dsr20030703
Copy link
Author

It seems that checks failed when installing dependencies because some additional dependencies are required to install on Linux. Should I add a new step after "Setup graphviz" in .github/workflows/build.yml like this?

- name: Build Additional Dependencies for manim
  run: |
    sudo apt update
    sudo apt install -y build-essential python3-dev libcairo2-dev libpango1.0-dev

@eliotwrobson
Copy link
Collaborator

@dsr20030703 go for it 👍

requirements.txt Outdated
Copy link
Collaborator

Choose a reason for hiding this comment

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

Missing a newline, and could you add the new dependency in alphabetical order with the rest?

Copy link
Owner

@caleb531 caleb531 left a comment

Choose a reason for hiding this comment

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

@eliotwrobson @dsr20030703 Apologies for the very late response from me.

I'm gonna be completely honest, but my initial impression is that animation feels out-of-scope for this package. Looking over the PR, it feels like it's going to add a lot of weight that I think would be better suited for a fork. However, I am open to discussion on this point.

Also, out of curiosity, can the output directory be customized? It feels rather restrictive to have the generated files be restrictively written to a media/ directory.

@dsr20030703
Copy link
Author

dsr20030703 commented Apr 1, 2025

@caleb531

I'm gonna be completely honest, but my initial impression is that animation feels out-of-scope for this package. Looking over the PR, it feels like it's going to add a lot of weight that I think would be better suited for a fork. However, I am open to discussion on this point.

Will adding a new optional-dependencies like animation be acceptable? The result of the current visualization function, FA.show_diagram, when provided with a long input_str, frankly speaking, may suck. For example, the following code

dfa = DFA(
    states={"q0", "q1", "q2", "q3"},
    input_symbols={"0", "1"},
    transitions={
        "q0": {"0": "q2", "1": "q1"},
        "q1": {},
        "q2": {"0": "q3", "1": "q1"},
        "q3": {"0": "q2", "1": "q1"},
    },
    initial_state="q0",
    final_states={"q3"},
    allow_partial=True,
)
dfa.show_diagram("1111111111111")

will produce such a graph
dfa

Also, out of curiosity, can the output directory be customized? It feels rather restrictive to have the generated files be restrictively written to a media/ directory.

Yes, there're a lot of configurations in manim. Users can customize the output directory with manim render --custom_folders.

@caleb531
Copy link
Owner

caleb531 commented Apr 1, 2025

Will adding a new optional-dependencies like animation be acceptable? The result of the current visualization function, FA.show_diagram, when provided with a long input_str, frankly speaking, may suck. For example, the following code
...
Yes, there're a lot of configurations in manim. Users can customize the output directory with manim render --custom_folders.

Is it truly an optional dependency, though? Because I also saw your comment about needing to globally install Manim😕

It seems that checks failed when installing dependencies because some additional dependencies are required to install on Linux. Should I add a new step after "Setup graphviz" in .github/workflows/build.yml like this?

- name: Build Additional Dependencies for manim
  run: |
    sudo apt update
    sudo apt install -y build-essential python3-dev libcairo2-dev libpango1.0-dev

I am mostly concerned about how people who don't need the animation functionality will potentially be required to install these sub-dependencies (or global dependencies, whatever you want to call them). But if we are able to work around this through the use of optional dependencies / addons (like we have the [visual] addon already), then I'd still consider myself open to this functionality.

cc @eliotwrobson

@eliotwrobson
Copy link
Collaborator

I am mostly concerned about how people who don't need the animation functionality will potentially be required to install these sub-dependencies (or global dependencies, whatever you want to call them). But if we are able to work around this through the use of optional dependencies / addons (like we have the [visual] addon already), then I'd still consider myself open to this functionality.

I could be wrong, but I think the way this PR is set up, the global dependencies are only needed if you're using the animation (i.e. someone could just install automata without the visual extra and everything would work fine), but we need to install everything in CI to run tests. I definitely agree though that there should be a way to make this additional functionality optional.

Made requirements.txt in alphabetical order.
@dsr20030703
Copy link
Author

It seems that build.yml, lint.yml and tests.yml shall be changed simultaneously when adding new dependencies. As the first 4 steps of them are same, could those steps be extracted and shared by 3 jobs so that just 1 place need to be changed?

Also, it seems that this project is going to adopt uv? It would help a lot in setting up since Manim also recommends using uv. Shall I wait until #254 gets reviewed and merged?

@caleb531
Copy link
Owner

caleb531 commented Apr 4, 2025

It seems that build.yml, lint.yml and tests.yml shall be changed simultaneously when adding new dependencies. As the first 4 steps of them are same, could those steps be extracted and shared by 3 jobs so that just 1 place need to be changed?

@dsr20030703 Are you familiar with how to consolidate shared steps among GHA workflows like this? I'm not as familiar, but if you are, I would love a PR for that sort of thing. The duplication between the files bothers me too.

Also, it seems that this project is going to adopt uv? It would help a lot in setting up since Manim also recommends using uv. Shall I wait until #254 gets reviewed and merged?

I would say yes, probably. @eliotwrobson, do you have any thoughts on this?

@eliotwrobson
Copy link
Collaborator

@caleb531 I agree, I think #254 is pretty close as well, so we should be able to close that out soon. I need a little bit of input from you there since I'm not 1000% how you want the deployment set up, and there's ways to consolidate the number of actions run if we would like.

@dsr20030703
Copy link
Author

It seems that build.yml, lint.yml and tests.yml shall be changed simultaneously when adding new dependencies. As the first 4 steps of them are same, could those steps be extracted and shared by 3 jobs so that just 1 place need to be changed?

@dsr20030703 Are you familiar with how to consolidate shared steps among GHA workflows like this? I'm not as familiar, but if you are, I would love a PR for that sort of thing. The duplication between the files bothers me too.

I’m not familiar with that either. 😢 Seems that we can combine these 3 files into 1 file, then extract the 4 common steps into a new job, like setup, then let the other 3 jobs needs the setup job. So the combined yaml file would be like this:

name: CI



jobs:
  setup:
    

    steps:
      # the common 4 steps

  build:
    needs: setup
    

  lint:
    needs: setup
    

  tests:
    needs: setup
    

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 this pull request may close these issues.

3 participants