Source code for manim.mobject.graphing.functions

"""Mobjects representing function graphs."""

from __future__ import annotations

__all__ = ["ParametricFunction", "FunctionGraph", "ImplicitFunction"]


from typing import Callable, Iterable, Sequence

import numpy as np
from isosurfaces import plot_isoline

from manim import config
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.color import YELLOW


[docs]class ParametricFunction(VMobject, metaclass=ConvertToOpenGL): """A parametric curve. Parameters ---------- function The function to be plotted in the form of ``(lambda x: x**2)`` t_range Determines the length that the function spans. By default ``[0, 1]`` scaling Scaling class applied to the points of the function. Default of :class:`~.LinearBase`. use_smoothing Whether to interpolate between the points of the function after they have been created. (Will have odd behaviour with a low number of points) use_vectorized Whether to pass in the generated t value array to the function as ``[t_0, t_1, ...]``. Only use this if your function supports it. Output should be a numpy array of shape ``[[x_0, x_1, ...], [y_0, y_1, ...], [z_0, z_1, ...]]`` but ``z`` can also be 0 if the Axes is 2D discontinuities Values of t at which the function experiences discontinuity. dt The left and right tolerance for the discontinuities. Examples -------- .. manim:: PlotParametricFunction :save_last_frame: class PlotParametricFunction(Scene): def func(self, t): return np.array((np.sin(2 * t), np.sin(3 * t), 0)) def construct(self): func = ParametricFunction(self.func, t_range = np.array([0, TAU]), fill_opacity=0).set_color(RED) self.add(func.scale(3)) .. manim:: ThreeDParametricSpring :save_last_frame: class ThreeDParametricSpring(ThreeDScene): def construct(self): curve1 = ParametricFunction( lambda u: np.array([ 1.2 * np.cos(u), 1.2 * np.sin(u), u * 0.05 ]), color=RED, t_range = np.array([-3*TAU, 5*TAU, 0.01]) ).set_shade_in_3d(True) axes = ThreeDAxes() self.add(axes, curve1) self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) self.wait() .. attention:: If your function has discontinuities, you'll have to specify the location of the discontinuities manually. See the following example for guidance. .. manim:: DiscontinuousExample :save_last_frame: class DiscontinuousExample(Scene): def construct(self): ax1 = NumberPlane((-3, 3), (-4, 4)) ax2 = NumberPlane((-3, 3), (-4, 4)) VGroup(ax1, ax2).arrange() discontinuous_function = lambda x: (x ** 2 - 2) / (x ** 2 - 4) incorrect = ax1.plot(discontinuous_function, color=RED) correct = ax2.plot( discontinuous_function, discontinuities=[-2, 2], # discontinuous points dt=0.1, # left and right tolerance of discontinuity color=GREEN, ) self.add(ax1, ax2, incorrect, correct) """ def __init__( self, function: Callable[[float, float], float], t_range: Sequence[float] | None = None, scaling: _ScaleBase = LinearBase(), dt: float = 1e-8, discontinuities: Iterable[float] | None = None, use_smoothing: bool = True, use_vectorized: bool = False, **kwargs, ): self.function = function t_range = [0, 1, 0.01] if t_range is None else t_range if len(t_range) == 2: t_range = np.array([*t_range, 0.01]) self.scaling = scaling self.dt = dt self.discontinuities = discontinuities self.use_smoothing = use_smoothing self.use_vectorized = use_vectorized self.t_min, self.t_max, self.t_step = t_range super().__init__(**kwargs) def get_function(self): return self.function def get_point_from_function(self, t): return self.function(t)
[docs] def generate_points(self): if self.discontinuities is not None: discontinuities = filter( lambda t: self.t_min <= t <= self.t_max, self.discontinuities, ) discontinuities = np.array(list(discontinuities)) boundary_times = np.array( [ self.t_min, self.t_max, *(discontinuities - self.dt), *(discontinuities + self.dt), ], ) boundary_times.sort() else: boundary_times = [self.t_min, self.t_max] for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2]): t_range = np.array( [ *self.scaling.function(np.arange(t1, t2, self.t_step)), self.scaling.function(t2), ], ) if self.use_vectorized: x, y, z = self.function(t_range) if not isinstance(z, np.ndarray): z = np.zeros_like(x) points = np.stack([x, y, z], axis=1) else: points = np.array([self.function(t) for t in t_range]) self.start_new_path(points[0]) self.add_points_as_corners(points[1:]) if self.use_smoothing: # TODO: not in line with upstream, approx_smooth does not exist self.make_smooth() return self
init_points = generate_points
[docs]class FunctionGraph(ParametricFunction): """A :class:`ParametricFunction` that spans the length of the scene by default. Examples -------- .. manim:: ExampleFunctionGraph :save_last_frame: class ExampleFunctionGraph(Scene): def construct(self): cos_func = FunctionGraph( lambda t: np.cos(t) + 0.5 * np.cos(7 * t) + (1 / 7) * np.cos(14 * t), color=RED, ) sin_func_1 = FunctionGraph( lambda t: np.sin(t) + 0.5 * np.sin(7 * t) + (1 / 7) * np.sin(14 * t), color=BLUE, ) sin_func_2 = FunctionGraph( lambda t: np.sin(t) + 0.5 * np.sin(7 * t) + (1 / 7) * np.sin(14 * t), x_range=[-4, 4], color=GREEN, ).move_to([0, 1, 0]) self.add(cos_func, sin_func_1, sin_func_2) """ def __init__(self, function, x_range=None, color=YELLOW, **kwargs): if x_range is None: x_range = np.array([-config["frame_x_radius"], config["frame_x_radius"]]) self.x_range = x_range self.parametric_function = lambda t: np.array([t, function(t), 0]) self.function = function super().__init__(self.parametric_function, self.x_range, color=color, **kwargs) def get_function(self): return self.function def get_point_from_function(self, x): return self.parametric_function(x)
[docs]class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL): def __init__( self, func: Callable[[float, float], float], x_range: Sequence[float] | None = None, y_range: Sequence[float] | None = None, min_depth: int = 5, max_quads: int = 1500, use_smoothing: bool = True, **kwargs, ): """An implicit function. Parameters ---------- func The implicit function in the form ``f(x, y) = 0``. x_range The x min and max of the function. y_range The y min and max of the function. min_depth The minimum depth of the function to calculate. max_quads The maximum number of quads to use. use_smoothing Whether or not to smoothen the curves. kwargs Additional parameters to pass into :class:`VMobject` .. note:: A small ``min_depth`` :math:`d` means that some small details might be ignored if they don't cross an edge of one of the :math:`4^d` uniform quads. The value of ``max_quads`` strongly corresponds to the quality of the curve, but a higher number of quads may take longer to render. Examples -------- .. manim:: ImplicitFunctionExample :save_last_frame: class ImplicitFunctionExample(Scene): def construct(self): graph = ImplicitFunction( lambda x, y: x * y ** 2 - x ** 2 * y - 2, color=YELLOW ) self.add(NumberPlane(), graph) """ self.function = func self.min_depth = min_depth self.max_quads = max_quads self.use_smoothing = use_smoothing self.x_range = x_range or [ -config.frame_width / 2, config.frame_width / 2, ] self.y_range = y_range or [ -config.frame_height / 2, config.frame_height / 2, ] super().__init__(**kwargs)
[docs] def generate_points(self): p_min, p_max = ( np.array([self.x_range[0], self.y_range[0]]), np.array([self.x_range[1], self.y_range[1]]), ) curves = plot_isoline( fn=lambda u: self.function(u[0], u[1]), pmin=p_min, pmax=p_max, min_depth=self.min_depth, max_quads=self.max_quads, ) # returns a list of lists of 2D points curves = [ np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != [] ] # add z coord as 0 for curve in curves: self.start_new_path(curve[0]) self.add_points_as_corners(curve[1:]) if self.use_smoothing: self.make_smooth() return self
init_points = generate_points