From 7ffaed0ac7c5f5cb0eec113ca82f13a63d08be2d Mon Sep 17 00:00:00 2001 From: GoThrones Date: Wed, 4 Mar 2026 18:41:22 +0530 Subject: [PATCH 1/9] Improvement to ComplexValueTracker.set_value to accept either a complex number(either in rectangular coordinate form or in polar form) or a tuple, list, string, or numpy array --- manim/mobject/value_tracker.py | 96 ++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 947a3c001f..c78a0a8960 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any import numpy as np +from collections.abc import Sequence from manim.mobject.mobject import Mobject from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL @@ -235,8 +236,93 @@ def get_value(self) -> complex: # type: ignore [override] """Get the current value of this ComplexValueTracker as a complex number.""" return complex(*self.points[0, :2]) - def set_value(self, value: complex | float) -> Self: - """Sets a new complex value to the ComplexValueTracker.""" - z = complex(value) - self.points[0, :2] = (z.real, z.imag) - return self + def set_value( + self, + value: complex | float | int | str | Sequence[float | int] | np.ndarray = 0+0j, + mode: str = "rectangular", # "rectangular" or "polar" + angle_unit: str = "radians" # "radians" or "degrees" — only used when mode="polar" + ) -> Self: + """ + Sets a new complex value to the ComplexValueTracker. + + Parameters + ---------- + value : complex | float | int | str | Sequence[float | int] | np.ndarray + The value to set. It can be: + - a complex number: 2+3j + - a float or int: 5.0 or 5 + - a valid numeric string: "23" or "2+3j" + - a sequence of exactly 2 real numbers: (2, 3), [2, 3], np.array([2, 3]) + - if mode="rectangular": interpreted as (x, y) + - if mode="polar": interpreted as (r, theta) + - theta can be in radians or degrees, specified by angle_unit + mode : str + "rectangular" (default) or "polar". + Only relevant when value is a sequence. + angle_unit : str + "radians" (default) or "degrees". + Only relevant when mode="polar". + If "degrees", theta is converted to radians internally. + + Examples + -------- + set_value(2+3j) # rectangular complex + set_value((2, 3)) # rectangular sequence + set_value((1, 90), mode="polar", angle_unit="degrees") # polar, degrees + set_value((1, np.pi/2), mode="polar") # polar, radians + """ + + # validate mode + if mode not in ("rectangular", "polar"): + raise ValueError( + f"mode must be 'rectangular' or 'polar', got '{mode}'" + ) + + # validate angle_unit + if angle_unit not in ("radians", "degrees"): + raise ValueError( + f"angle_unit must be 'radians' or 'degrees', got '{angle_unit}'" + ) + + if isinstance(value, (list, tuple, np.ndarray)): + # length check + if len(value) != 2: + raise ValueError( + f"Expected exactly 2 numbers, got {len(value)}" + ) + # check for type of number provided and finiteness check + if not all(np.isreal(v) and np.isfinite(v) for v in value): + raise TypeError( + f"Elements must be real and finite numbers — no NAN(Not a Number) or infinity is allowed" + ) + a, b = value + + if mode == "polar": + r, theta = a, b + if r < 0: + raise ValueError( + f"Radius r must be non-negative in polar form, got {r}" + ) + # convert degrees to radians if needed + if angle_unit == "degrees": + theta = np.deg2rad(theta) + x = r * np.cos(theta) + y = r * np.sin(theta) + else: # rectangular + x, y = a, b + + else: + z = complex(value) # handles complex, float, int, valid strings + # check real and imag parts individually for finiteness + if not np.isfinite(z.real): + raise ValueError( + f"Real part must be finite, got {z.real}" + ) + if not np.isfinite(z.imag): + raise ValueError( + f"Imaginary part must be finite, got {z.imag}" + ) + x, y = z.real, z.imag + + self.points[0, :2] = (x, y) + return self \ No newline at end of file From 17e9ad195dac78ae44a36c65149b5fec9e0286c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:28:47 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/value_tracker.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index c78a0a8960..aa190c5d5e 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -4,10 +4,10 @@ __all__ = ["ValueTracker", "ComplexValueTracker"] +from collections.abc import Sequence from typing import TYPE_CHECKING, Any import numpy as np -from collections.abc import Sequence from manim.mobject.mobject import Mobject from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL @@ -238,9 +238,10 @@ def get_value(self) -> complex: # type: ignore [override] def set_value( self, - value: complex | float | int | str | Sequence[float | int] | np.ndarray = 0+0j, + value: complex | float | int | str | Sequence[float | int] | np.ndarray = 0 + + 0j, mode: str = "rectangular", # "rectangular" or "polar" - angle_unit: str = "radians" # "radians" or "degrees" — only used when mode="polar" + angle_unit: str = "radians", # "radians" or "degrees" — only used when mode="polar" ) -> Self: """ Sets a new complex value to the ComplexValueTracker. @@ -271,12 +272,9 @@ def set_value( set_value((1, 90), mode="polar", angle_unit="degrees") # polar, degrees set_value((1, np.pi/2), mode="polar") # polar, radians """ - # validate mode if mode not in ("rectangular", "polar"): - raise ValueError( - f"mode must be 'rectangular' or 'polar', got '{mode}'" - ) + raise ValueError(f"mode must be 'rectangular' or 'polar', got '{mode}'") # validate angle_unit if angle_unit not in ("radians", "degrees"): @@ -287,13 +285,11 @@ def set_value( if isinstance(value, (list, tuple, np.ndarray)): # length check if len(value) != 2: - raise ValueError( - f"Expected exactly 2 numbers, got {len(value)}" - ) + raise ValueError(f"Expected exactly 2 numbers, got {len(value)}") # check for type of number provided and finiteness check if not all(np.isreal(v) and np.isfinite(v) for v in value): raise TypeError( - f"Elements must be real and finite numbers — no NAN(Not a Number) or infinity is allowed" + "Elements must be real and finite numbers — no NAN(Not a Number) or infinity is allowed" ) a, b = value @@ -315,14 +311,10 @@ def set_value( z = complex(value) # handles complex, float, int, valid strings # check real and imag parts individually for finiteness if not np.isfinite(z.real): - raise ValueError( - f"Real part must be finite, got {z.real}" - ) + raise ValueError(f"Real part must be finite, got {z.real}") if not np.isfinite(z.imag): - raise ValueError( - f"Imaginary part must be finite, got {z.imag}" - ) + raise ValueError(f"Imaginary part must be finite, got {z.imag}") x, y = z.real, z.imag self.points[0, :2] = (x, y) - return self \ No newline at end of file + return self From 44041423eef69673784a7ad938afe1078fbdd76d Mon Sep 17 00:00:00 2001 From: GoThrones Date: Thu, 5 Mar 2026 08:56:52 +0530 Subject: [PATCH 3/9] fixed: mypy type error in ComplexValueTracker.set_value --- manim/mobject/value_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index c78a0a8960..380e94ea60 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -4,7 +4,7 @@ __all__ = ["ValueTracker", "ComplexValueTracker"] -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Union, cast import numpy as np from collections.abc import Sequence @@ -312,6 +312,7 @@ def set_value( x, y = a, b else: + value = cast(Union[complex, float, int, str], value) z = complex(value) # handles complex, float, int, valid strings # check real and imag parts individually for finiteness if not np.isfinite(z.real): From 25197588928972771775cc5f67c2f2897fa20eee Mon Sep 17 00:00:00 2001 From: GoThrones Date: Thu, 5 Mar 2026 10:49:18 +0530 Subject: [PATCH 4/9] Improving ValueTracker.set_value to accept int, str and reject complex, nan and inf values --- manim/mobject/value_tracker.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 64e01e4da7..522baab5bc 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -85,8 +85,22 @@ def get_value(self) -> float: value: float = self.points[0, 0] return value - def set_value(self, value: float) -> Self: - """Sets a new scalar value to the ValueTracker.""" + def set_value(self, value: float | int | str) -> Self: + if isinstance(value, str): + try: + value = float(value) + except ValueError: + raise ValueError( + f"String '{value}' cannot be converted to a float" + ) + if not np.isreal(value): + raise TypeError( + f"ValueTracker only accepts real numbers — use ComplexValueTracker for having 2 ValueTrackers simultaneously, got {value}" + ) + if not np.isfinite(value): + raise ValueError( + f"Value must be finite — no nan or inf allowed, got {value}" + ) self.points[0, 0] = value return self From 7f23ef036ebfd6ffce2dbd691b29822898396c6b Mon Sep 17 00:00:00 2001 From: GoThrones Date: Thu, 5 Mar 2026 11:04:06 +0530 Subject: [PATCH 5/9] fix: simplify ValueTracker.set_value string handling --- manim/mobject/value_tracker.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 522baab5bc..3d1587dbc8 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -85,14 +85,8 @@ def get_value(self) -> float: value: float = self.points[0, 0] return value - def set_value(self, value: float | int | str) -> Self: - if isinstance(value, str): - try: - value = float(value) - except ValueError: - raise ValueError( - f"String '{value}' cannot be converted to a float" - ) + def set_value(self, value: float | int | str) -> Self: + value = float(value) if not np.isreal(value): raise TypeError( f"ValueTracker only accepts real numbers — use ComplexValueTracker for having 2 ValueTrackers simultaneously, got {value}" From b12173b757c16577b7ea6ff009c4f1ec7fd243d4 Mon Sep 17 00:00:00 2001 From: GoThrones Date: Fri, 6 Mar 2026 16:01:23 +0530 Subject: [PATCH 6/9] Add ThreeDValueTracker to value_tracker.py --- manim/mobject/value_tracker.py | 82 ++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 3d1587dbc8..775387773f 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -2,10 +2,11 @@ from __future__ import annotations -__all__ = ["ValueTracker", "ComplexValueTracker"] +__all__ = ["ValueTracker", "ComplexValueTracker", "ThreeDValueTracker"] from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Union, cast + import numpy as np from manim.mobject.mobject import Mobject @@ -85,8 +86,8 @@ def get_value(self) -> float: value: float = self.points[0, 0] return value - def set_value(self, value: float | int | str) -> Self: - value = float(value) + def set_value(self, value: float | int | str) -> Self: + value = float(value) if not np.isreal(value): raise TypeError( f"ValueTracker only accepts real numbers — use ComplexValueTracker for having 2 ValueTrackers simultaneously, got {value}" @@ -245,7 +246,8 @@ def get_value(self) -> complex: # type: ignore [override] def set_value( self, - value: complex | float | int | str | Sequence[float | int] | np.ndarray = 0 + 0j, + value: complex | float | int | str | Sequence[float | int] | np.ndarray = 0 + + 0j, mode: str = "rectangular", # "rectangular" or "polar" angle_unit: str = "radians", # "radians" or "degrees" — only used when mode="polar" ) -> Self: @@ -325,3 +327,75 @@ def set_value( self.points[0, :2] = (x, y) return self + + +class ThreeDValueTracker(ValueTracker): + """ + A ValueTracker that tracks 3 numeric values simultaneously, equivalent to using 3 ValueTrackers at once. + Useful when working in 3D Scenes. + Accepts list, tuple, ndarray, int/float or numpy integer/floating as input. + + Arrays of length < 3 are zero-padded on the right. Example: + If only 1 number is provided, it is stored as [float(number), 0., 0.]. + If only 2 numbers are provided, it is stored as [float(number1), float(number2), 0.]. + + Arrays of length > 3 raise a ValueError. + + Example of values + -------- + tracker = ThreeDValueTracker([1, 2, 3]) # OK + tracker = ThreeDValueTracker([1, 2]) # stored as [1., 2., 0.] + tracker = ThreeDValueTracker(5) # stored as [5., 0., 0.] + + class testThreeDValueTracker(ThreeDScene): + def construct(self): + self.set_camera_orientation(**self.default_angled_camera_orientation_kwargs) + axes = ThreeDAxes().add_coordinates() + for axis,color in zip(axes.get_axes(),[RED, GREEN, BLUE]): + axis.set_color(color) + self.add(axes) + x = ThreeDValueTracker([-3,0,0]) + t = Sphere(radius = 0.1).set_color(GOLD) + t.move_to(axes.c2p(x.get_value())) + self.add(t) + self.begin_ambient_camera_rotation(rate=2) + self.wait(2) + t.add_updater(lambda m: m.move_to(axes.c2p(x.get_value()))) + self.play(x.animate(run_time = 2).set_value([0,3,4])) + self.wait() + self.play(x.animate(run_time = 2).set_value([-2,0,-4])) + self.wait() + """ + + def _validate(self, value: list | tuple | np.ndarray | int | float) -> np.ndarray: + """ + Converts input to a float numpy array of shape (3,). + + Accepts: + - int, float, np.integer, np.floating → [value, 0., 0.] + - list, tuple, ndarray of length <= 3 → zero padded if length < 3 + + Raises: + ValueError: if input is non-numeric or length > 3 + """ + if isinstance(value, (int, float, np.integer, np.floating)): + return np.array([float(value), 0.0, 0.0]) + try: + value = np.asarray(value, dtype=float).flatten() + except (TypeError, ValueError): + raise ValueError( + "Value must be numeric — list, tuple, ndarray, int, or float" + ) + if len(value) > 3: + raise ValueError(f"Expected length at most 3, got length {len(value)}") + value = np.pad(value, (0, 3 - len(value))) if len(value) < 3 else value + return value + + def get_value(self) -> np.ndarray: + """Returns a copy of the current value.""" + return self.points[0, :3].copy() + + def set_value(self, value: list | tuple | np.ndarray | int | float) -> Self: + """Sets a new 3D vector value to the tracker.""" + self.points[0, :3] = self._validate(value) + return self From e44595aadce09e6d40cdea042956f76a4956b4d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:41:53 +0000 Subject: [PATCH 7/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/value_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 775387773f..d6fe16a348 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -5,7 +5,7 @@ __all__ = ["ValueTracker", "ComplexValueTracker", "ThreeDValueTracker"] from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np @@ -316,7 +316,7 @@ def set_value( x, y = a, b else: - value = cast(Union[complex, float, int, str], value) + value = cast(complex | float | int | str, value) z = complex(value) # handles complex, float, int, valid strings # check real and imag parts individually for finiteness if not np.isfinite(z.real): From 94f35695fba854f4cdea4a6d0073f26d7d20944b Mon Sep 17 00:00:00 2001 From: GoThrones Date: Fri, 6 Mar 2026 17:19:58 +0530 Subject: [PATCH 8/9] Fix B904: chain exception with from err in _validate --- manim/mobject/value_tracker.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 775387773f..4bf1349375 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -354,16 +354,18 @@ def construct(self): for axis,color in zip(axes.get_axes(),[RED, GREEN, BLUE]): axis.set_color(color) self.add(axes) - x = ThreeDValueTracker([-3,0,0]) - t = Sphere(radius = 0.1).set_color(GOLD) - t.move_to(axes.c2p(x.get_value())) - self.add(t) - self.begin_ambient_camera_rotation(rate=2) + position = ThreeDValueTracker([-3,0,0]) + s = Sphere(radius = 0.1).set_color(GOLD) + s.move_to(axes.c2p(position.get_value())) + self.add(s) + self.begin_ambient_camera_rotation(rate=1.5) self.wait(2) - t.add_updater(lambda m: m.move_to(axes.c2p(x.get_value()))) - self.play(x.animate(run_time = 2).set_value([0,3,4])) + s.add_updater(lambda m: m.move_to(axes.c2p(position.get_value()))) + self.play(position.animate(run_time = 2).set_value([0,3,4])) + self.wait() + self.play(position.animate(run_time = 2).set_value([-2,0,-4])) self.wait() - self.play(x.animate(run_time = 2).set_value([-2,0,-4])) + self.play(position.animate(run_time = 2).set_value([2,0,0])) self.wait() """ @@ -382,10 +384,10 @@ def _validate(self, value: list | tuple | np.ndarray | int | float) -> np.ndarra return np.array([float(value), 0.0, 0.0]) try: value = np.asarray(value, dtype=float).flatten() - except (TypeError, ValueError): + except (TypeError, ValueError) as err: raise ValueError( "Value must be numeric — list, tuple, ndarray, int, or float" - ) + ) from err if len(value) > 3: raise ValueError(f"Expected length at most 3, got length {len(value)}") value = np.pad(value, (0, 3 - len(value))) if len(value) < 3 else value From d0b5ae398e171d4a3560268510685156ac048dd6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:53:39 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/value_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 10846c0733..3d690c19ab 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -362,7 +362,7 @@ def construct(self): self.wait(2) s.add_updater(lambda m: m.move_to(axes.c2p(position.get_value()))) self.play(position.animate(run_time = 2).set_value([0,3,4])) - self.wait() + self.wait() self.play(position.animate(run_time = 2).set_value([-2,0,-4])) self.wait() self.play(position.animate(run_time = 2).set_value([2,0,0]))