Manimating the Lorenz Attractor

An introduction rendering the Lorenz Attractor in Manim

Yash
Quantaphy
4 min readAug 22, 2023

--

Image by author.

Shout out 3Blue1Brown. This will be a short article. I will break the code down and do an ELI5 of it all. Let’s get straight to it.

from manim import *
import numpy as np
import os

These lines import the required modules. manim is the Manim library used for creating animations. numpy (imported as np) is used for numerical operations. os is used for interacting with the operating system.

class LorenzAttractor(ThreeDScene):
def construct(self):
self.create_lorenz_attractor()

This block defines a new scene class named LorenzAttractor that inherits from ThreeDScene. Inside this class, there's a method called construct that is executed when the scene is rendered. It calls the create_lorenz_attractor() method to create the Lorenz Attractor animation.

def lorenz_system(self, pos, sigma=10, rho=28, beta=8/3):
x, y, z = pos
dx_dt = sigma * (y - x)
dy_dt = x * (rho - z) - y
dz_dt = x * y - beta * z
return np.array([dx_dt, dy_dt, dz_dt])

This function, lorenz_system, calculates the derivatives of the Lorenz system equations based on the current position pos and the Lorenz parameters (sigma, rho, beta). It returns a NumPy array containing the derivatives for each coordinate. A side note: for those of you that don’t know, an array is a collection of similar data elements stored at contiguous memory locations. It’s a set of information.

def rate_to_color(self, rate, min_rate, max_rate):
epsilon = 1e-6
rate_log = np.log(rate + epsilon)
min_rate_log = np.log(min_rate + epsilon)
max_rate_log = np.log(max_rate + epsilon)
rate_normalized = (rate_log - min_rate_log) / (max_rate_log - min_rate_log)
return interpolate_color(BLUE, PURPLE, rate_normalized)

The rate_to_color function maps a rate of change to a color gradient between BLUE and PURPLE. This is what will make our Manimation a solid ten. The function calculates the normalized rate of change and uses the interpolate_color function from Manim to generate a color based on this normalized rate. The epsilon in the code is a very small positive value added to prevent issues when taking the logarithm of small or zero values. In the context of the rate_to_color method, the np.log function is used to calculate the logarithm of the rate value. However, the logarithm function is not defined for zero or negative values. Adding a small positive value like epsilon ensures that the value passed to the logarithm function is positive, preventing potential errors or infinite results.

def create_lorenz_attractor(self):
pos = np.array([-1.0, 1.0, 0.0])
dt = 0.001
steps = 40000
scale_factor = 0.1
dt_scaling_factor = 0.4

In this method, initial values and parameters are set for creating the Lorenz attractor animation. pos represents the initial position in 3D space. dt is the time step, steps is the number of steps for the animation, scale_factor controls the scale of the curve, and dt_scaling_factor is used for adjusting the time step dynamically. A lot dt_scaling_factorwill provide a more accurate curve in fast-moving regions but may make the entire Manimation too slow. The current parameters still take an awfully long time to render. Reduce dt and steps to have a lower render time.

curve = ParametricFunction(
lambda t: pos,
t_range=[], # FIX ME! Add arguments here.
)

Here, a ParametricFunction object called curve is created. This will represent the trajectory of the Lorenz attractor. The lambda function defines how the position changes over time t. t_range specifies the time range where the first entry is the start time, the second one is the end time, and the third one is the interval value.

min_rate = float("inf")
max_rate = float("-inf")

for i in range(steps):
dp = self.lorenz_system(pos) * dt
rate = np.linalg.norm(dp)
min_rate = min(min_rate, rate)
max_rate = max(max_rate, rate)
pos += dp

In this loop, the minimum and maximum rates of change are calculated by iterating through the steps. The position is updated using the Lorenz system and the time step. The rate of change is computed using the Euclidean norm of the derivative. The minimum and maximum rates are updated based on the current rate value.

curve.scale(scale_factor)
curve.move_to(ORIGIN)

self.set_camera_orientation(phi=80 * DEGREES, theta= 45 * DEGREES)
self.begin_ambient_camera_rotation(rate=0.8)

These lines adjust the scale and position of the curve and set the camera orientation for the animation. The begin_ambient_camera_rotation function is called to start the camera rotation with a specified rate.

lorenz_equation = MathTex(r"\begin{cases} \dot{x} = \sigma(y - x) \\ \dot{y} = x(\rho - z) - y \\ \dot{z} = xy - \beta z \end{cases}").scale(0.8)
lorenz_equation.to_edge(UL)
self.add_fixed_in_frame_mobjects(lorenz_equation)

Here, a mathematical equation representing the Lorenz system is created using MathTex. The equation is scaled and positioned at the upper left corner of the scene using to_edge and add_fixed_in_frame_mobjects.

self.play(Write(lorenz_equation), run_time=2)
self.play(Create(curve), run_time=20, rate_func=linear)
self.wait()

These lines initiate the animation sequence. First, the Lorenz equation is written on the screen with a run time of 2 seconds. Then, the curve is created with a run time of 20 seconds and a linear rate function. Finally, the animation waits until it's completed. And below is the complete rendering.

Thank you for reading! And as always, have a great day.

--

--

Yash
Quantaphy

A high schooler | Top Writer in Space, Science, and Education