Repository: ManimCommunity/manim

Issue:
Calling `Circle.point_at_angle(0)` produces an error
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->

Calling `point_at_angle` with an angle of `0` produces an error.

## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->

Calling `point_at_angle` with an angle of `0` should return a point on the circumference of the circle.

## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

<details><summary>Code for reproducing the problem</summary>

```py
class Test(Scene):
    def construct(self):
        c1 = Circle(radius=0.5, color=RED).move_to(LEFT)
        c2 = Circle(radius=0.5, color=RED).move_to(RIGHT)

        def get_start_end():
            return c1.point_at_angle(0), c2.point_at_angle(PI)

        a = Arrow(*get_start_end())
        a.add_updater(lambda z: z.become(Arrow(*get_start_end())))

        self.add(c1, c2, a)

        self.play(c1.animate.shift(UP * 2))
```

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
Manim Community v0.15.1

[03/12/22 15:18:56] DEBUG    Hashing ...                                                                                                         hashing.py:343
                    DEBUG    Hashing done in 0.014411 s.                                                                                         hashing.py:355
                    DEBUG    Hash generated :  2543892337_2626425972_243081626                                                                   hashing.py:358
                    DEBUG    List of the first few animation hashes of the scene: ['2543892337_2626425972_243081626']                      cairo_renderer.py:84
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮                                                           
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/cli/render/commands.py:141 in render                │
│                                                                                                  │
│   138 │   │   for SceneClass in scene_classes_from_file(file):                                   │
│   139 │   │   │   try:                                                                           │
│   140 │   │   │   │   scene = SceneClass()                                                       │
│ ❱ 141 │   │   │   │   scene.render()                                                             │
│   142 │   │   │   except Exception:                                                              │
│   143 │   │   │   │   error_console.print_exception()                                            │
│   144 │   │   │   │   sys.exit(1)                                                                │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:222 in render                        │
│                                                                                                  │
│    219 │   │   """                                                                               │
│    220 │   │   self.setup()                                                                      │
│    221 │   │   try:                                                                              │
│ ❱  222 │   │   │   self.construct()                                                              │
│    223 │   │   except EndSceneEarlyException:                                                    │
│    224 │   │   │   pass                                                                          │
│    225 │   │   except RerunSceneException as e:                                                  │
│                                                                                                  │
│ /manim/scenes.py:322 in construct                                                                │
│                                                                                                  │
│   319 │   │                                                                                      │
│   320 │   │   self.add(c1, c2, a)                                                                │
│   321 │   │                                                                                      │
│ ❱ 322 │   │   self.play(c1.animate.shift(UP * 2))                                                │
│   323                                                                                            │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:957 in play                          │
│                                                                                                  │
│    954 │   │                                                                                     │
│    955 │   │   """                                                                               │
│    956 │   │   start_time = self.renderer.time                                                   │
│ ❱  957 │   │   self.renderer.play(self, *args, **kwargs)                                         │
│    958 │   │   run_time = self.renderer.time - start_time                                        │
│    959 │   │   if subcaption:                                                                    │
│    960 │   │   │   if subcaption_duration is None:                                               │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/renderer/cairo_renderer.py:101 in play              │
│                                                                                                  │
│    98 │   │   │   # In this case, as there is only a wait, it will be the length of the wait.    │
│    99 │   │   │   self.freeze_current_frame(scene.duration)                                      │
│   100 │   │   else:                                                                              │
│ ❱ 101 │   │   │   scene.play_internal()                                                          │
│   102 │   │   self.file_writer.end_animation(not self.skip_animations)                           │
│   103 │   │                                                                                      │
│   104 │   │   self.num_plays += 1                                                                │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:1122 in play_internal                │
│                                                                                                  │
│   1119 │   │   │   self.duration,                                                                │
│   1120 │   │   )                                                                                 │
│   1121 │   │   for t in self.time_progression:                                                   │
│ ❱ 1122 │   │   │   self.update_to_time(t)                                                        │
│   1123 │   │   │   if not skip_rendering and not self.skip_animation_preview:                    │
│   1124 │   │   │   │   self.renderer.render(self, t, self.moving_mobjects)                       │
│   1125 │   │   │   if self.stop_condition is not None and self.stop_condition():                 │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:1356 in update_to_time               │
│                                                                                                  │
│   1353 │   │   │   animation.update_mobjects(dt)                                                 │
│   1354 │   │   │   alpha = t / animation.run_time                                                │
│   1355 │   │   │   animation.interpolate(alpha)                                                  │
│ ❱ 1356 │   │   self.update_mobjects(dt)                                                          │
│   1357 │   │   self.update_meshes(dt)                                                            │
│   1358 │   │   self.update_self(dt)                                                              │
│   1359                                                                                           │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:341 in update_mobjects               │
│                                                                                                  │
│    338 │   │   │   Change in time between updates. Defaults (mostly) to 1/frames_per_second      │
│    339 │   │   """                                                                               │
│    340 │   │   for mobject in self.mobjects:                                                     │
│ ❱  341 │   │   │   mobject.update(dt)                                                            │
│    342 │                                                                                         │
│    343 │   def update_meshes(self, dt):                                                          │
│    344 │   │   for obj in self.meshes:                                                           │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/mobject.py:806 in update                    │
│                                                                                                  │
│    803 │   │   │   if "dt" in parameters:                                                        │
│    804 │   │   │   │   updater(self, dt)                                                         │
│    805 │   │   │   else:                                                                         │
│ ❱  806 │   │   │   │   updater(self)                                                             │
│    807 │   │   if recursive:                                                                     │
│    808 │   │   │   for submob in self.submobjects:                                               │
│    809 │   │   │   │   submob.update(dt, recursive)                                              │
│                                                                                                  │
│ /manim/scenes.py:318 in <lambda>                                                                 │
│                                                                                                  │
│   315 │   │   │   return c1.point_at_angle(0), c2.point_at_angle(PI)                             │
│   316 │   │                                                                                      │
│   317 │   │   a = Arrow(*get_start_end())                                                        │
│ ❱ 318 │   │   a.add_updater(lambda z: z.become(Arrow(*get_start_end())))                         │
│   319 │   │                                                                                      │
│   320 │   │   self.add(c1, c2, a)                                                                │
│   321                                                                                            │
│                                                                                                  │
│ /manim/scenes.py:315 in get_start_end                                                            │
│                                                                                                  │
│   312 │   │   c2 = Circle(radius=0.5, color=RED).move_to(RIGHT)                                  │
│   313 │   │                                                                                      │
│   314 │   │   def get_start_end():                                                               │
│ ❱ 315 │   │   │   return c1.point_at_angle(0), c2.point_at_angle(PI)                             │
│   316 │   │                                                                                      │
│   317 │   │   a = Arrow(*get_start_end())                                                        │
│   318 │   │   a.add_updater(lambda z: z.become(Arrow(*get_start_end())))                         │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/geometry/arc.py:578 in point_at_angle       │
│                                                                                                  │
│    575 │   │   """                                                                               │
│    576 │   │                                                                                     │
│    577 │   │   start_angle = angle_of_vector(self.points[0] - self.get_center())                 │
│ ❱  578 │   │   return self.point_from_proportion((angle - start_angle) / TAU)                    │
│    579 │                                                                                         │
│    580 │   @staticmethod                                                                         │
│    581 │   def from_three_points(                                                                │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/types/vectorized_mobject.py:1231 in         │
│ point_from_proportion                                                                            │
│                                                                                                  │
│   1228 │   │   """                                                                               │
│   1229 │   │                                                                                     │
│   1230 │   │   if alpha < 0 or alpha > 1:                                                        │
│ ❱ 1231 │   │   │   raise ValueError(f"Alpha {alpha} not between 0 and 1.")                       │
│   1232 │   │                                                                                     │
│   1233 │   │   self.throw_error_if_no_points()                                                   │
│   1234 │   │   if alpha == 1:                                                                    │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: Alpha -1.076750282787128e-17 not between 0 and 1.
$
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

- OS: `Ubuntu 20.04.1 LTS`
- Using the following Docker container: `manimcommunity/manim:stable`


Discussion:
Based on the error, it looks like a possible floating-point precision issue at
https://github.com/ManimCommunity/manim/blob/0bc593a8a07e58d1707ca736849ba7d57d9b2534/manim/mobject/geometry/arc.py#L578

The error also occurs if a value greater than `2 * PI` is used.

I propose making the logic slightly smarter and wrapping the proportion around to the start/end if it is out of bounds.

```py
proportion = (angle - start_angle) / TAU
proportion -= math.floor(proportion)
return self.point_from_proportion(proportion)
```
Thank you for the report! I could not reproduce it locally using 0, but arguments larger than 2 pi definitely have the same problem. Your proposed solution looks good to me; would you be willing to submit a pull request with this change and an added unit test where `circle.point_at_angle(3*PI)` or something like that is tested?
I think this would break when `angle < start_angle` ?
How about this?
```py
proportion = abs((angle - start_angle) / TAU)
proportion -= math.floor(proportion)
return self.point_from_proportion(proportion)
```
> I think this would break when `angle < start_angle` ? How about this?
> 
> ```python
> proportion = abs((angle - start_angle) / TAU)
> proportion -= math.floor(proportion)
> return self.point_from_proportion(proportion)
> ```

No, `proportion - floor(proportion)` is (or at least should be, mathematically) always between 0 and 1; independent of the sign of proportion.
> would you be willing to submit a pull request with this change and an added unit test where `circle.point_at_angle(3*PI)` or something like that is tested?

Sure. Which file should the unit test go in? I can't see any tests already for `point_at_angle`.
> > would you be willing to submit a pull request with this change and an added unit test where `circle.point_at_angle(3*PI)` or something like that is tested?
> 
> Sure. Which file should the unit test go in? I can't see any tests already for `point_at_angle`.

Our non-graphical tests are a bit of a mess; you could just put the test in `tests/test_mobject.py`for now. We intend to restructure the tests soon anyways, #2591.

Resolving patch:
diff --git a/manim/mobject/geometry/arc.py b/manim/mobject/geometry/arc.py
--- a/manim/mobject/geometry/arc.py
+++ b/manim/mobject/geometry/arc.py
@@ -575,7 +575,9 @@ def construct(self):
         """
 
         start_angle = angle_of_vector(self.points[0] - self.get_center())
-        return self.point_from_proportion((angle - start_angle) / TAU)
+        proportion = (angle - start_angle) / TAU
+        proportion -= math.floor(proportion)
+        return self.point_from_proportion(proportion)
 
     @staticmethod
     def from_three_points(


Test patch:
diff --git a/tests/test_vectorized_mobject.py b/tests/test_vectorized_mobject.py
--- a/tests/test_vectorized_mobject.py
+++ b/tests/test_vectorized_mobject.py
@@ -289,3 +289,9 @@ def test_vmobject_different_num_points_and_submobjects_become():
     a.become(b)
     assert np.array_equal(a.points, b.points)
     assert len(a.submobjects) == len(b.submobjects)
+
+
+def test_vmobject_point_at_angle():
+    a = Circle()
+    p = a.point_at_angle(4 * PI)
+    assert np.array_equal(a.points[0], p)