3b1b-manim/old_projects/div_curl.py
2019-05-02 20:36:14 -07:00

4616 lines
136 KiB
Python

from manimlib.imports import *
# Quick note to anyone coming to this file with the
# intent of recreating animations from the video. Some
# of these, especially those involving AnimatedStreamLines,
# can take an extremely long time to run, but much of the
# computational cost is just for giving subtle little effects
# which don't matter too much. Switching the line_anim_class
# to ShowPassingFlash will give significant speedups, as will
# increasing the values of delta_x and delta_y in sampling for
# the stream lines. Certainly while developing, things were not
# run at production quality.
FOX_COLOR = "#DF7F20"
RABBIT_COLOR = "#C6D6EF"
# Warning, this file uses ContinualChangingDecimal,
# which has since been been deprecated. Use a mobject
# updater instead
# Helper functions
def joukowsky_map(z):
if z == 0:
return 0
return z + fdiv(1, z)
def inverse_joukowsky_map(w):
u = 1 if w.real >= 0 else -1
return (w + u * np.sqrt(w**2 - 4)) / 2
def derivative(func, dt=1e-7):
return lambda z: (func(z + dt) - func(z)) / dt
def negative_gradient(potential_func, dt=1e-7):
def result(p):
output = potential_func(p)
dx = dt * RIGHT
dy = dt * UP
dz = dt * OUT
return -np.array([
(potential_func(p + dx) - output) / dt,
(potential_func(p + dy) - output) / dt,
(potential_func(p + dz) - output) / dt,
])
return result
def divergence(vector_func, dt=1e-7):
def result(point):
value = vector_func(point)
return sum([
(vector_func(point + dt * RIGHT) - value)[i] / dt
for i, vect in enumerate([RIGHT, UP, OUT])
])
return result
def two_d_curl(vector_func, dt=1e-7):
def result(point):
value = vector_func(point)
return op.add(
(vector_func(point + dt * RIGHT) - value)[1] / dt,
-(vector_func(point + dt * UP) - value)[0] / dt,
)
return result
def cylinder_flow_vector_field(point, R=1, U=1):
z = R3_to_complex(point)
# return complex_to_R3(1.0 / derivative(joukowsky_map)(z))
return complex_to_R3(derivative(joukowsky_map)(z).conjugate())
def cylinder_flow_magnitude_field(point):
return get_norm(cylinder_flow_vector_field(point))
def vec_tex(s):
return "\\vec{\\textbf{%s}}" % s
def four_swirls_function(point):
x, y = point[:2]
result = (y**3 - 4 * y) * RIGHT + (x**3 - 16 * x) * UP
result *= 0.05
norm = get_norm(result)
if norm == 0:
return result
# result *= 2 * sigmoid(norm) / norm
return result
def get_force_field_func(*point_strength_pairs, **kwargs):
radius = kwargs.get("radius", 0.5)
def func(point):
result = np.array(ORIGIN)
for center, strength in point_strength_pairs:
to_center = center - point
norm = get_norm(to_center)
if norm == 0:
continue
elif norm < radius:
to_center /= radius**3
elif norm >= radius:
to_center /= norm**3
to_center *= -strength
result += to_center
return result
return func
def get_charged_particles(color, sign, radius=0.1):
result = Circle(
stroke_color=WHITE,
stroke_width=0.5,
fill_color=color,
fill_opacity=0.8,
radius=radius
)
sign = TexMobject(sign)
sign.set_stroke(WHITE, 1)
sign.set_width(0.5 * result.get_width())
sign.move_to(result)
result.add(sign)
return result
def get_proton(radius=0.1):
return get_charged_particles(RED, "+", radius)
def get_electron(radius=0.05):
return get_charged_particles(BLUE, "-", radius)
def preditor_prey_vector_field(point):
alpha = 30.0
beta = 1.0
gamma = 30.0
delta = 1.0
x, y = point[:2]
result = 0.05 * np.array([
alpha * x - beta * x * y,
delta * x * y - gamma * y,
0,
])
return rotate(result, 1 * DEGREES)
# Mobjects
# TODO, this is untested after turning it from a
# ContinualAnimation into a VGroup
class JigglingSubmobjects(VGroup):
CONFIG = {
"amplitude": 0.05,
"jiggles_per_second": 1,
}
def __init__(self, group, **kwargs):
VGroup.__init__(self, **kwargs)
for submob in group.submobjects:
submob.jiggling_direction = rotate_vector(
RIGHT, np.random.random() * TAU,
)
submob.jiggling_phase = np.random.random() * TAU
self.add(submob)
self.add_updater(lambda m, dt: m.update(dt))
def update(self, dt):
for submob in self.submobjects:
submob.jiggling_phase += dt * self.jiggles_per_second * TAU
submob.shift(
self.amplitude *
submob.jiggling_direction *
np.sin(submob.jiggling_phase) * dt
)
# Scenes
class Introduction(MovingCameraScene):
CONFIG = {
"stream_lines_config": {
"start_points_generator_config": {
"delta_x": 1.0 / 8,
"delta_y": 1.0 / 8,
"y_min": -8.5,
"y_max": 8.5,
}
},
"vector_field_config": {},
"virtual_time": 3,
}
def construct(self):
# Divergence
def div_func(p):
return p / 3
div_vector_field = VectorField(
div_func, **self.vector_field_config
)
stream_lines = StreamLines(
div_func, **self.stream_lines_config
)
stream_lines.shuffle()
div_title = self.get_title("Divergence")
self.add(div_vector_field)
self.play(
LaggedStartMap(ShowPassingFlash, stream_lines),
FadeIn(div_title[0]),
*list(map(GrowFromCenter, div_title[1]))
)
# Curl
def curl_func(p):
return rotate_vector(p / 3, 90 * DEGREES)
curl_vector_field = VectorField(
curl_func, **self.vector_field_config
)
stream_lines = StreamLines(
curl_func, **self.stream_lines_config
)
stream_lines.shuffle()
curl_title = self.get_title("Curl")
self.play(
ReplacementTransform(div_vector_field, curl_vector_field),
ReplacementTransform(
div_title, curl_title,
path_arc=90 * DEGREES
),
)
self.play(ShowPassingFlash(stream_lines, run_time=3))
self.wait()
def get_title(self, word):
title = TextMobject(word)
title.scale(2)
title.to_edge(UP)
title.add_background_rectangle()
return title
class ShowWritingTrajectory(TeacherStudentsScene):
def construct(self):
self.add_screen()
self.show_meandering_path()
self.show_previous_video()
def add_screen(self):
self.screen.scale(1.4, about_edge=UL)
self.add(self.screen)
def show_meandering_path(self):
solid_path = VMobject().set_points_smoothly([
3 * UP + 2 * RIGHT,
2 * UP + 4 * RIGHT,
3.5 * UP + 3.5 * RIGHT,
2 * UP + 3 * RIGHT,
3 * UP + 7 * RIGHT,
3 * UP + 5 * RIGHT,
UP + 6 * RIGHT,
2 * RIGHT,
2 * UP + 2 * RIGHT,
self.screen.get_right()
])
step = 1.0 / 80
dashed_path = VGroup(*[
VMobject().pointwise_become_partial(
solid_path, x, x + step / 2
)
for x in np.arange(0, 1 + step, step)
])
arrow = Arrow(
solid_path.points[-2],
solid_path.points[-1],
buff=0
)
dashed_path.add(arrow.tip)
dashed_path.set_color_by_gradient(BLUE, YELLOW)
self.play(
ShowCreation(
dashed_path,
rate_func=bezier([0, 0, 1, 1]),
run_time=5,
),
self.teacher.change, "raise_right_hand",
self.get_student_changes(*["sassy"] * 3)
)
self.play(
LaggedStartMap(
ApplyMethod, dashed_path,
lambda m: (m.scale, 0),
remover=True
),
self.teacher.change, "tease",
self.get_student_changes(
*["pondering"] * 3,
look_at_arg=self.screen
)
)
def show_previous_video(self):
screen = self.screen
arrow = Vector(LEFT, color=WHITE)
arrow.next_to(screen, RIGHT)
prev_words = TextMobject("Previous video")
prev_words.next_to(arrow, RIGHT)
screen.generate_target()
screen.target.set_height(3.75)
screen.target.to_corner(UR)
complex_words = TextMobject("Complex derivatives")
complex_words.next_to(
screen.target, LEFT,
buff=2 * SMALL_BUFF + arrow.get_length()
)
self.play(
GrowArrow(arrow),
Write(prev_words)
)
self.wait(3)
self.play(
arrow.flip,
arrow.next_to, screen.target, LEFT, SMALL_BUFF,
MoveToTarget(screen),
FadeOut(prev_words),
Write(complex_words),
self.teacher.change, "raise_right_hand",
path_arc=30 * DEGREES
)
self.change_student_modes("erm", "sassy", "confused")
self.look_at(screen)
self.wait(2)
self.change_student_modes("pondering", "confused", "sassy")
self.wait(2)
bubble = self.teacher.get_bubble(
bubble_class=SpeechBubble,
height=3, width=5
)
complex_words.generate_target()
complex_words.target.move_to(bubble.get_bubble_center())
# self.play(
# FadeOut(screen),
# FadeOut(arrow),
# ShowCreation(bubble),
# self.teacher.change, "hooray",
# MoveToTarget(complex_words),
# )
s0 = self.students[0]
s0.target_center = s0.get_center()
def update_s0(s0, dt):
s0.target_center += dt * LEFT * 0.5
s0.move_to(s0.target_center)
self.add(Mobject.add_updater(s0, update_s0))
self.change_student_modes("tired", "horrified", "sad")
self.play(s0.look, LEFT)
self.wait(4)
class TestVectorField(Scene):
CONFIG = {
"func": cylinder_flow_vector_field,
"flow_time": 15,
}
def construct(self):
lines = StreamLines(
four_swirls_function,
virtual_time=3,
min_magnitude=0,
max_magnitude=2,
)
self.add(AnimatedStreamLines(
lines,
line_anim_class=ShowPassingFlash
))
self.wait(10)
class CylinderModel(Scene):
CONFIG = {
"production_quality_flow": True,
"vector_field_func": cylinder_flow_vector_field,
}
def construct(self):
self.add_plane()
self.add_title()
self.show_numbers()
self.show_contour_lines()
self.show_flow()
self.apply_joukowsky_map()
def add_plane(self):
self.plane = ComplexPlane()
self.plane.add_coordinates()
self.plane.coordinate_labels.submobjects.pop(-1)
self.add(self.plane)
def add_title(self):
title = TextMobject("Complex Plane")
title.to_edge(UP, buff=MED_SMALL_BUFF)
title.add_background_rectangle()
self.title = title
self.add(title)
def show_numbers(self):
run_time = 5
unit_circle = self.unit_circle = Circle(
radius=self.plane.unit_size,
fill_color=BLACK,
fill_opacity=0,
stroke_color=YELLOW
)
dot = Dot()
dot_update = UpdateFromFunc(
dot, lambda d: d.move_to(unit_circle.point_from_proportion(1))
)
exp_tex = TexMobject("e^{", "0.00", "i}")
zero = exp_tex.get_part_by_tex("0.00")
zero.fade(1)
exp_tex_update = UpdateFromFunc(
exp_tex, lambda et: et.next_to(dot, UR, SMALL_BUFF)
)
exp_decimal = DecimalNumber(
0, num_decimal_places=2,
include_background_rectangle=True,
color=YELLOW
)
exp_decimal.replace(zero)
exp_decimal_update = ChangeDecimalToValue(
exp_decimal, TAU,
position_update_func=lambda mob: mob.move_to(zero),
run_time=run_time,
)
sample_numbers = [
complex(-5, 2),
complex(2, 2),
complex(3, 1),
complex(-5, -2),
complex(-4, 1),
]
sample_labels = VGroup()
for z in sample_numbers:
sample_dot = Dot(self.plane.number_to_point(z))
sample_label = DecimalNumber(
z,
num_decimal_places=0,
include_background_rectangle=True,
)
sample_label.next_to(sample_dot, UR, SMALL_BUFF)
sample_labels.add(VGroup(sample_dot, sample_label))
self.play(
ShowCreation(unit_circle, run_time=run_time),
VFadeIn(exp_tex),
UpdateFromAlphaFunc(
exp_decimal,
lambda ed, a: ed.set_fill(opacity=a)
),
dot_update,
exp_tex_update,
exp_decimal_update,
LaggedStartMap(
FadeIn, sample_labels,
remover=True,
rate_func=there_and_back,
run_time=run_time,
)
)
self.play(
FadeOut(exp_tex),
FadeOut(exp_decimal),
FadeOut(dot),
unit_circle.set_fill, BLACK, {"opacity": 1},
)
self.wait()
def show_contour_lines(self):
warped_grid = self.warped_grid = self.get_warpable_grid()
h_line = Line(3 * LEFT, 3 * RIGHT, color=WHITE) # Hack
func_label = self.get_func_label()
self.remove(self.plane)
self.add_foreground_mobjects(self.unit_circle, self.title)
self.play(
warped_grid.apply_complex_function, inverse_joukowsky_map,
Animation(h_line, remover=True)
)
self.play(Write(func_label))
self.add_foreground_mobjects(func_label)
self.wait()
def show_flow(self):
stream_lines = self.get_stream_lines()
stream_lines_copy = stream_lines.copy()
stream_lines_copy.set_stroke(YELLOW, 1)
stream_lines_animation = self.get_stream_lines_animation(
stream_lines
)
tiny_buff = 0.0001
v_lines = VGroup(*[
Line(
UP, ORIGIN,
path_arc=0,
n_arc_anchors=20,
).shift(x * RIGHT)
for x in np.linspace(0, 1, 5)
])
v_lines.match_background_image_file(stream_lines)
fast_lines, slow_lines = [
VGroup(*[
v_lines.copy().next_to(point, vect, tiny_buff)
for point, vect in it.product(h_points, [UP, DOWN])
])
for h_points in [
[0.5 * LEFT, 0.5 * RIGHT],
[2 * LEFT, 2 * RIGHT],
]
]
for lines in fast_lines, slow_lines:
lines.apply_complex_function(inverse_joukowsky_map)
self.add(stream_lines_animation)
self.wait(7)
self.play(
ShowCreationThenDestruction(
stream_lines_copy,
lag_ratio=0,
run_time=3,
)
)
self.wait()
self.play(ShowCreation(fast_lines))
self.wait(2)
self.play(ReplacementTransform(fast_lines, slow_lines))
self.wait(3)
self.play(
FadeOut(slow_lines),
VFadeOut(stream_lines_animation.mobject)
)
self.remove(stream_lines_animation)
def apply_joukowsky_map(self):
shift_val = 0.1 * LEFT + 0.2 * UP
scale_factor = get_norm(RIGHT - shift_val)
movers = VGroup(self.warped_grid, self.unit_circle)
self.unit_circle.insert_n_curves(50)
stream_lines = self.get_stream_lines()
stream_lines.scale(scale_factor)
stream_lines.shift(shift_val)
stream_lines.apply_complex_function(joukowsky_map)
self.play(
movers.scale, scale_factor,
movers.shift, shift_val,
)
self.wait()
self.play(
movers.apply_complex_function, joukowsky_map,
ShowCreationThenFadeAround(self.func_label),
run_time=2
)
self.add(self.get_stream_lines_animation(stream_lines))
self.wait(20)
# Helpers
def get_func_label(self):
func_label = self.func_label = TexMobject("f(z) = z + 1 / z")
func_label.add_background_rectangle()
func_label.next_to(self.title, DOWN, MED_SMALL_BUFF)
return func_label
def get_warpable_grid(self):
top_grid = NumberPlane()
top_grid.prepare_for_nonlinear_transform()
bottom_grid = top_grid.copy()
tiny_buff = 0.0001
top_grid.next_to(ORIGIN, UP, buff=tiny_buff)
bottom_grid.next_to(ORIGIN, DOWN, buff=tiny_buff)
result = VGroup(top_grid, bottom_grid)
result.add(*[
Line(
ORIGIN, FRAME_WIDTH * RIGHT / 2,
color=WHITE,
path_arc=0,
n_arc_anchors=100,
).next_to(ORIGIN, vect, buff=2)
for vect in (LEFT, RIGHT)
])
# This line is a bit of a hack
h_line = Line(LEFT, RIGHT, color=WHITE)
h_line.set_points([LEFT, LEFT, RIGHT, RIGHT])
h_line.scale(2)
result.add(h_line)
return result
def get_stream_lines(self):
func = self.vector_field_func
if self.production_quality_flow:
delta_x = 0.5
delta_y = 0.1
else:
delta_x = 1
# delta_y = 1
delta_y = 0.1
return StreamLines(
func,
start_points_generator_config={
"x_min": -8,
"x_max": -7,
"y_min": -4,
"y_max": 4,
"delta_x": delta_x,
"delta_y": delta_y,
"n_repeats": 1,
"noise_factor": 0.1,
},
stroke_width=2,
virtual_time=15,
)
def get_stream_lines_animation(self, stream_lines):
if self.production_quality_flow:
line_anim_class = ShowPassingFlashWithThinningStrokeWidth
else:
line_anim_class = ShowPassingFlash
return AnimatedStreamLines(
stream_lines,
line_anim_class=line_anim_class,
)
class OkayNotToUnderstand(Scene):
def construct(self):
words = TextMobject(
"It's okay not to \\\\ understand this just yet."
)
morty = Mortimer()
morty.change("confused")
words.next_to(morty, UP)
self.add(morty, words)
class ThatsKindOfInteresting(TeacherStudentsScene):
def construct(self):
self.student_says(
"Cool!", target_mode="hooray",
student_index=2,
added_anims=[self.teacher.change, "happy"]
)
self.change_student_modes("happy", "happy")
self.wait(2)
class ElectricField(CylinderModel, MovingCameraScene):
def construct(self):
self.add_plane()
self.add_title()
self.setup_warped_grid()
self.show_uniform_field()
self.show_moving_charges()
self.show_field_lines()
def setup_warped_grid(self):
warped_grid = self.warped_grid = self.get_warpable_grid()
warped_grid.save_state()
func_label = self.get_func_label()
unit_circle = self.unit_circle = Circle(
radius=self.plane.unit_size,
stroke_color=YELLOW,
fill_color=BLACK,
fill_opacity=1
)
self.add_foreground_mobjects(self.title, func_label, unit_circle)
self.remove(self.plane)
self.play(
warped_grid.apply_complex_function, inverse_joukowsky_map,
)
self.wait()
def show_uniform_field(self):
vector_field = self.vector_field = VectorField(
lambda p: UP,
colors=[BLUE_E, WHITE, RED]
)
protons, electrons = groups = [
VGroup(*[method(radius=0.2) for x in range(20)])
for method in (get_proton, get_electron)
]
for group in groups:
group.arrange(RIGHT, buff=MED_SMALL_BUFF)
random.shuffle(group.submobjects)
protons.next_to(FRAME_HEIGHT * DOWN / 2, DOWN)
electrons.next_to(FRAME_HEIGHT * UP / 2, UP)
self.play(
self.warped_grid.restore,
FadeOut(self.unit_circle),
FadeOut(self.title),
FadeOut(self.func_label),
LaggedStartMap(GrowArrow, vector_field)
)
self.remove_foreground_mobjects(self.title, self.func_label)
self.wait()
for group, vect in (protons, UP), (electrons, DOWN):
self.play(LaggedStartMap(
ApplyMethod, group,
lambda m: (m.shift, (FRAME_HEIGHT + 1) * vect),
run_time=3,
rate_func=rush_into
))
def show_moving_charges(self):
unit_circle = self.unit_circle
protons = VGroup(*[
get_proton().move_to(
rotate_vector(0.275 * n * RIGHT, angle)
)
for n in range(4)
for angle in np.arange(
0, TAU, TAU / (6 * n) if n > 0 else TAU
)
])
jiggling_protons = JigglingSubmobjects(protons)
electrons = VGroup(*[
get_electron().move_to(
proton.get_center() +
proton.radius * rotate_vector(RIGHT, angle)
)
for proton in protons
for angle in [np.random.random() * TAU]
])
jiggling_electrons = JigglingSubmobjects(electrons)
electrons.generate_target()
for electron in electrons.target:
y_part = electron.get_center()[1]
if y_part > 0:
electron.shift(2 * y_part * DOWN)
# New vector field
def new_electric_field(point):
if get_norm(point) < 1:
return ORIGIN
vect = cylinder_flow_vector_field(point)
return rotate_vector(vect, 90 * DEGREES)
new_vector_field = VectorField(
new_electric_field,
colors=self.vector_field.colors
)
warped_grid = self.warped_grid
self.play(GrowFromCenter(unit_circle))
self.add(jiggling_protons, jiggling_electrons)
self.add_foreground_mobjects(
self.vector_field, unit_circle, protons, electrons
)
self.play(
LaggedStartMap(VFadeIn, protons),
LaggedStartMap(VFadeIn, electrons),
)
self.play(
self.camera.frame.scale, 0.7,
run_time=3
)
self.play(
MoveToTarget(electrons), # More indication?
warped_grid.apply_complex_function, inverse_joukowsky_map,
Transform(
self.vector_field,
new_vector_field
),
run_time=3
)
self.wait(5)
def show_field_lines(self):
h_lines = VGroup(*[
Line(
5 * LEFT, 5 * RIGHT,
path_arc=0,
n_arc_anchors=50,
stroke_color=LIGHT_GREY,
stroke_width=2,
).shift(y * UP)
for y in np.arange(-3, 3.25, 0.25)
if y != 0
])
h_lines.apply_complex_function(inverse_joukowsky_map)
for h_line in h_lines:
h_line.save_state()
voltage = DecimalNumber(
10, num_decimal_places=1,
unit="\\, V",
color=YELLOW,
include_background_rectangle=True,
)
vp_prop = 0.1
voltage_point = VectorizedPoint(
h_lines[4].point_from_proportion(vp_prop)
)
def get_voltage(dummy_arg):
y = voltage_point.get_center()[1]
return 10 - y
voltage_update = voltage.add_updater(
lambda d: d.set_value(get_voltage),
)
voltage.add_updater(
lambda d: d.next_to(
voltage_point, UP, SMALL_BUFF
)
)
self.play(ShowCreation(
h_lines,
run_time=2,
lag_ratio=0
))
self.add(voltage_update)
self.add_foreground_mobjects(voltage)
self.play(
UpdateFromAlphaFunc(
voltage, lambda m, a: m.set_fill(opacity=a)
),
h_lines[4].set_stroke, YELLOW, 4,
)
for hl1, hl2 in zip(h_lines[4:], h_lines[5:]):
self.play(
voltage_point.move_to,
hl2.point_from_proportion(vp_prop),
hl1.restore,
hl2.set_stroke, YELLOW, 3,
run_time=0.5
)
self.wait(0.5)
class AskQuestions(TeacherStudentsScene):
def construct(self):
div_tex = TexMobject("\\nabla \\cdot", vec_tex("v"))
curl_tex = TexMobject("\\nabla \\times", vec_tex("v"))
div_name = TextMobject("Divergence")
curl_name = TextMobject("Curl")
div = VGroup(div_name, div_tex)
curl = VGroup(curl_name, curl_tex)
for group in div, curl:
group[1].set_color_by_tex(vec_tex("v"), YELLOW)
group.arrange(DOWN)
topics = VGroup(div, curl)
topics.arrange(DOWN, buff=LARGE_BUFF)
topics.move_to(self.hold_up_spot, DOWN)
div.save_state()
div.move_to(self.hold_up_spot, DOWN)
screen = self.screen
self.student_says(
"What does fluid flow have \\\\ to do with electricity?",
added_anims=[self.teacher.change, "happy"]
)
self.wait()
self.student_says(
"And you mentioned \\\\ complex numbers?",
student_index=0,
)
self.wait(3)
self.play(
FadeInFromDown(div),
self.teacher.change, "raise_right_hand",
FadeOut(self.students[0].bubble),
FadeOut(self.students[0].bubble.content),
self.get_student_changes(*["pondering"] * 3)
)
self.play(
FadeInFromDown(curl),
div.restore
)
self.wait()
self.look_at(self.screen)
self.wait()
self.change_all_student_modes("hooray", look_at_arg=screen)
self.wait(3)
topics.generate_target()
topics.target.to_edge(LEFT, buff=LARGE_BUFF)
arrow = TexMobject("\\leftrightarrow")
arrow.scale(2)
arrow.next_to(topics.target, RIGHT, buff=LARGE_BUFF)
screen.next_to(arrow, RIGHT, LARGE_BUFF)
complex_analysis = TextMobject("Complex analysis")
complex_analysis.next_to(screen, UP)
self.play(
MoveToTarget(topics),
self.get_student_changes(
"confused", "sassy", "erm",
look_at_arg=topics.target
),
self.teacher.change, "pondering", screen
)
self.play(
Write(arrow),
FadeInFromDown(complex_analysis)
)
self.look_at(screen)
self.wait(6)
class ScopeMeiosis(PiCreatureScene):
CONFIG = {
"default_pi_creature_kwargs": {
"flip_at_start": True,
"color": GREY_BROWN,
},
"default_pi_creature_start_corner": DR,
}
def construct(self):
morty = self.pi_creature
section_titles = VGroup(*list(map(TextMobject, [
"Background on div/curl",
"Conformal maps",
"Conformal map $\\Rightarrow" +
"\\text{div}\\textbf{F} = " +
"\\text{curl}\\textbf{F} = 0$",
"Complex derivatives",
])))
sections = VGroup(*[
VGroup(title, self.get_lines(title, 3))
for title in section_titles
])
sections.arrange(
DOWN, buff=MED_LARGE_BUFF,
aligned_edge=LEFT
)
sections.to_edge(UP)
top_title = section_titles[0]
lower_sections = sections[1:]
self.add(sections)
modes = [
"pondering",
"pondering",
"bump",
"bump",
"concerned_musician",
"concerned_musician",
]
for n, mode in zip(list(range(6)), modes):
if n % 2 == 1:
top_title = lines
lines = self.get_lines(top_title, 4)
else:
lines = self.get_lines(top_title, 6)
lower_sections.generate_target()
lower_sections.target.next_to(
lines, DOWN, MED_LARGE_BUFF, LEFT,
)
self.play(
ShowCreation(lines),
MoveToTarget(lower_sections),
morty.change, mode, lines,
)
def get_lines(self, title, n_lines):
lines = VGroup(*[
Line(3 * LEFT, 3 * RIGHT, color=LIGHT_GREY)
for x in range(n_lines)
])
lines.arrange(DOWN, buff=MED_SMALL_BUFF)
lines.next_to(
title, DOWN,
buff=MED_LARGE_BUFF,
aligned_edge=LEFT
)
lines[-1].pointwise_become_partial(
lines[-1], 0, random.random()
)
return lines
class WhyAreYouTellingUsThis(TeacherStudentsScene):
def construct(self):
self.student_says(
"Cool story bro...\\\\ how about the actual math?",
target_mode="sassy",
added_anims=[self.teacher.change, "guilty"]
)
self.change_student_modes("angry", "sassy", "angry")
self.wait(2)
class TopicsAndConnections(Scene):
CONFIG = {
"random_seed": 1,
}
def construct(self):
dots = VGroup(*[
Dot(8 * np.array([
random.random(),
random.random(),
0
]))
for n in range(5)
])
topics = VGroup(*[
TextMobject(word).next_to(
dot, RIGHT, SMALL_BUFF
)
for dot, word in zip(dots, [
"Divergence/curl",
"Fluid flow",
"Electricity and magnetism",
"Conformal maps",
"Complex numbers"
])
])
for topic in topics:
topic.add_to_back(
topic.copy().set_stroke(BLACK, 2)
)
VGroup(dots, topics).center()
for dot in dots:
dot.save_state()
dot.scale(3)
dot.set_fill(opacity=0)
connections = VGroup(*[
DashedLine(d1.get_center(), d2.get_center())
for d1, d2 in it.combinations(dots, 2)
])
connections.set_stroke(YELLOW, 2)
full_rect = FullScreenFadeRectangle()
self.play(
LaggedStartMap(
ApplyMethod, dots,
lambda d: (d.restore,)
),
LaggedStartMap(Write, topics),
)
self.wait()
self.play(
LaggedStartMap(ShowCreation, connections),
Animation(topics),
Animation(dots),
)
self.wait()
self.play(
FadeIn(full_rect),
Animation(topics[0]),
Animation(dots[0]),
)
self.wait()
class OnToTheLesson(Scene):
def construct(self):
words = TextMobject("On to the lesson!")
words.scale(1.5)
self.add(words)
self.play(FadeInFromDown(words))
self.wait()
class IntroduceVectorField(Scene):
CONFIG = {
"vector_field_config": {
# "delta_x": 2,
# "delta_y": 2,
"delta_x": 0.5,
"delta_y": 0.5,
},
"stream_line_config": {
"start_points_generator_config": {
# "delta_x": 1,
# "delta_y": 1,
"delta_x": 0.25,
"delta_y": 0.25,
},
"virtual_time": 3,
},
"stream_line_animation_config": {
# "line_anim_class": ShowPassingFlash,
"line_anim_class": ShowPassingFlashWithThinningStrokeWidth,
}
}
def construct(self):
self.add_plane()
self.add_title()
self.points_to_vectors()
self.show_fluid_flow()
self.show_gravitational_force()
self.show_magnetic_force()
self.show_fluid_flow()
def add_plane(self):
plane = self.plane = NumberPlane()
plane.add_coordinates()
plane.remove(plane.coordinate_labels[-1])
self.add(plane)
def add_title(self):
title = TextMobject("Vector field")
title.scale(1.5)
title.to_edge(UP, buff=MED_SMALL_BUFF)
title.add_background_rectangle(opacity=1, buff=SMALL_BUFF)
self.add_foreground_mobjects(title)
def points_to_vectors(self):
vector_field = self.vector_field = VectorField(
four_swirls_function,
**self.vector_field_config
)
dots = VGroup()
for vector in vector_field:
dot = Dot(radius=0.05)
dot.move_to(vector.get_start())
dot.target = vector
dots.add(dot)
self.play(LaggedStartMap(GrowFromCenter, dots))
self.wait()
self.play(LaggedStartMap(MoveToTarget, dots, remover=True))
self.add(vector_field)
self.wait()
def show_fluid_flow(self):
vector_field = self.vector_field
stream_lines = StreamLines(
vector_field.func,
**self.stream_line_config
)
stream_line_animation = AnimatedStreamLines(
stream_lines,
**self.stream_line_animation_config
)
self.add(stream_line_animation)
self.play(
vector_field.set_fill, {"opacity": 0.5}
)
self.wait(7)
self.play(
vector_field.set_fill, {"opacity": 1},
VFadeOut(stream_line_animation.mobject),
)
self.remove(stream_line_animation)
def show_gravitational_force(self):
earth = self.earth = ImageMobject("earth")
moon = self.moon = ImageMobject("moon", height=1)
earth_center = 3 * RIGHT + 2 * UP
moon_center = 3 * LEFT + DOWN
earth.move_to(earth_center)
moon.move_to(moon_center)
gravity_func = get_force_field_func((earth_center, -6), (moon_center, -1))
gravity_field = VectorField(
gravity_func,
**self.vector_field_config
)
self.add_foreground_mobjects(earth, moon)
self.play(
GrowFromCenter(earth),
GrowFromCenter(moon),
Transform(self.vector_field, gravity_field),
run_time=2
)
self.vector_field.func = gravity_field.func
self.wait()
def show_magnetic_force(self):
magnetic_func = get_force_field_func(
(3 * LEFT, -1), (3 * RIGHT, +1)
)
magnetic_field = VectorField(
magnetic_func,
**self.vector_field_config
)
magnet = VGroup(*[
Rectangle(
width=3.5,
height=1,
stroke_width=0,
fill_opacity=1,
fill_color=color
)
for color in (BLUE, RED)
])
magnet.arrange(RIGHT, buff=0)
for char, vect in ("S", LEFT), ("N", RIGHT):
letter = TextMobject(char)
edge = magnet.get_edge_center(vect)
letter.next_to(edge, -vect, buff=MED_LARGE_BUFF)
magnet.add(letter)
self.add_foreground_mobjects(magnet)
self.play(
self.earth.scale, 0,
self.moon.scale, 0,
DrawBorderThenFill(magnet),
Transform(self.vector_field, magnetic_field),
run_time=2
)
self.vector_field.func = magnetic_field.func
self.remove_foreground_mobjects(self.earth, self.moon)
class QuickNoteOnDrawingThese(TeacherStudentsScene):
def construct(self):
self.teacher_says(
"Quick note on \\\\ drawing vector fields",
bubble_kwargs={"width": 5, "height": 3},
added_anims=[self.get_student_changes(
"confused", "erm", "sassy"
)]
)
self.look_at(self.screen)
self.wait(3)
class ShorteningLongVectors(IntroduceVectorField):
def construct(self):
self.add_plane()
self.add_title()
self.contrast_adjusted_and_non_adjusted()
def contrast_adjusted_and_non_adjusted(self):
func = four_swirls_function
unadjusted = VectorField(
func, length_func=lambda n: n, colors=[WHITE],
)
adjusted = VectorField(func)
for v1, v2 in zip(adjusted, unadjusted):
v1.save_state()
v1.target = v2
self.add(adjusted)
self.wait()
self.play(LaggedStartMap(
MoveToTarget, adjusted,
run_time=3
))
self.wait()
self.play(LaggedStartMap(
ApplyMethod, adjusted,
lambda m: (m.restore,),
run_time=3
))
self.wait()
class TimeDependentVectorField(ExternallyAnimatedScene):
pass
class ChangingElectricField(Scene):
CONFIG = {
"vector_field_config": {}
}
def construct(self):
particles = self.get_particles()
vector_field = self.get_vector_field()
def update_vector_field(vector_field):
new_field = self.get_vector_field()
Transform(vector_field, new_field).update(1)
vector_field.func = new_field.func
def update_particles(particles, dt):
func = vector_field.func
for particle in particles:
force = func(particle.get_center())
particle.velocity += force * dt
particle.shift(particle.velocity * dt)
self.add(
Mobject.add_updater(vector_field, update_vector_field),
Mobject.add_updater(particles, update_particles),
)
self.wait(20)
def get_particles(self):
particles = self.particles = VGroup()
for n in range(9):
if n % 2 == 0:
particle = get_proton(radius=0.2)
particle.charge = +1
else:
particle = get_electron(radius=0.2)
particle.charge = -1
particle.velocity = np.random.normal(0, 0.1, 3)
particles.add(particle)
particle.shift(np.random.normal(0, 0.2, 3))
particles.arrange_in_grid(buff=LARGE_BUFF)
return particles
def get_vector_field(self):
func = get_force_field_func(*list(zip(
list(map(Mobject.get_center, self.particles)),
[p.charge for p in self.particles]
)))
self.vector_field = VectorField(func, **self.vector_field_config)
return self.vector_field
class InsertAirfoildTODO(TODOStub):
CONFIG = {"message": "Insert airfoil flow animation"}
class ThreeDVectorField(ExternallyAnimatedScene):
pass
class ThreeDVectorFieldEquation(Scene):
def construct(self):
vector = Matrix([
"yz",
"xz",
"xy",
])
vector.set_height(FRAME_HEIGHT - 1)
self.add(vector)
class GravityFluidFlow(IntroduceVectorField):
def construct(self):
self.vector_field = VectorField(
lambda p: np.array(ORIGIN),
**self.vector_field_config
)
self.show_gravitational_force()
self.show_fluid_flow()
class TotallyToScale(Scene):
def construct(self):
words = TextMobject(
"Totally drawn to scale. \\\\ Don't even worry about it."
)
words.set_width(FRAME_WIDTH - 1)
words.add_background_rectangle()
self.add(words)
self.wait()
# TODO: Revisit this
class FluidFlowAsHillGradient(CylinderModel, ThreeDScene):
CONFIG = {
"production_quality_flow": False,
}
def construct(self):
def potential(point):
x, y = point[:2]
result = 2 - 0.01 * op.mul(
((x - 4)**2 + y**2),
((x + 4)**2 + y**2)
)
return max(-10, result)
vector_field_func = negative_gradient(potential)
stream_lines = StreamLines(
vector_field_func,
virtual_time=3,
color_lines_by_magnitude=False,
start_points_generator_config={
"delta_x": 0.2,
"delta_y": 0.2,
}
)
for line in stream_lines:
line.points[:, 2] = np.apply_along_axis(
potential, 1, line.points
)
stream_lines_animation = self.get_stream_lines_animation(
stream_lines
)
plane = NumberPlane()
self.add(plane)
self.add(stream_lines_animation)
self.wait(3)
self.begin_ambient_camera_rotation(rate=0.1)
self.move_camera(
phi=70 * DEGREES,
run_time=2
)
self.wait(5)
class DefineDivergence(ChangingElectricField):
CONFIG = {
"vector_field_config": {
"length_func": lambda norm: 0.3,
"min_magnitude": 0,
"max_magnitude": 1,
},
"stream_line_config": {
"start_points_generator_config": {
"delta_x": 0.125,
"delta_y": 0.125,
},
"virtual_time": 5,
"n_anchors_per_line": 10,
"min_magnitude": 0,
"max_magnitude": 1,
"stroke_width": 2,
},
"stream_line_animation_config": {
"line_anim_class": ShowPassingFlash,
},
"flow_time": 10,
"random_seed": 7,
}
def construct(self):
self.draw_vector_field()
self.show_flow()
self.point_out_sources_and_sinks()
self.show_divergence_values()
def draw_vector_field(self):
particles = self.get_particles()
random.shuffle(particles.submobjects)
particles.remove(particles[0])
particles.arrange_in_grid(
n_cols=4, buff=3
)
for particle in particles:
particle.shift(
np.random.normal(0, 0.75) * RIGHT,
np.random.normal(0, 0.5) * UP,
)
particle.shift_onto_screen(buff=2 * LARGE_BUFF)
particle.charge *= 0.125
vector_field = self.get_vector_field()
self.play(
LaggedStartMap(GrowArrow, vector_field),
LaggedStartMap(GrowFromCenter, particles),
run_time=4
)
self.wait()
self.play(LaggedStartMap(FadeOut, particles))
def show_flow(self):
stream_lines = StreamLines(
self.vector_field.func,
**self.stream_line_config
)
stream_line_animation = AnimatedStreamLines(
stream_lines,
**self.stream_line_animation_config
)
self.add(stream_line_animation)
self.wait(self.flow_time)
def point_out_sources_and_sinks(self):
particles = self.particles
self.positive_points, self.negative_points = [
[
particle.get_center()
for particle in particles
if u * particle.charge > 0
]
for u in (+1, -1)
]
pair_of_vector_circle_groups = VGroup()
for point_set in self.positive_points, self.negative_points:
vector_circle_groups = VGroup()
for point in point_set:
vector_circle_group = VGroup()
for angle in np.linspace(0, TAU, 12, endpoint=False):
step = 0.5 * rotate_vector(RIGHT, angle)
vect = self.vector_field.get_vector(point + step)
vect.set_color(WHITE)
vect.set_stroke(width=2)
vector_circle_group.add(vect)
vector_circle_groups.add(vector_circle_group)
pair_of_vector_circle_groups.add(vector_circle_groups)
self.play(
self.vector_field.set_fill, {"opacity": 0.5},
LaggedStartMap(
LaggedStartMap, vector_circle_groups,
lambda vcg: (GrowArrow, vcg),
),
)
self.wait(4)
self.play(FadeOut(vector_circle_groups))
self.play(self.vector_field.set_fill, {"opacity": 1})
self.positive_vector_circle_groups = pair_of_vector_circle_groups[0]
self.negative_vector_circle_groups = pair_of_vector_circle_groups[1]
self.wait()
def show_divergence_values(self):
positive_points = self.positive_points
negative_points = self.negative_points
div_func = divergence(self.vector_field.func)
circle = Circle(color=WHITE, radius=0.2)
circle.add(Dot(circle.get_center(), radius=0.02))
circle.move_to(positive_points[0])
div_tex = TexMobject(
"\\text{div} \\, \\textbf{F}(x, y) = "
)
div_tex.add_background_rectangle()
div_tex_update = Mobject.add_updater(
div_tex, lambda m: m.next_to(circle, UP, SMALL_BUFF)
)
div_value = DecimalNumber(
0,
num_decimal_places=1,
include_background_rectangle=True,
include_sign=True,
)
div_value_update = ContinualChangingDecimal(
div_value,
lambda a: np.round(div_func(circle.get_center()), 1),
position_update_func=lambda m: m.next_to(div_tex, RIGHT, SMALL_BUFF),
include_sign=True,
)
self.play(
ShowCreation(circle),
FadeIn(div_tex),
FadeIn(div_value),
)
self.add(div_tex_update)
self.add(div_value_update)
self.wait()
for point in positive_points[1:-1]:
self.play(circle.move_to, point)
self.wait(1.5)
for point in negative_points:
self.play(circle.move_to, point)
self.wait(2)
self.wait(4)
# self.remove(div_tex_update)
# self.remove(div_value_update)
# self.play(
# ApplyMethod(circle.scale, 0, remover=True),
# FadeOut(div_tex),
# FadeOut(div_value),
# )
class DefineDivergenceJustFlow(DefineDivergence):
CONFIG = {
"flow_time": 10,
}
def construct(self):
self.force_skipping()
self.draw_vector_field()
self.revert_to_original_skipping_status()
self.clear()
self.show_flow()
class DefineDivergenceSymbols(Scene):
def construct(self):
tex_mob = TexMobject(
"\\text{div}",
"\\textbf{F}",
"(x, y)",
"=",
)
div, F, xy, eq = tex_mob
output = DecimalNumber(0, include_sign=True)
output.next_to(tex_mob, RIGHT)
time_tracker = ValueTracker()
always_shift(time_tracker, rate=1)
self.add(time_tracker)
output_animation = ContinualChangingDecimal(
output, lambda a: 3 * np.cos(int(time_tracker.get_value())),
)
F.set_color(BLUE)
xy.set_color(YELLOW)
F_brace = Brace(F, UP, buff=SMALL_BUFF)
F_label = F_brace.get_text(
"Vector field function",
)
F_label.match_color(F)
xy_brace = Brace(xy, DOWN, buff=SMALL_BUFF)
xy_label = xy_brace.get_text("Some point")
xy_label.match_color(xy)
output_brace = Brace(output, UP, buff=SMALL_BUFF)
output_label = output_brace.get_text(
"Measure of how much \\\\ "
"$(x, y)$ ``generates'' fluid"
)
brace_label_pairs = [
(F_brace, F_label),
(xy_brace, xy_label),
(output_brace, output_label),
]
self.add(tex_mob, output_animation)
fade_anims = []
for brace, label in brace_label_pairs:
self.play(
GrowFromCenter(brace),
FadeInFromDown(label),
*fade_anims
)
self.wait(2)
fade_anims = list(map(FadeOut, [brace, label]))
self.wait()
class DivergenceAtSlowFastPoint(Scene):
CONFIG = {
"vector_field_config": {
"length_func": lambda norm: 0.1 + 0.4 * norm / 4.0,
"min_magnitude": 0,
"max_magnitude": 3,
},
"stream_lines_config": {
"start_points_generator_config": {
"delta_x": 0.125,
"delta_y": 0.125,
},
"virtual_time": 1,
"min_magnitude": 0,
"max_magnitude": 3,
},
}
def construct(self):
def func(point):
return 3 * sigmoid(point[0]) * RIGHT
vector_field = self.vector_field = VectorField(
func, **self.vector_field_config
)
circle = Circle(color=WHITE)
slow_words = TextMobject("Slow flow in")
fast_words = TextMobject("Fast flow out")
words = VGroup(slow_words, fast_words)
for word, vect in zip(words, [LEFT, RIGHT]):
word.add_background_rectangle()
word.next_to(circle, vect)
div_tex = TexMobject(
"\\text{div}\\,\\textbf{F}(x, y) > 0"
)
div_tex.add_background_rectangle()
div_tex.next_to(circle, UP)
self.add(vector_field)
self.add_foreground_mobjects(circle, div_tex)
self.begin_flow()
self.wait(2)
for word in words:
self.add_foreground_mobjects(word)
self.play(Write(word))
self.wait(8)
def begin_flow(self):
stream_lines = StreamLines(
self.vector_field.func,
**self.stream_lines_config
)
stream_line_animation = AnimatedStreamLines(stream_lines)
stream_line_animation.update(3)
self.add(stream_line_animation)
class DivergenceAsNewFunction(Scene):
def construct(self):
self.add_plane()
self.show_vector_field_function()
self.show_divergence_function()
def add_plane(self):
plane = self.plane = NumberPlane()
plane.add_coordinates()
self.add(plane)
def show_vector_field_function(self):
func = self.func
unscaled_vector_field = VectorField(
func,
length_func=lambda norm: norm,
colors=[BLUE_C, YELLOW, RED],
delta_x=np.inf,
delta_y=np.inf,
)
in_dot = Dot(color=PINK)
in_dot.move_to(3.75 * LEFT + 1.25 * UP)
def get_input():
return in_dot.get_center()
def get_out_vect():
return unscaled_vector_field.get_vector(get_input())
# Tex
func_tex = TexMobject(
"\\textbf{F}(", "+0.00", ",", "+0.00", ")", "=",
)
dummy_in_x, dummy_in_y = func_tex.get_parts_by_tex("+0.00")
func_tex.add_background_rectangle()
rhs = DecimalMatrix(
[[0], [0]],
element_to_mobject_config={
"num_decimal_places": 2,
"include_sign": True,
},
include_background_rectangle=True
)
rhs.next_to(func_tex, RIGHT)
dummy_out_x, dummy_out_y = rhs.get_mob_matrix().flatten()
VGroup(func_tex, rhs).to_corner(UL, buff=MED_SMALL_BUFF)
VGroup(
dummy_in_x, dummy_in_y,
dummy_out_x, dummy_out_y,
).set_fill(BLACK, opacity=0)
# Changing decimals
in_x, in_y, out_x, out_y = [
DecimalNumber(0, include_sign=True)
for x in range(4)
]
VGroup(in_x, in_y).set_color(in_dot.get_color())
VGroup(out_x, out_y).set_color(get_out_vect().get_fill_color())
in_x_update = ContinualChangingDecimal(
in_x, lambda a: get_input()[0],
position_update_func=lambda m: m.move_to(dummy_in_x)
)
in_y_update = ContinualChangingDecimal(
in_y, lambda a: get_input()[1],
position_update_func=lambda m: m.move_to(dummy_in_y)
)
out_x_update = ContinualChangingDecimal(
out_x, lambda a: func(get_input())[0],
position_update_func=lambda m: m.move_to(dummy_out_x)
)
out_y_update = ContinualChangingDecimal(
out_y, lambda a: func(get_input())[1],
position_update_func=lambda m: m.move_to(dummy_out_y)
)
self.add(func_tex, rhs)
# self.add(Mobject.add_updater(
# rhs, lambda m: m.next_to(func_tex, RIGHT)
# ))
# Where those decimals actually change
self.add(in_x_update, in_y_update)
in_dot.save_state()
in_dot.move_to(ORIGIN)
self.play(in_dot.restore)
self.wait()
self.play(*[
ReplacementTransform(
VGroup(mob.copy().fade(1)),
VGroup(out_x, out_y),
)
for mob in (in_x, in_y)
])
out_vect = get_out_vect()
VGroup(out_x, out_y).match_style(out_vect)
out_vect.save_state()
out_vect.move_to(rhs)
out_vect.set_fill(opacity=0)
self.play(out_vect.restore)
self.out_vect_update = Mobject.add_updater(
out_vect,
lambda ov: Transform(ov, get_out_vect()).update(1)
)
self.add(self.out_vect_update)
self.add(out_x_update, out_y_update)
self.add(Mobject.add_updater(
VGroup(out_x, out_y),
lambda m: m.match_style(out_vect)
))
self.wait()
for vect in DOWN, 2 * RIGHT, UP:
self.play(
in_dot.shift, 3 * vect,
run_time=3
)
self.wait()
self.in_dot = in_dot
self.out_vect = out_vect
self.func_equation = VGroup(func_tex, rhs)
self.out_x, self.out_y = out_x, out_y
self.in_x, self.in_y = out_x, out_y
self.in_x_update = in_x_update
self.in_y_update = in_y_update
self.out_x_update = out_x_update
self.out_y_update = out_y_update
def show_divergence_function(self):
vector_field = VectorField(self.func)
vector_field.remove(*[
v for v in vector_field
if v.get_start()[0] < 0 and v.get_start()[1] > 2
])
vector_field.set_fill(opacity=0.5)
in_dot = self.in_dot
def get_neighboring_points(step_sizes=[0.3], n_angles=12):
point = in_dot.get_center()
return list(it.chain(*[
[
point + step_size * step
for step in compass_directions(n_angles)
]
for step_size in step_sizes
]))
def get_vector_ring():
return VGroup(*[
vector_field.get_vector(point)
for point in get_neighboring_points()
])
def get_stream_lines():
return StreamLines(
self.func,
start_points_generator=get_neighboring_points,
start_points_generator_config={
"step_sizes": np.arange(0.1, 0.5, 0.1)
},
virtual_time=1,
stroke_width=3,
)
def show_flow():
stream_lines = get_stream_lines()
random.shuffle(stream_lines.submobjects)
self.play(LaggedStartMap(
ShowCreationThenDestruction,
stream_lines,
remover=True
))
vector_ring = get_vector_ring()
vector_ring_update = Mobject.add_updater(
vector_ring,
lambda vr: Transform(vr, get_vector_ring()).update(1)
)
func_tex, rhs = self.func_equation
out_x, out_y = self.out_x, self.out_y
out_x_update = self.out_x_update
out_y_update = self.out_y_update
div_tex = TexMobject("\\text{div}")
div_tex.add_background_rectangle()
div_tex.move_to(func_tex, LEFT)
div_tex.shift(2 * SMALL_BUFF * RIGHT)
self.remove(out_x_update, out_y_update)
self.remove(self.out_vect_update)
self.add(self.in_x_update, self.in_y_update)
self.play(
func_tex.next_to, div_tex, RIGHT, SMALL_BUFF,
{"submobject_to_align": func_tex[1][0]},
Write(div_tex),
FadeOut(self.out_vect),
FadeOut(out_x),
FadeOut(out_y),
FadeOut(rhs),
)
# This line is a dumb hack around a Scene bug
self.add(*[
Mobject.add_updater(
mob, lambda m: m.set_fill(None, 0)
)
for mob in (out_x, out_y)
])
self.add_foreground_mobjects(div_tex)
self.play(
LaggedStartMap(GrowArrow, vector_field),
LaggedStartMap(GrowArrow, vector_ring),
)
self.add(vector_ring_update)
self.wait()
div_func = divergence(self.func)
div_rhs = DecimalNumber(
0, include_sign=True,
include_background_rectangle=True
)
div_rhs_update = ContinualChangingDecimal(
div_rhs, lambda a: div_func(in_dot.get_center()),
position_update_func=lambda d: d.next_to(func_tex, RIGHT, SMALL_BUFF)
)
self.play(FadeIn(div_rhs))
self.add(div_rhs_update)
show_flow()
for vect in 2 * RIGHT, 3 * DOWN, 2 * LEFT, 2 * LEFT:
self.play(in_dot.shift, vect, run_time=3)
show_flow()
self.wait()
def func(self, point):
x, y = point[:2]
return np.sin(x + y) * RIGHT + np.sin(y * x / 3) * UP
class DivergenceZeroCondition(Scene):
def construct(self):
title = TextMobject(
"For actual (incompressible) fluid flow:"
)
title.to_edge(UP)
equation = TexMobject(
"\\text{div} \\, \\textbf{F} = 0 \\quad \\text{everywhere}"
)
equation.next_to(title, DOWN)
for mob in title, equation:
mob.add_background_rectangle(buff=MED_SMALL_BUFF / 2)
self.add_foreground_mobjects(mob)
self.wait(1)
class PureCylinderFlow(Scene):
def construct(self):
self.add_vector_field()
self.begin_flow()
self.add_circle()
self.wait(5)
def add_vector_field(self):
vector_field = VectorField(
cylinder_flow_vector_field,
)
for vector in vector_field:
if get_norm(vector.get_start()) < 1:
vector_field.remove(vector)
vector_field.set_fill(opacity=0.75)
self.modify_vector_field(vector_field)
self.add_foreground_mobjects(vector_field)
def begin_flow(self):
stream_lines = StreamLines(
cylinder_flow_vector_field,
colors=[BLUE_E, BLUE_D, BLUE_C],
start_points_generator_config={
"delta_x": 0.125,
"delta_y": 0.125,
},
virtual_time=5,
)
self.add(stream_lines)
for stream_line in stream_lines:
if get_norm(stream_line.points[0]) < 1:
stream_lines.remove(stream_line)
self.modify_flow(stream_lines)
stream_line_animation = AnimatedStreamLines(stream_lines)
stream_line_animation.update(3)
self.add(stream_line_animation)
def add_circle(self):
circle = Circle(
radius=1,
stroke_color=YELLOW,
fill_color=BLACK,
fill_opacity=1,
)
self.modify_flow(circle)
self.add_foreground_mobjects(circle)
def modify_flow(self, mobject):
pass
def modify_vector_field(self, vector_field):
pass
class PureAirfoilFlow(PureCylinderFlow):
def modify_flow(self, mobject):
vect = 0.1 * LEFT + 0.2 * UP
mobject.scale(get_norm(vect - RIGHT))
mobject.shift(vect)
mobject.apply_complex_function(joukowsky_map)
return mobject
def modify_vector_field(self, vector_field):
def func(z):
w = complex(-0.1, 0.2)
n = abs(w - 1)
return joukowsky_map(inverse_joukowsky_map(z) - w / n)
def new_vector_field_func(point):
z = R3_to_complex(point)
return complex_to_R3(derivative(func)(z).conjugate())
vf = VectorField(new_vector_field_func, delta_y=0.33)
Transform(vector_field, vf).update(1)
vf.set_fill(opacity=0.5)
class IntroduceCurl(IntroduceVectorField):
CONFIG = {
"stream_line_animation_config": {
"line_anim_class": ShowPassingFlash,
},
"stream_line_config": {
"start_points_generator_config": {
"delta_x": 0.125,
"delta_y": 0.125,
},
"virtual_time": 1,
}
}
def construct(self):
self.add_title()
self.show_vector_field()
self.begin_flow()
self.show_rotation()
def add_title(self):
title = self.title = Title(
"Curl",
match_underline_width_to_text=True,
scale_factor=1.5,
)
title.add_background_rectangle()
title.to_edge(UP, buff=MED_SMALL_BUFF)
self.add_foreground_mobjects(title)
def show_vector_field(self):
vector_field = self.vector_field = VectorField(
four_swirls_function,
**self.vector_field_config
)
vector_field.submobjects.sort(
key=lambda v: v.get_length()
)
self.play(LaggedStartMap(GrowArrow, vector_field))
self.wait()
def begin_flow(self):
stream_lines = StreamLines(
self.vector_field.func,
**self.stream_line_config
)
stream_line_animation = AnimatedStreamLines(
stream_lines,
**self.stream_line_animation_config
)
self.add(stream_line_animation)
self.wait(3)
def show_rotation(self):
clockwise_arrows, counterclockwise_arrows = [
VGroup(*[
self.get_rotation_arrows(clockwise=cw).move_to(point)
for point in points
])
for cw, points in [
(True, [2 * UP, 2 * DOWN]),
(False, [4 * LEFT, 4 * RIGHT]),
]
]
for group, u in (counterclockwise_arrows, +1), (clockwise_arrows, -1):
for arrows in group:
label = TexMobject(
"\\text{curl} \\, \\textbf{F}",
">" if u > 0 else "<",
"0"
)
label.add_background_rectangle()
label.next_to(arrows, DOWN)
self.add_foreground_mobjects(label)
always_rotate(arrows, rate=u * 30 * DEGREES)
self.play(
FadeIn(arrows),
FadeIn(label)
)
self.wait(2)
for group in counterclockwise_arrows, clockwise_arrows:
self.play(FocusOn(group[0]))
self.play(
UpdateFromAlphaFunc(
group,
lambda mob, alpha: mob.set_color(
interpolate_color(WHITE, PINK, alpha)
).set_stroke(
width=interpolate(5, 10, alpha)
),
rate_func=there_and_back,
run_time=2
)
)
self.wait()
self.wait(6)
# Helpers
def get_rotation_arrows(self, clockwise=True, width=1):
result = VGroup(*[
Arrow(
*points,
buff=2 * SMALL_BUFF,
path_arc=90 * DEGREES
).set_stroke(width=5)
for points in adjacent_pairs(compass_directions(4, RIGHT))
])
if clockwise:
result.flip()
result.set_width(width)
return result
class ShearCurl(IntroduceCurl):
def construct(self):
self.show_vector_field()
self.begin_flow()
self.wait(2)
self.comment_on_relevant_region()
def show_vector_field(self):
vector_field = self.vector_field = VectorField(
self.func, **self.vector_field_config
)
vector_field.submobjects.key=sort(
key=lambda a: a.get_length()
)
self.play(LaggedStartMap(GrowArrow, vector_field))
def comment_on_relevant_region(self):
circle = Circle(color=WHITE, radius=0.75)
circle.next_to(ORIGIN, UP, LARGE_BUFF)
self.play(ShowCreation(circle))
slow_words, fast_words = words = [
TextMobject("Slow flow below"),
TextMobject("Fast flow above")
]
for word, vect in zip(words, [DOWN, UP]):
word.add_background_rectangle(buff=SMALL_BUFF)
word.next_to(circle, vect)
self.add_foreground_mobjects(word)
self.play(Write(word))
self.wait()
twig = Rectangle(
height=0.8 * 2 * circle.radius,
width=SMALL_BUFF,
stroke_width=0,
fill_color=GREY_BROWN,
fill_opacity=1,
)
twig.add(Dot(twig.get_center()))
twig.move_to(circle)
always_rotate(
twig, rate=-90 * DEGREES,
)
self.play(FadeInFrom(twig, UP))
self.add(twig_rotation)
self.wait(16)
# Helpers
def func(self, point):
return 0.5 * point[1] * RIGHT
class FromKAWrapper(TeacherStudentsScene):
def construct(self):
screen = self.screen
self.play(
self.teacher.change, "raise_right_hand",
self.get_student_changes(
"pondering", "confused", "hooray",
)
)
self.look_at(screen)
self.wait(2)
self.change_student_modes("erm", "happy", "confused")
self.wait(3)
self.teacher_says(
"Our focus is \\\\ the 2d version",
bubble_kwargs={"width": 4, "height": 3},
added_anims=[self.get_student_changes(
"happy", "hooray", "happy"
)]
)
self.wait()
class ShowCurlAtVariousPoints(IntroduceCurl):
CONFIG = {
"func": four_swirls_function,
"sample_points": [
4 * RIGHT,
2 * UP,
4 * LEFT,
2 * DOWN,
ORIGIN,
3 * RIGHT + 2 * UP,
3 * LEFT + 2 * UP,
],
"vector_field_config": {
"fill_opacity": 0.75
},
"stream_line_config": {
"virtual_time": 5,
"start_points_generator_config": {
"delta_x": 0.25,
"delta_y": 0.25,
}
}
}
def construct(self):
self.add_plane()
self.show_vector_field()
self.begin_flow()
self.show_curl_at_points()
def add_plane(self):
plane = NumberPlane()
plane.add_coordinates()
self.add(plane)
self.plane = plane
def show_curl_at_points(self):
dot = Dot()
circle = Circle(radius=0.25, color=WHITE)
circle.move_to(dot)
circle_update = Mobject.add_updater(
circle,
lambda m: m.move_to(dot)
)
curl_tex = TexMobject(
"\\text{curl} \\, \\textbf{F}(x, y) = "
)
curl_tex.add_background_rectangle(buff=0.025)
curl_tex_update = Mobject.add_updater(
curl_tex,
lambda m: m.next_to(circle, UP, SMALL_BUFF)
)
curl_func = two_d_curl(self.func)
curl_value = DecimalNumber(
0, include_sign=True,
include_background_rectangle=True,
)
curl_value_update = ContinualChangingDecimal(
curl_value,
lambda a: curl_func(dot.get_center()),
position_update_func=lambda m: m.next_to(
curl_tex, RIGHT, buff=0
),
include_background_rectangle=True,
include_sign=True,
)
points = self.sample_points
self.add(dot, circle_update)
self.play(
dot.move_to, points[0],
VFadeIn(dot),
VFadeIn(circle),
)
curl_tex_update.update(0)
curl_value_update.update(0)
self.play(Write(curl_tex), FadeIn(curl_value))
self.add(curl_tex_update, curl_value_update)
self.wait()
for point in points[1:]:
self.play(dot.move_to, point, run_time=3)
self.wait(2)
self.wait(2)
class IllustrationUseVennDiagram(Scene):
def construct(self):
title = Title("Divergence \\& Curl")
title.to_edge(UP, buff=MED_SMALL_BUFF)
useful_for = TextMobject("Useful for")
useful_for.next_to(title, DOWN)
useful_for.set_color(BLUE)
fluid_flow = TextMobject("Fluid \\\\ flow")
fluid_flow.next_to(ORIGIN, UL)
ff_circle = Circle(color=YELLOW)
ff_circle.surround(fluid_flow, stretch=True)
fluid_flow.match_color(ff_circle)
big_circle = Circle(
fill_color=BLUE,
fill_opacity=0.2,
stroke_color=BLUE,
)
big_circle.stretch_to_fit_width(9)
big_circle.stretch_to_fit_height(6)
big_circle.next_to(useful_for, DOWN, SMALL_BUFF)
illustrated_by = TextMobject("Illustrated by")
illustrated_by.next_to(
big_circle.point_from_proportion(3. / 8), UL
)
illustrated_by.match_color(ff_circle)
illustrated_by_arrow = Arrow(
illustrated_by.get_bottom(),
ff_circle.get_left(),
path_arc=90 * DEGREES,
color=YELLOW,
)
illustrated_by_arrow.pointwise_become_partial(
illustrated_by_arrow, 0, 0.95
)
examples = VGroup(
TextMobject("Electricity"),
TextMobject("Magnetism"),
TextMobject("Phase flow"),
TextMobject("Stokes' theorem"),
)
points = [
2 * RIGHT + 0.5 * UP,
2 * RIGHT + 0.5 * DOWN,
2 * DOWN,
2 * LEFT + DOWN,
]
for example, point in zip(examples, points):
example.move_to(point)
self.play(Write(title), run_time=1)
self.play(
Write(illustrated_by),
ShowCreation(illustrated_by_arrow),
run_time=1,
)
self.play(
ShowCreation(ff_circle),
FadeIn(fluid_flow),
)
self.wait()
self.play(
Write(useful_for),
DrawBorderThenFill(big_circle),
Animation(fluid_flow),
Animation(ff_circle),
)
self.play(LaggedStartMap(
FadeIn, examples,
run_time=3,
))
self.wait()
class MaxwellsEquations(Scene):
CONFIG = {
"faded_opacity": 0.3,
}
def construct(self):
self.add_equations()
self.circle_gauss_law()
self.circle_magnetic_divergence()
self.circle_curl_equations()
def add_equations(self):
title = Title("Maxwell's equations")
title.to_edge(UP, buff=MED_SMALL_BUFF)
tex_to_color_map = {
"\\textbf{E}": BLUE,
"\\textbf{B}": YELLOW,
"\\rho": WHITE,
}
equations = self.equations = VGroup(*[
TexMobject(
tex, tex_to_color_map=tex_to_color_map
)
for tex in [
"""
\\text{div} \\, \\textbf{E} =
{\\rho \\over \\varepsilon_0}
""",
"""\\text{div} \\, \\textbf{B} = 0""",
"""
\\text{curl} \\, \\textbf{E} =
-{\\partial \\textbf{B} \\over \\partial t}
""",
"""
\\text{curl} \\, \\textbf{B} =
\\mu_0 \\left(
\\textbf{J} + \\varepsilon_0
{\\partial \\textbf{E} \\over \\partial t}
\\right)
""",
]
])
equations.arrange(
DOWN, aligned_edge=LEFT,
buff=MED_LARGE_BUFF
)
field_definitions = VGroup(*[
TexMobject(text, tex_to_color_map=tex_to_color_map)
for text in [
"\\text{Electric field: } \\textbf{E}",
"\\text{Magnetic field: } \\textbf{B}",
]
])
field_definitions.arrange(
RIGHT, buff=MED_LARGE_BUFF
)
field_definitions.next_to(title, DOWN, MED_LARGE_BUFF)
equations.next_to(field_definitions, DOWN, MED_LARGE_BUFF)
field_definitions.shift(MED_SMALL_BUFF * UP)
self.add(title)
self.add(field_definitions)
self.play(LaggedStartMap(
FadeIn, equations,
run_time=3,
lag_range=0.4
))
self.wait()
def circle_gauss_law(self):
equation = self.equations[0]
rect = SurroundingRectangle(equation)
rect.set_color(RED)
rho = equation.get_part_by_tex("\\rho")
sub_rect = SurroundingRectangle(rho)
sub_rect.match_color(rect)
rho_label = TextMobject("Charge density")
rho_label.next_to(sub_rect, RIGHT)
rho_label.match_color(sub_rect)
gauss_law = TextMobject("Gauss's law")
gauss_law.next_to(rect, RIGHT)
self.play(
ShowCreation(rect),
Write(gauss_law, run_time=1),
self.equations[1:].set_fill, {"opacity": self.faded_opacity}
)
self.wait(2)
self.play(
ReplacementTransform(rect, sub_rect),
FadeOut(gauss_law),
FadeIn(rho_label),
rho.match_color, sub_rect,
)
self.wait()
self.play(
self.equations.to_edge, LEFT,
MaintainPositionRelativeTo(rho_label, equation),
MaintainPositionRelativeTo(sub_rect, equation),
VFadeOut(rho_label),
VFadeOut(sub_rect),
)
self.wait()
def circle_magnetic_divergence(self):
equations = self.equations
rect = SurroundingRectangle(equations[1])
self.play(
equations[0].set_fill, {"opacity": self.faded_opacity},
equations[1].set_fill, {"opacity": 1.0},
)
self.play(ShowCreation(rect))
self.wait(3)
self.play(FadeOut(rect))
def circle_curl_equations(self):
equations = self.equations
rect = SurroundingRectangle(equations[2:])
randy = Randolph(height=2)
randy.flip()
randy.next_to(rect, RIGHT, aligned_edge=DOWN)
randy.look_at(rect)
self.play(
equations[1].set_fill, {"opacity": self.faded_opacity},
equations[2:].set_fill, {"opacity": 1.0},
)
self.play(ShowCreation(rect))
self.play(
randy.change, "confused",
VFadeIn(randy),
)
self.play(Blink(randy))
self.play(randy.look_at, 2 * RIGHT)
self.wait(3)
self.play(
FadeOut(rect),
randy.change, "pondering",
randy.look_at, rect,
)
self.wait()
self.play(Blink(randy))
self.wait()
class ThatWeKnowOf(Scene):
def construct(self):
words = TextMobject("*That we know of!")
self.add(words)
class IllustrateGaussLaw(DefineDivergence, MovingCameraScene):
CONFIG = {
"flow_time": 10,
"stream_line_config": {
"start_points_generator_config": {
"delta_x": 1.0 / 16,
"delta_y": 1.0 / 16,
"x_min": -2,
"x_max": 2,
"y_min": -1.5,
"y_max": 1.5,
},
"color_lines_by_magnitude": True,
"colors": [BLUE_E, BLUE_D, BLUE_C],
"stroke_width": 3,
},
"stream_line_animation_config": {
"line_anim_class": ShowPassingFlashWithThinningStrokeWidth,
"line_anim_config": {
"n_segments": 5,
}
},
"final_frame_width": 4,
}
def construct(self):
particles = self.get_particles()
vector_field = self.get_vector_field()
self.add_foreground_mobjects(vector_field)
self.add_foreground_mobjects(particles)
self.zoom_in()
self.show_flow()
def get_particles(self):
particles = VGroup(
get_proton(radius=0.1),
get_electron(radius=0.1),
)
particles.arrange(RIGHT, buff=2.25)
particles.shift(0.25 * UP)
for particle, sign in zip(particles, [+1, -1]):
particle.charge = sign
self.particles = particles
return particles
def zoom_in(self):
self.play(
self.camera_frame.set_width, self.final_frame_width,
run_time=2
)
class IllustrateGaussMagnetic(IllustrateGaussLaw):
CONFIG = {
"final_frame_width": 7,
"stream_line_config": {
"start_points_generator_config": {
"delta_x": 1.0 / 16,
"delta_y": 1.0 / 16,
"x_min": -3.5,
"x_max": 3.5,
"y_min": -2,
"y_max": 2,
},
"color_lines_by_magnitude": True,
"colors": [BLUE_E, BLUE_D, BLUE_C],
"stroke_width": 3,
},
"stream_line_animation_config": {
"start_up_time": 0,
},
"flow_time": 10,
}
def construct(self):
self.add_wires()
self.show_vector_field()
self.zoom_in()
self.show_flow()
def add_wires(self):
top, bottom = [
Circle(
radius=0.275,
stroke_color=WHITE,
fill_color=BLACK,
fill_opacity=1
)
for x in range(2)
]
top.add(TexMobject("\\times").scale(0.5))
bottom.add(Dot().scale(0.5))
top.move_to(1 * UP)
bottom.move_to(1 * DOWN)
self.add_foreground_mobjects(top, bottom)
def show_vector_field(self):
vector_field = self.vector_field = VectorField(
self.func, **self.vector_field_config
)
vector_field.submobjects.sort(
key=lambda a: -a1.get_length()
)
self.play(LaggedStartMap(GrowArrow, vector_field))
self.add_foreground_mobjects(
vector_field, *self.foreground_mobjects
)
def func(self, point):
x, y = point[:2]
top_part = np.array([(y - 1.0), -x, 0])
bottom_part = np.array([-(y + 1.0), x, 0])
norm = get_norm
return 1 * op.add(
top_part / (norm(top_part) * norm(point - UP) + 0.1),
bottom_part / (norm(bottom_part) * norm(point - DOWN) + 0.1),
# top_part / (norm(top_part)**2 + 1),
# bottom_part / (norm(bottom_part)**2 + 1),
)
class IllustrateEMCurlEquations(ExternallyAnimatedScene):
pass
class RelevantInNonSpatialCircumstances(TeacherStudentsScene):
def construct(self):
self.teacher_says(
"""
$\\textbf{div}$ and $\\textbf{curl}$ are \\\\
even useful in some \\\\
non-spatial problems
""",
target_mode="hooray"
)
self.change_student_modes(
"sassy", "confused", "hesitant"
)
self.wait(3)
class ShowTwoPopulations(Scene):
CONFIG = {
"total_num_animals": 80,
"start_num_foxes": 40,
"start_num_rabbits": 20,
"animal_height": 0.5,
"final_wait_time": 30,
"count_word_scale_val": 1,
}
def construct(self):
self.introduce_animals()
self.evolve_system()
def introduce_animals(self):
foxes = self.foxes = VGroup(*[
self.get_fox()
for n in range(self.total_num_animals)
])
rabbits = self.rabbits = VGroup(*[
self.get_rabbit()
for n in range(self.total_num_animals)
])
foxes[self.start_num_foxes:].set_fill(opacity=0)
rabbits[self.start_num_rabbits:].set_fill(opacity=0)
fox, rabbit = examples = VGroup(foxes[0], rabbits[0])
for mob in examples:
mob.save_state()
mob.set_height(3)
examples.arrange(LEFT, buff=2)
preditor, prey = words = VGroup(
TextMobject("Predator"),
TextMobject("Prey")
)
for mob, word in zip(examples, words):
word.scale(1.5)
word.next_to(mob, UP)
self.play(
FadeInFromDown(mob),
Write(word, run_time=1),
)
self.play(
LaggedStartMap(
ApplyMethod, examples,
lambda m: (m.restore,)
),
LaggedStartMap(FadeOut, words),
*[
LaggedStartMap(
FadeIn,
group[1:],
run_time=4,
lag_ratio=0.1,
rate_func=lambda t: np.clip(smooth(2 * t), 0, 1)
)
for group in [foxes, rabbits]
]
)
def evolve_system(self):
foxes = self.foxes
rabbits = self.rabbits
phase_point = VectorizedPoint(
self.start_num_rabbits * RIGHT +
self.start_num_foxes * UP
)
self.add(move_along_vector_field(
phase_point,
preditor_prey_vector_field,
))
def get_num_rabbits():
return phase_point.get_center()[0]
def get_num_foxes():
return phase_point.get_center()[1]
def get_updater(pop_size_getter):
def update(animals):
target_num = pop_size_getter()
for n, animal in enumerate(animals):
animal.set_fill(
opacity=np.clip(target_num - n, 0, 1)
)
target_int = int(np.ceil(target_num))
tail = animals.submobjects[target_int:]
random.shuffle(tail)
animals.submobjects[target_int:] = tail
return update
self.add(Mobject.add_updater(
foxes, get_updater(get_num_foxes)
))
self.add(Mobject.add_updater(
rabbits, get_updater(get_num_rabbits)
))
# Add counts for foxes and rabbits
labels = self.get_pop_labels()
num_foxes = Integer(10)
num_foxes.scale(self.count_word_scale_val)
num_foxes.next_to(labels[0], RIGHT)
num_foxes.align_to(labels[0][0][1], DOWN)
num_rabbits = Integer(10)
num_rabbits.scale(self.count_word_scale_val)
num_rabbits.next_to(labels[1], RIGHT)
num_rabbits.align_to(labels[1][0][1], DOWN)
num_foxes.add_updater(lambda d: d.set_value(get_num_foxes()))
num_rabbits.add_updater(lambda d: d.set_value(get_num_rabbits()))
self.add(num_foxes, num_rabbits)
for count in num_foxes, num_rabbits:
self.add(Mobject.add_updater(
count, self.update_count_color,
))
self.play(
FadeIn(labels),
*[
UpdateFromAlphaFunc(count, lambda m, a: m.set_fill(opacity=a))
for count in (num_foxes, num_rabbits)
]
)
self.wait(self.final_wait_time)
# Helpers
def get_animal(self, name, color):
result = SVGMobject(
file_name=name,
height=self.animal_height,
fill_color=color,
)
# for submob in result.family_members_with_points():
# if submob.is_subpath:
# submob.is_subpath = False
# submob.set_fill(
# interpolate_color(color, BLACK, 0.8),
# opacity=1
# )
x_shift, y_shift = [
(2 * random.random() - 1) * max_val
for max_val in [
FRAME_WIDTH / 2 - 2,
FRAME_HEIGHT / 2 - 2
]
]
result.shift(x_shift * RIGHT + y_shift * UP)
return result
def get_fox(self):
return self.get_animal("fox", FOX_COLOR)
def get_rabbit(self):
# return self.get_animal("rabbit", WHITE)
return self.get_animal("bunny", RABBIT_COLOR)
def get_pop_labels(self):
labels = VGroup(
TextMobject("\\# Foxes: "),
TextMobject("\\# Rabbits: "),
)
for label in labels:
label.scale(self.count_word_scale_val)
labels.arrange(RIGHT, buff=2)
labels.to_edge(UP)
return labels
def update_count_color(self, count):
count.set_fill(interpolate_color(
BLUE, RED, (count.number - 20) / 30.0
))
return count
class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCameraScene):
CONFIG = {
"origin": 5 * LEFT + 2.5 * DOWN,
"vector_field_config": {
"max_magnitude": 50,
},
"pi_creatures_start_on_screen": False,
"default_pi_creature_kwargs": {
"height": 1.8
},
"flow_time": 10,
}
def setup(self):
MovingCameraScene.setup(self)
PiCreatureScene.setup(self)
def construct(self):
self.add_axes()
self.add_example_point()
self.write_differential_equations()
self.add_vectors()
self.show_phase_flow()
def add_axes(self):
axes = self.axes = Axes(
x_min=0,
x_max=55,
x_axis_config={"unit_size": 0.15},
y_min=0,
y_max=55,
y_axis_config={"unit_size": 0.09},
number_line_config={
"tick_frequency": 10,
},
)
axes.shift(self.origin)
for axis in axes.x_axis, axes.y_axis:
axis.add_numbers(*list(range(10, 60, 10)))
axes_labels = self.axes_labels = VGroup(*[
VGroup(
method().set_height(0.75),
TextMobject("Population"),
).arrange(RIGHT, buff=MED_SMALL_BUFF)
for method in (self.get_rabbit, self.get_fox)
])
for axis, label, vect in zip(axes, axes_labels, [RIGHT, UP]):
label.next_to(
axis, vect,
submobject_to_align=label[0]
)
self.add(axes, axes_labels)
def add_example_point(self):
axes = self.axes
origin = self.origin
x = self.start_num_rabbits
y = self.start_num_foxes
point = axes.coords_to_point(x, y)
x_point = axes.coords_to_point(x, 0)
y_point = axes.coords_to_point(0, y)
v_line = DashedLine(x_point, point)
h_line = DashedLine(y_point, point)
v_line.set_color(FOX_COLOR)
h_line.set_color(LIGHT_GREY)
dot = Dot(point)
coord_pair = TexMobject(
"(10, 10)", substrings_to_isolate=["10"]
)
pop_sizes = VGroup(Integer(10), Integer(10))
pop_sizes[0].set_color(LIGHT_GREY)
pop_sizes[1].set_color(FOX_COLOR)
tens = coord_pair.get_parts_by_tex("10")
tens.fade(1)
def get_pop_size_update(i):
return ContinualChangingDecimal(
pop_sizes[i],
lambda a: int(np.round(
axes.point_to_coords(dot.get_center())[i]
)),
position_update_func=lambda m: m.move_to(tens[i])
)
coord_pair.add_background_rectangle()
coord_pair_update = Mobject.add_updater(
coord_pair, lambda m: m.next_to(dot, UR, SMALL_BUFF)
)
pop_sizes_updates = [get_pop_size_update(i) for i in (0, 1)]
phase_space = TextMobject("``Phase space''")
phase_space.set_color(YELLOW)
phase_space.scale(1.5)
phase_space.to_edge(UP)
phase_space.shift(2 * RIGHT)
self.play(ShowCreation(v_line))
self.play(ShowCreation(h_line))
dot.save_state()
dot.move_to(origin)
self.add(coord_pair_update)
self.add(*pop_sizes_updates)
self.play(
dot.restore,
VFadeIn(coord_pair),
UpdateFromAlphaFunc(pop_sizes, lambda m, a: m.set_fill(opacity=a)),
)
self.wait()
self.play(Write(phase_space))
self.wait(2)
self.play(FadeOut(VGroup(h_line, v_line, phase_space)))
self.play(Rotating(
dot,
about_point=axes.coords_to_point(30, 30),
rate_func=smooth,
))
self.dot = dot
self.coord_pair = coord_pair
self.coord_pair_update = coord_pair_update
self.pop_sizes = pop_sizes
self.pop_sizes_updates = pop_sizes_updates
def write_differential_equations(self):
equations = self.get_equations()
equations.shift(2 * DOWN)
rect = SurroundingRectangle(equations, color=YELLOW)
rect.set_fill(BLACK, 0.8)
title = TextMobject("Differential equations")
title.next_to(rect, UP)
title.set_color(rect.get_stroke_color())
self.differential_equation_group = VGroup(
rect, equations, title
)
self.differential_equation_group.to_corner(UR)
randy = self.pi_creature
randy.next_to(rect, DL)
self.play(
Write(title, run_time=1),
ShowCreation(rect)
)
self.play(
LaggedStartMap(FadeIn, equations),
randy.change, "confused", equations,
VFadeIn(randy),
)
self.wait(3)
def add_vectors(self):
origin = self.axes.coords_to_point(0, 0)
dot = self.dot
randy = self.pi_creature
def rescaled_field(point):
x, y = self.axes.point_to_coords(point)
result = preditor_prey_vector_field(np.array([x, y, 0]))
return self.axes.coords_to_point(*result[:2]) - origin
self.vector_field_config.update({
"x_min": origin[0] + 0.5,
"x_max": self.axes.get_right()[0] + 1,
"y_min": origin[1] + 0.5,
"y_max": self.axes.get_top()[1],
})
vector_field = VectorField(
rescaled_field, **self.vector_field_config
)
def get_dot_vector():
vector = vector_field.get_vector(dot.get_center())
vector.scale(1, about_point=vector.get_start())
return vector
dot_vector = get_dot_vector()
self.play(
LaggedStartMap(GrowArrow, vector_field),
randy.change, "thinking", dot,
Animation(self.differential_equation_group)
)
self.wait(3)
self.play(
Animation(dot),
vector_field.set_fill, {"opacity": 0.2},
Animation(self.differential_equation_group),
GrowArrow(dot_vector),
randy.change, "pondering",
)
self.wait()
self.play(
dot.move_to, dot_vector.get_end(),
dot.align_to, dot, RIGHT,
run_time=3,
)
self.wait(2)
self.play(
dot.move_to, dot_vector.get_end(),
run_time=3,
)
self.wait(2)
for x in range(6):
new_dot_vector = get_dot_vector()
fade_anims = [
FadeOut(dot_vector),
FadeIn(new_dot_vector),
Animation(dot),
]
if x == 4:
fade_anims += [
vector_field.set_fill, {"opacity": 0.5},
FadeOut(randy),
FadeOut(self.differential_equation_group),
]
self.play(*fade_anims)
dot_vector = new_dot_vector
self.play(dot.move_to, dot_vector.get_end())
dot_movement = move_along_vector_field(
dot, lambda p: 0.3 * vector_field.func(p)
)
self.add(dot_movement)
self.play(FadeOut(dot_vector))
self.wait(10)
self.play(
vector_field.set_fill, {"opacity": 1.0},
VFadeOut(dot),
VFadeOut(self.coord_pair),
UpdateFromAlphaFunc(self.pop_sizes, lambda m, a: m.set_fill(opacity=1 - a)),
)
self.remove(
dot_movement,
self.coord_pair_update,
*self.pop_sizes_updates
)
self.wait()
self.vector_field = vector_field
def show_phase_flow(self):
vector_field = self.vector_field
stream_lines = StreamLines(
vector_field.func,
start_points_generator_config={
"x_min": vector_field.x_min,
"x_max": vector_field.x_max,
"y_min": vector_field.y_min,
"y_max": vector_field.y_max,
"delta_x": 0.25,
"delta_y": 0.25,
},
min_magnitude=vector_field.min_magnitude,
max_magnitude=vector_field.max_magnitude,
virtual_time=4,
)
stream_line_animation = AnimatedStreamLines(
stream_lines,
)
self.add(stream_line_animation)
self.add_foreground_mobjects(vector_field)
self.wait(self.flow_time)
self.play(
self.camera_frame.scale, 1.5, {"about_point": self.origin},
run_time=self.flow_time,
)
self.wait(self.flow_time)
#
def get_equations(self):
variables = ["X", "YY"]
equations = TexMobject(
"""
{dX \\over dt} =
X \\cdot (\\alpha - \\beta YY \\,) \\\\
\\quad \\\\
{dYY \\over dt} =
YY \\cdot (\\delta X - \\gamma)
""",
substrings_to_isolate=variables
)
animals = [self.get_rabbit(), self.get_fox().flip()]
for char, animal in zip(variables, animals):
for part in equations.get_parts_by_tex(char):
animal_copy = animal.copy()
animal_copy.set_height(0.5)
animal_copy.move_to(part, DL)
part.become(animal_copy)
return equations
class PhaseFlowWords(Scene):
def construct(self):
words = TextMobject("``Phase flow''")
words.scale(2)
self.play(Write(words))
self.wait()
class PhaseFlowQuestions(Scene):
def construct(self):
questions = VGroup(
TextMobject(
"Which points does the flow \\\\" +
"converge to? Diverge away from?",
),
TextMobject("Where are there cycles?"),
)
questions.arrange(DOWN, buff=LARGE_BUFF)
questions.to_corner(UR)
for question in questions:
self.play(FadeInFromDown(question))
self.wait(2)
class ToolsBeyondDivAndCurlForODEs(PiCreatureScene):
CONFIG = {
"default_pi_creature_kwargs": {
"color": GREY_BROWN,
},
"default_pi_creature_start_corner": DOWN,
}
def construct(self):
morty = self.pi_creature
div_curl = TextMobject("div \\\\", "curl")
div_curl.set_color_by_tex("div", BLUE)
div_curl.set_color_by_tex("curl", YELLOW)
div_curl.next_to(morty.get_corner(UL), UP, MED_LARGE_BUFF)
jacobian = TextMobject("Analyze the \\\\ Jacobian")
jacobian.set_color(GREEN)
jacobian.next_to(morty.get_corner(UR), UP, MED_LARGE_BUFF)
flow_intuitions = TextMobject("Flow-based intuitions")
flow_intuitions.next_to(
VGroup(div_curl, jacobian),
UP, buff=1.5
)
arrow1 = Arrow(div_curl.get_top(), flow_intuitions.get_bottom())
arrow2 = Arrow(flow_intuitions.get_bottom(), jacobian.get_top())
self.play(
FadeInFromDown(div_curl),
morty.change, "raise_left_hand",
)
self.wait()
self.play(
FadeInFromDown(jacobian),
morty.change, "raise_right_hand"
)
self.wait()
self.play(
ReplacementTransform(
flow_intuitions.copy().fade(1).move_to(div_curl),
flow_intuitions,
),
GrowArrow(arrow1),
morty.change, "pondering"
)
self.wait(0.5)
self.play(GrowArrow(arrow2))
self.wait()
class AskAboutComputation(TeacherStudentsScene):
def construct(self):
self.student_says(
"Sure, but how do you \\\\" +
"\\emph{compute} $\\textbf{div}$ and $\\textbf{curl}$?",
target_mode="sassy",
)
self.change_student_modes(
"confused", "sassy", "angry",
added_anims=[self.teacher.change, "guilty"]
)
self.wait()
self.teacher_says(
"Are you familiar \\\\" +
"with my work \\\\" +
"at Khan Academy?",
target_mode="speaking",
bubble_kwargs={"width": 4, "height": 3}
)
self.change_student_modes(
* 3 * ["pondering"],
look_at_arg=self.screen
)
self.wait(5)
class QuickWordsOnNotation(Scene):
def construct(self):
words = TextMobject("Quick words on notation:")
words.scale(1.5)
self.play(FadeInFromDown(words))
self.wait()
class NablaNotation(PiCreatureScene, MovingCameraScene):
CONFIG = {
"default_pi_creature_kwargs": {
"color": GREY_BROWN,
},
"default_pi_creature_start_corner": DL,
}
def setup(self):
MovingCameraScene.setup(self)
PiCreatureScene.setup(self)
def construct(self):
self.show_notation()
self.show_expansion()
self.zoom_out()
def show_notation(self):
morty = self.pi_creature
tex_to_color_map = {
"\\text{div}": BLUE,
"\\nabla \\cdot": BLUE,
"\\text{curl}": YELLOW,
"\\nabla \\times": YELLOW,
}
div_equation = TexMobject(
"\\text{div} \\, \\textbf{F} = \\nabla \\cdot \\textbf{F}",
tex_to_color_map=tex_to_color_map
)
div_nabla = div_equation.get_part_by_tex("\\nabla")
curl_equation = TexMobject(
"\\text{curl} \\, \\textbf{F} = \\nabla \\times \\textbf{F}",
tex_to_color_map=tex_to_color_map
)
curl_nabla = curl_equation.get_part_by_tex("\\nabla")
equations = VGroup(div_equation, curl_equation)
equations.arrange(DOWN, buff=LARGE_BUFF)
equations.next_to(morty, UP, 2)
equations.to_edge(LEFT)
self.play(
FadeInFromDown(div_equation),
morty.change, "raise_right_hand"
)
self.wait()
self.play(WiggleOutThenIn(div_nabla, scale_value=1.5))
self.wait()
self.play(
FadeInFromDown(curl_equation),
morty.change, "raise_left_hand"
)
self.wait()
self.play(WiggleOutThenIn(curl_nabla, scale_value=1.5))
self.wait()
self.equations = equations
def show_expansion(self):
equations = self.equations
morty = self.pi_creature
nabla_vector = Matrix([
["\\partial \\over \\partial x"],
["\\partial \\over \\partial y"],
], v_buff=1.5)
F_vector = Matrix([
["\\textbf{F}_x"],
["\\textbf{F}_y"],
], v_buff=1.2)
nabla_vector.match_height(F_vector)
div_lhs, curl_lhs = lhs_groups = VGroup(*[
VGroup(
nabla_vector.deepcopy(),
TexMobject(tex).scale(1.5),
F_vector.copy(),
TexMobject("=")
)
for tex in ("\\cdot", "\\times")
])
colors = [BLUE, YELLOW]
for lhs, color in zip(lhs_groups, colors):
lhs.arrange(RIGHT, buff=MED_SMALL_BUFF)
VGroup(lhs[0].brackets, lhs[1]).set_color(color)
div_lhs.to_edge(UP)
curl_lhs.next_to(div_lhs, DOWN, buff=LARGE_BUFF)
div_rhs = TexMobject(
"{\\partial F_x \\over \\partial x} + " +
"{\\partial F_y \\over \\partial y}"
)
curl_rhs = TexMobject(
"{\\partial F_y \\over \\partial x} - " +
"{\\partial F_x \\over \\partial y}"
)
rhs_groups = VGroup(div_rhs, curl_rhs)
for rhs, lhs in zip(rhs_groups, lhs_groups):
rhs.next_to(lhs, RIGHT)
for rhs, tex, color in zip(rhs_groups, ["div", "curl"], colors):
rhs.rect = SurroundingRectangle(rhs, color=color)
rhs.label = TexMobject(
"\\text{%s}" % tex,
"\\, \\textbf{F}"
)
rhs.label.set_color(color)
rhs.label.next_to(rhs.rect, UP)
for i in 0, 1:
self.play(
ReplacementTransform(
equations[i][2].copy(),
lhs_groups[i][0].brackets
),
ReplacementTransform(
equations[i][3].copy(),
lhs_groups[i][2],
),
morty.change, "pondering",
*[
GrowFromPoint(mob, equations[i].get_right())
for mob in [
lhs_groups[i][0].get_entries(),
lhs_groups[i][1],
lhs_groups[i][3]
]
],
run_time=2
)
self.wait()
self.wait()
for rhs in rhs_groups:
self.play(
Write(rhs),
morty.change, 'confused'
)
self.play(
ShowCreation(rhs.rect),
FadeInFromDown(rhs.label),
)
self.wait()
self.play(morty.change, "erm")
self.wait(3)
def zoom_out(self):
screen_rect = self.camera_frame.copy()
screen_rect.set_stroke(WHITE, 3)
screen_rect.scale(1.01)
words = TextMobject("Something deeper at play...")
words.scale(1.3)
words.next_to(screen_rect, UP)
self.add(screen_rect)
self.play(
self.camera_frame.set_height, FRAME_HEIGHT + 3,
Write(words, rate_func=squish_rate_func(smooth, 0.3, 1)),
run_time=2,
)
self.wait()
class DivCurlDotCross(Scene):
def construct(self):
rects = VGroup(*[
ScreenRectangle(height=2.5)
for n in range(4)
])
rects.arrange_in_grid(n_rows=2, buff=LARGE_BUFF)
rects[2:].shift(MED_LARGE_BUFF * DOWN)
titles = VGroup(*list(map(TextMobject, [
"Divergence", "Curl",
"Dot product", "Cross product"
])))
for title, rect in zip(titles, rects):
title.next_to(rect, UP)
self.add(rects, titles)
class ShowDotProduct(MovingCameraScene):
CONFIG = {
"prod_tex": "\\cdot"
}
def construct(self):
plane = NumberPlane()
v1 = Vector(RIGHT, color=BLUE)
v2 = Vector(UP, color=YELLOW)
dot_product = TexMobject(
"\\vec{\\textbf{v}}", self.prod_tex,
"\\vec{\\textbf{w}}", "="
)
dot_product.set_color_by_tex_to_color_map({
"textbf{v}": BLUE,
"textbf{w}": YELLOW,
})
dot_product.add_background_rectangle()
dot_product.next_to(2.25 * UP, RIGHT)
dot_product_value = DecimalNumber(
1.0,
include_background_rectangle=True,
)
dot_product_value.next_to(dot_product)
dot_product_value_update = ContinualChangingDecimal(
dot_product_value,
lambda a: self.get_product(v1, v2),
include_background_rectangle=True,
)
self.camera_frame.set_height(4)
self.camera_frame.move_to(DL, DL)
self.add(plane)
self.add(dot_product, dot_product_value_update)
self.add_additional_continual_animations(v1, v2)
self.add_foreground_mobjects(v1, v2)
for n in range(5):
self.play(
Rotate(v1, 45 * DEGREES, about_point=ORIGIN),
Rotate(v2, -45 * DEGREES, about_point=ORIGIN),
run_time=3,
rate_func=there_and_back
)
self.wait(0.5)
def get_product(self, v1, v2):
return np.dot(v1.get_vector(), v2.get_vector())
def add_additional_continual_animations(self, v1, v2):
pass
class ShowCrossProduct(ShowDotProduct):
CONFIG = {
"prod_tex": "\\times"
}
def get_product(self, v1, v2):
return get_norm(
np.cross(v1.get_vector(), v2.get_vector())
)
def add_additional_continual_animations(self, v1, v2):
square = Square(
stroke_color=YELLOW,
stroke_width=3,
fill_color=YELLOW,
fill_opacity=0.2,
)
self.add(Mobject.add_updater(
square,
lambda s: s.set_points_as_corners([
ORIGIN,
v1.get_end(),
v1.get_end() + v2.get_end(),
v2.get_end(),
])
))
class DivergenceTinyNudgesView(MovingCameraScene):
CONFIG = {
"scale_factor": 0.25,
"point": ORIGIN,
}
def construct(self):
self.add_vector_field()
self.zoom_in()
self.take_tiny_step()
self.show_dot_product()
self.show_circle_of_values()
self.switch_to_curl_words()
self.rotate_difference_vectors()
def add_vector_field(self):
plane = self.plane = NumberPlane()
def func(p):
x, y = p[:2]
result = np.array([
np.sin(x + 0.1),
np.cos(2 * y),
0
])
result /= (get_norm(result)**0.5 + 1)
return result
vector_field = self.vector_field = VectorField(
func,
length_func=lambda n: 0.5 * sigmoid(n),
# max_magnitude=1.0,
)
self.add(plane)
self.add(vector_field)
def zoom_in(self):
point = self.point
vector_field = self.vector_field
sf = self.scale_factor
vector_field.vector_config.update({
"rectangular_stem_width": 0.02,
"tip_length": 0.1,
})
vector_field.length_func = lambda n: n
vector = vector_field.get_vector(point)
input_dot = Dot(point).scale(sf)
input_words = TextMobject("$(x_0, y_0)$").scale(sf)
input_words.next_to(input_dot, DL, SMALL_BUFF * sf)
output_words = TextMobject("Output").scale(sf)
output_words.add_background_rectangle()
output_words.next_to(vector.get_top(), UP, sf * SMALL_BUFF)
output_words.match_color(vector)
self.play(
self.camera_frame.scale, sf,
self.camera_frame.move_to, point,
FadeOut(vector_field),
FadeIn(vector),
run_time=2
)
self.add_foreground_mobjects(input_dot)
self.play(
FadeInFrom(input_dot, SMALL_BUFF * DL),
Write(input_words),
)
self.play(
Indicate(vector),
Write(output_words),
)
self.wait()
self.set_variables_as_attrs(
point, vector, input_dot,
input_words, output_words,
)
def take_tiny_step(self):
sf = self.scale_factor
vector_field = self.vector_field
point = self.point
vector = self.vector
output_words = self.output_words
input_dot = self.input_dot
nudge = 0.5 * RIGHT
nudged_point = point + nudge
new_vector = vector_field.get_vector(nudged_point)
new_vector.set_color(YELLOW)
new_dot = Dot(nudged_point).scale(sf)
step_vector = Arrow(
point, nudged_point,
buff=0,
color=TEAL,
**vector_field.vector_config
)
step_vector.set_stroke(BLACK, 0.5)
new_output_words = TextMobject("New output").scale(sf)
new_output_words.add_background_rectangle()
new_output_words.next_to(new_vector.get_end(), UP, sf * SMALL_BUFF)
new_output_words.match_color(new_vector)
step_words = TextMobject("Step").scale(sf)
step_words.next_to(step_vector, UP, buff=0)
step_words.set_color(step_vector.get_fill_color())
step_words.add_background_rectangle()
small_step_words = TextMobject("(think tiny step)").scale(sf)
small_step_words.next_to(
step_words, RIGHT,
buff=sf * MED_SMALL_BUFF,
)
small_step_words.add_background_rectangle()
small_step_words.match_style(step_words)
shifted_vector = vector.copy().shift(nudge)
diff_vector = Arrow(
shifted_vector.get_end(),
new_vector.get_end(),
buff=0,
color=RED,
**vector_field.vector_config
)
diff_words = TextMobject("Difference").scale(sf)
diff_words.add_background_rectangle()
diff_words.next_to(diff_vector.get_start(), UR, buff=2 * sf * SMALL_BUFF)
diff_words.match_color(diff_vector)
diff_words.rotate(
diff_vector.get_angle(),
about_point=diff_vector.get_start()
)
self.play(
GrowArrow(step_vector),
Write(step_words),
ReplacementTransform(input_dot.copy(), new_dot)
)
self.add_foreground_mobjects(new_dot)
self.play(FadeIn(small_step_words))
self.play(FadeOut(small_step_words))
self.play(
ReplacementTransform(vector.copy(), new_vector),
ReplacementTransform(output_words.copy(), new_output_words),
)
self.wait()
self.play(ReplacementTransform(
vector.copy(), shifted_vector,
path_arc=-TAU / 4
))
self.wait()
self.play(
FadeOut(output_words),
FadeOut(new_output_words),
GrowArrow(diff_vector),
Write(diff_words)
)
self.wait()
self.play(
vector.scale, 0, {"about_point": vector.get_start()},
shifted_vector.scale, 0, {"about_point": shifted_vector.get_start()},
ReplacementTransform(
new_vector,
diff_vector.copy().shift(-vector.get_vector()),
remover=True
),
diff_vector.shift, -vector.get_vector(),
MaintainPositionRelativeTo(diff_words, diff_vector),
run_time=2
)
self.wait()
self.set_variables_as_attrs(
step_vector, step_words,
diff_vector, diff_words,
)
def show_dot_product(self):
sf = self.scale_factor
point = self.point
step_vector = self.step_vector
step_words = self.step_words
diff_vector = self.diff_vector
diff_words = self.diff_words
vects = VGroup(step_vector, diff_vector)
moving_step_vector = step_vector.copy()
moving_diff_vector = diff_vector.copy()
def update_moving_diff_vector(dv):
step = moving_step_vector.get_vector()
o1 = self.vector_field.get_vector(point).get_vector()
o2 = self.vector_field.get_vector(point + step).get_vector()
diff = o2 - o1
dv.put_start_and_end_on(
moving_step_vector.get_end(),
moving_step_vector.get_end() + diff,
)
self.moving_diff_vector_update = Mobject.add_updater(
moving_diff_vector,
update_moving_diff_vector
)
self.add(self.moving_diff_vector_update)
div_text = self.get_operator_text("div")
step_words_copy = step_words.copy()
diff_words_copy = diff_words.copy()
copies = VGroup(step_words_copy, diff_words_copy)
substrings = ["Step", "Difference"]
dot_product = TextMobject(
"(Step) $\\cdot$ (Difference)",
substrings_to_isolate=substrings,
arg_separator="",
).scale(sf)
group = VGroup(div_text, dot_product)
group.arrange(RIGHT, buff=sf * MED_SMALL_BUFF)
group.next_to(
self.camera_frame.get_top(), DOWN,
buff=sf * MED_SMALL_BUFF
)
for substring, mob, vect in zip(substrings, copies, vects):
part = dot_product.get_part_by_tex(substring)
mob.generate_target()
mob.target.rotate(-vect.get_angle())
mob.target.replace(part)
# part.set_fill(opacity=0)
part.match_color(mob)
dot_product.add_background_rectangle()
brace = Brace(
dot_product.copy().scale(1 / sf, about_point=ORIGIN), DOWN,
buff=SMALL_BUFF
).scale(sf, about_point=ORIGIN)
dp_kwargs = {
"include_sign": True,
}
dot_product_value = DecimalNumber(1.0, **dp_kwargs)
dot_product_value.scale(sf)
dot_product_value.next_to(brace, DOWN, sf * SMALL_BUFF)
dot_product_value_update = ContinualChangingDecimal(
dot_product_value,
lambda a: np.dot(
moving_step_vector.get_vector(),
moving_diff_vector.get_vector(),
),
**dp_kwargs
)
self.play(
Write(dot_product),
LaggedStartMap(MoveToTarget, copies)
)
self.remove(copies)
self.play(FadeIn(div_text))
self.play(ShowPassingFlashAround(
div_text[1:3],
surrounding_rectangle_config={"buff": sf * SMALL_BUFF}
))
self.add(BackgroundRectangle(dot_product_value))
self.play(
GrowFromCenter(brace),
Write(dot_product_value),
)
self.add(dot_product_value_update)
self.wait()
self.set_variables_as_attrs(
div_text, dot_product,
moving_step_vector,
moving_diff_vector,
dot_product_value,
dot_product_value_update,
brace,
)
def show_circle_of_values(self):
point = self.point
moving_step_vector = self.moving_step_vector
moving_diff_vector = self.moving_diff_vector
all_diff_vectors = VGroup()
all_step_vectors = VGroup()
# Loop around
n_samples = 12
angle = TAU / n_samples
self.add_foreground_mobjects(self.step_words)
for n in range(n_samples):
self.play(
Rotating(
moving_step_vector,
radians=angle,
about_point=point,
run_time=15.0 / n_samples,
rate_func=linear,
)
)
step_vector_copy = moving_step_vector.copy()
diff_vector_copy = moving_diff_vector.copy()
diff_vector_copy.set_stroke(BLACK, 0.5)
self.add(step_vector_copy, diff_vector_copy)
all_step_vectors.add(step_vector_copy)
all_diff_vectors.add(diff_vector_copy)
self.remove(
self.step_vector, self.diff_vector,
self.moving_step_vector, self.moving_diff_vector,
self.moving_diff_vector_update,
self.dot_product_value_update
)
self.remove_foreground_mobjects(self.step_words)
self.play(
FadeOut(self.brace),
FadeOut(self.dot_product_value),
FadeOut(self.step_words),
FadeOut(self.diff_words),
)
self.wait()
for s in 0.6, -0.6:
for step, diff in zip(all_step_vectors, all_diff_vectors):
diff.generate_target()
diff.target.put_start_and_end_on(
step.get_end(),
step.get_end() + s * step.get_vector()
)
self.play(
all_step_vectors.set_fill, {"opacity": 0.5},
LaggedStartMap(
MoveToTarget, all_diff_vectors,
run_time=3
),
)
self.wait()
self.show_stream_lines(lambda p: s * (p - point))
self.wait()
self.set_variables_as_attrs(
all_step_vectors, all_diff_vectors,
)
def switch_to_curl_words(self):
sf = self.scale_factor
div_text = self.div_text
dot_product = self.dot_product
curl_text = self.get_operator_text("curl")
cross_product = TextMobject(
"(Step) $\\times$ (Difference)",
tex_to_color_map={
"Step": TEAL,
"Difference": RED
},
arg_separator="",
).scale(sf)
cross_product.add_background_rectangle(opacity=1)
group = VGroup(curl_text, cross_product)
group.arrange(RIGHT, buff=sf * MED_SMALL_BUFF)
group.next_to(self.camera_frame.get_top(), sf * DOWN)
self.play(
dot_product.shift, sf * DOWN,
dot_product.fade, 1,
remover=True
)
self.play(FadeInFrom(cross_product, sf * DOWN))
self.play(
div_text.shift, sf * DOWN,
div_text.fade, 1,
remover=True
)
self.play(FadeInFrom(curl_text, sf * DOWN))
self.wait()
def rotate_difference_vectors(self):
point = self.point
all_step_vectors = self.all_step_vectors
all_diff_vectors = self.all_diff_vectors
for s in 0.6, -0.6:
for step, diff in zip(all_step_vectors, all_diff_vectors):
diff.generate_target()
diff.target.put_start_and_end_on(
step.get_end(),
step.get_end() + s * rotate_vector(
step.get_vector(),
90 * DEGREES
)
)
self.play(
LaggedStartMap(
MoveToTarget, all_diff_vectors,
run_time=2
),
)
self.wait()
self.show_stream_lines(
lambda p: s * rotate_vector((p - point), 90 * DEGREES)
)
self.wait()
self.set_variables_as_attrs(
all_step_vectors, all_diff_vectors,
)
# Helpers
def get_operator_text(self, operator):
text = TextMobject(
operator + "\\,",
"$\\textbf{F}(x_0, y_0)\\,$",
"corresponds to average of",
arg_separator=""
).scale(self.scale_factor)
text.set_color_by_tex(operator, YELLOW)
text.add_background_rectangle()
return text
def show_stream_lines(self, func):
point = self.point
stream_lines = StreamLines(
func,
start_points_generator_config={
"x_min": point[0] - 2,
"x_max": point[0] + 2,
"y_min": point[1] - 1,
"y_max": point[1] + 1,
"delta_x": 0.025,
"delta_y": 0.025,
},
virtual_time=1,
)
random.shuffle(stream_lines.submobjects)
self.play(LaggedStartMap(
ShowPassingFlash,
stream_lines,
run_time=4,
))
class ZToHalfFlowNearWall(ComplexTransformationScene, MovingCameraScene):
CONFIG = {
"num_anchors_to_add_per_line": 200,
"plane_config": {"y_radius": 8}
}
def setup(self):
MovingCameraScene.setup(self)
ComplexTransformationScene.setup(self)
def construct(self):
# self.camera.frame.shift(2 * UP)
self.camera.frame.scale(0.5, about_point=ORIGIN)
plane = NumberPlane(
x_radius=15,
y_radius=25,
y_unit_size=0.5,
secondary_line_ratio=0,
)
plane.next_to(ORIGIN, UP, buff=0.001)
horizontal_lines = VGroup(*[l for l in list(planes) + [plane.axes[0]] if np.abs(l.get_center()[0]) < 0.1])
plane.set_stroke(MAROON_B, width=2)
horizontal_lines.set_stroke(BLUE, width=2)
self.prepare_for_transformation(plane)
self.add_transformable_mobjects(plane)
self.background.set_stroke(width=2)
for label in self.background.coordinate_labels:
label.set_stroke(width=0)
label.scale(0.75, about_edge=UR)
words = TextMobject("(Idealized) Flow \\\\", "near a wall")
words.scale(0.75)
words.add_background_rectangle_to_submobjects()
words.next_to(0.75 * UP, LEFT, MED_LARGE_BUFF)
equation = TexMobject("z \\rightarrow z^{1/2}")
equation.scale(0.75)
equation.add_background_rectangle()
equation.next_to(words, UP)
self.apply_complex_function(
lambda x: x**(1. / 2),
added_anims=[Write(equation)]
)
self.play(Write(words, run_time=1))
def func(point):
z = R3_to_complex(point)
d_half = derivative(lambda z: z**2)
return complex_to_R3(d_half(z).conjugate())
stream_lines = StreamLines(
func,
start_points_generator_config={
"x_min": 0.01,
"y_min": 0.01,
"delta_x": 0.125,
"delta_y": 0.125,
},
virtual_time=3,
stroke_width=2,
max_magnitude=10,
)
stream_line_animation = AnimatedStreamLines(stream_lines)
self.add(stream_line_animation)
self.wait(7)
class IncmpressibleAndIrrotational(Scene):
def construct(self):
div_0 = TextMobject("div$\\textbf{F} = 0$")
curl_0 = TextMobject("curl$\\textbf{F}$ = 0")
incompressible = TextMobject("Incompressible")
irrotational = TextMobject("Irrotational")
for text in [div_0, curl_0, incompressible, irrotational]:
self.stylize_word_for_background(text)
div_0.to_edge(UP)
curl_0.next_to(div_0, DOWN, MED_LARGE_BUFF)
for op, word in (div_0, incompressible), (curl_0, irrotational):
op.generate_target()
group = VGroup(op.target, word)
group.arrange(RIGHT, buff=MED_LARGE_BUFF)
group.move_to(op)
self.play(FadeInFromDown(div_0))
self.play(FadeInFromDown(curl_0))
self.wait()
self.play(
MoveToTarget(div_0),
FadeInFromDown(incompressible),
)
self.wait()
self.play(
MoveToTarget(curl_0),
FadeInFromDown(irrotational),
)
self.wait()
rect = SurroundingRectangle(VGroup(curl_0, irrotational))
question = TextMobject("Does this actually happen?")
question.next_to(rect, DOWN)
question.match_color(rect)
self.stylize_word_for_background(question)
self.play(ShowCreation(rect))
self.play(Write(question))
self.wait()
def stylize_word_for_background(self, word):
word.add_background_rectangle()
class NoChargesOverlay(Scene):
def construct(self):
rect = FullScreenFadeRectangle()
rect.set_fill(BLUE_D, 0.75)
circle = Circle(radius=1.5, num_anchors=5000)
circle.rotate(135 * DEGREES)
rect.add_subpath(circle.points)
words = TextMobject("No charges outside wire")
words.scale(1.5)
words.to_edge(UP)
self.add(rect, words)
# End message
class BroughtToYouBy(PiCreatureScene):
CONFIG = {
"pi_creatures_start_on_screen": False,
}
def construct(self):
self.brought_to_you_by()
self.just_you_and_me()
def brought_to_you_by(self):
so_words = TextMobject("So", "...", arg_separator="")
so_words.scale(2)
btyb = TextMobject("Brought to you", "by")
btyb.scale(1.5)
btyb_line = Line(LEFT, RIGHT)
btyb_line.next_to(btyb, RIGHT, SMALL_BUFF)
btyb_line.align_to(btyb[0], DOWN)
btyb_group = VGroup(btyb, btyb_line)
btyb_group.center()
you_word = TextMobject("\\emph{you}")
you_word.set_color(YELLOW)
you_word.scale(1.75)
you_word.move_to(btyb_line)
you_word.align_to(btyb, DOWN)
only_word = TextMobject("(only)")
only_word.scale(1.25)
only_brace = Brace(only_word, DOWN, buff=SMALL_BUFF)
only_group = VGroup(only_word, only_brace)
only_group.next_to(
VGroup(btyb[0][-1], btyb[1][0]), UP, SMALL_BUFF
)
only_group.set_color(RED)
full_group = VGroup(btyb_group, only_group, you_word)
full_group.generate_target()
full_group.target.scale(0.4)
full_group.target.to_corner(UL)
patreon_logo = PatreonLogo()
patreon_logo.scale(0.4)
patreon_logo.next_to(full_group.target, DOWN)
self.play(
Write(so_words[0]),
LaggedStartMap(
DrawBorderThenFill, so_words[1],
run_time=5
),
)
self.play(
so_words.shift, DOWN,
so_words.fade, 1,
remover=True
)
self.play(FadeInFromDown(btyb_group))
self.wait()
self.play(Write(you_word))
self.play(
GrowFromCenter(only_brace),
Write(only_word)
)
self.wait()
self.play(MoveToTarget(
full_group,
rate_func=running_start,
))
self.play(LaggedStartMap(
DrawBorderThenFill, patreon_logo
))
self.wait()
def just_you_and_me(self):
randy, morty = self.pi_creatures
for pi in self.pi_creatures:
pi.change("pondering")
math = TexMobject("\\sum_{n=1}^\\infty \\frac{1}{n^s}")
math.scale(2)
math.move_to(self.pi_creatures)
spiral = Line(0.5 * RIGHT, 0.5 * RIGHT + 70 * UP)
spiral.insert_n_curves(1000)
from old_projects.zeta import zeta
spiral.apply_complex_function(zeta)
step = 0.1
spiral = VGroup(*[
VMobject().pointwise_become_partial(
spiral, a, a + step
)
for a in np.arange(0, 1, step)
])
spiral.set_color_by_gradient(BLUE, YELLOW, RED)
spiral.scale(0.5)
spiral.move_to(math)
self.play(FadeInFromDown(randy))
self.play(FadeInFromDown(morty))
self.play(
Write(math),
randy.change, "hooray",
morty.change, "hooray",
)
self.look_at(math)
self.play(
ShowCreation(spiral, run_time=6, rate_func=linear),
math.scale, 0.5,
math.shift, 3 * UP,
randy.change, "thinking",
morty.change, "thinking",
)
self.play(LaggedStartMap(FadeOut, spiral, run_time=3))
self.wait(3)
# Helpers
def create_pi_creatures(self):
randy = Randolph(color=BLUE_C)
randy.to_edge(DOWN).shift(4 * LEFT)
morty = Mortimer()
morty.to_edge(DOWN).shift(4 * RIGHT)
return VGroup(randy, morty)
class ThoughtsOnAds(Scene):
def construct(self):
title = Title(
"Internet advertising",
match_underline_width_to_text=True,
underline_buff=SMALL_BUFF,
)
line = NumberLine(
color=LIGHT_GREY,
x_min=0,
x_max=12,
numbers_with_elongated_ticks=[]
)
line.move_to(DOWN)
arrows = VGroup(Vector(2 * LEFT), Vector(2 * RIGHT))
arrows.arrange(RIGHT, buff=2)
arrows.next_to(line, DOWN)
misaligned = TextMobject("Misaligned")
misaligned.next_to(arrows[0], DOWN)
aligned = TextMobject("Well-aligned")
aligned.next_to(arrows[1], DOWN)
VGroup(arrows[0], misaligned).set_color(RED)
VGroup(arrows[1], aligned).set_color(BLUE)
left_text = TextMobject(
"Any website presented \\\\",
"as a click-maximizing \\\\ slideshow"
)
left_text.scale(0.8)
left_text.next_to(line, UP, buff=MED_LARGE_BUFF)
left_text.to_edge(LEFT)
viewer, brand, creator = vcb = VGroup(
*list(map(TextMobject, ["viewer", "brand", "creator"]))
)
brand.next_to(creator, LEFT, LARGE_BUFF)
viewer.next_to(vcb[1:], UP, LARGE_BUFF)
arrow_config = {
"path_arc": 60 * DEGREES,
"tip_length": 0.15,
}
vcb_arrows = VGroup(*[
VGroup(
Arrow(p1, p2, **arrow_config),
Arrow(p2, p1, **arrow_config),
)
for p1, p2 in [
(creator.get_left(), brand.get_right()),
(brand.get_top(), viewer.get_bottom()),
(viewer.get_bottom(), creator.get_top()),
]
])
vcb_arrows.set_stroke(width=2)
vcb_arrows.set_color(BLUE)
vcb_group = VGroup(vcb, vcb_arrows)
vcb_group.next_to(line, UP, buff=MED_LARGE_BUFF)
vcb_group.to_edge(RIGHT)
knob = RegularPolygon(n=3, start_angle=-90 * DEGREES)
knob.set_height(0.25)
knob.set_stroke(width=0)
knob.set_fill(YELLOW, 1)
knob.move_to(line.get_left(), DOWN)
right_rect = Rectangle(
width=3,
height=0.25,
stroke_color=WHITE,
stroke_width=2,
fill_color=BLUE,
fill_opacity=0.5
)
right_rect.move_to(line, RIGHT)
right_rect_label = Group(
ImageMobject("3b1b_logo", height=1),
TextMobject("(hopefully)").scale(0.8)
)
right_rect_label.arrange(DOWN, buff=SMALL_BUFF)
# TextMobject(
# "Where I hope \\\\ I've been"
# )
right_rect_label.next_to(
right_rect, UP, SMALL_BUFF
)
# right_rect_label.set_color(BLUE)
self.add(title)
self.play(ShowCreation(line))
self.play(
Write(misaligned),
Write(aligned),
*list(map(GrowArrow, arrows)),
run_time=1
)
self.play(
FadeIn(left_text),
FadeInFrom(knob, 2 * RIGHT)
)
self.wait()
self.play(
LaggedStartMap(FadeInFromDown, vcb),
LaggedStartMap(ShowCreation, vcb_arrows),
ApplyMethod(
knob.move_to, line.get_right(), DOWN,
run_time=2
)
)
self.wait(2)
self.play(vcb_group.shift, 2 * UP)
self.play(
DrawBorderThenFill(right_rect),
FadeIn(right_rect_label),
)
self.wait()
class HoldUpPreviousPromo(PiCreatureScene):
CONFIG = {
"default_pi_creature_kwargs": {
"color": GREY_BROWN,
"flip_at_start": True,
},
"default_pi_creature_start_corner": DR,
}
def construct(self):
morty = self.pi_creature
screen_rect = ScreenRectangle(height=5)
screen_rect.to_corner(UL)
self.play(
FadeInFromDown(screen_rect),
morty.change, "raise_right_hand",
)
self.wait(5)
class GoalWrapper(Scene):
def construct(self):
goal = TextMobject(
"Goal: Teach/remind people \\\\ that they love math"
)
goal.to_edge(UP)
self.add(goal)
screen_rect = ScreenRectangle(height=6)
screen_rect.next_to(goal, DOWN)
self.play(ShowCreation(screen_rect))
self.wait()
class PeopleValueGraph(GraphScene):
CONFIG = {
"x_axis_label": "People reached",
"y_axis_label": "Value per person",
"x_min": 0,
"x_max": 12,
"x_axis_width": 11,
"y_max": 8,
"y_axis_height": 5,
"graph_origin": 2 * DOWN + 5 * LEFT,
"riemann_rectangles_config": {
"dx": 0.01,
"stroke_width": 0,
"start_color": GREEN,
"end_color": BLUE,
}
}
def construct(self):
self.setup_axes()
self.tweak_labels()
self.add_curve()
self.comment_on_incentives()
self.change_curve()
def tweak_labels(self):
self.add_foreground_mobjects(self.x_axis_label_mob)
self.y_axis_label_mob.to_edge(LEFT)
def add_curve(self):
graph = self.graph = self.get_graph(
lambda x: 7 * np.exp(-0.5 * x),
)
self.play(
ShowCreation(graph),
rate_func=bezier([0, 0, 1, 1]),
run_time=3
)
def comment_on_incentives(self):
reach_arrow = Vector(5 * RIGHT)
reach_arrow.next_to(
self.x_axis, DOWN,
buff=SMALL_BUFF,
aligned_edge=RIGHT
)
reach_words = TextMobject("Maximize reach?")
reach_words.next_to(reach_arrow, DOWN, buff=SMALL_BUFF)
reach_words.match_color(reach_arrow)
area = self.area = self.get_riemann_rectangles(
self.graph, **self.riemann_rectangles_config
)
area_words = TextMobject("Maximize this area")
area_words.set_color(BLUE)
area_words.move_to(self.coords_to_point(4, 5))
area_arrow = Arrow(
area_words.get_bottom(),
self.coords_to_point(1.5, 2)
)
self.play(GrowArrow(reach_arrow))
self.play(Write(reach_words))
self.wait()
self.play(
LaggedStartMap(DrawBorderThenFill, area),
Animation(self.graph),
Animation(self.axes),
Write(area_words),
GrowArrow(area_arrow),
)
self.wait()
self.area_label_group = VGroup(area_words, area_arrow)
def change_curve(self):
new_graph = self.get_graph(
lambda x: interpolate(
7 * np.exp(-0.01 * x),
7 * np.exp(-3 * x),
smooth(np.clip(x / 5, 0, 1))
)
)
new_area = self.get_riemann_rectangles(
new_graph, **self.riemann_rectangles_config
)
self.play(
Transform(self.area, new_area),
Transform(self.graph, new_graph),
self.area_label_group[0].shift, RIGHT,
Animation(self.area_label_group),
Animation(self.axes),
run_time=4,
)
self.wait()
class DivCurlEndScreen(PatreonEndScreen):
CONFIG = {
"specific_patrons": [
"Juan Benet",
"Keith Smith",
"Chloe Zhou",
"Desmos ",
"Burt Humburg",
"CrypticSwarm",
"Andrew Sachs",
"Devin Scott",
"Akash Kumar",
"Felix Tripier",
"Arthur Zey",
"David Kedmey",
"Ali Yahya",
"Mayank M. Mehrotra",
"Lukas Biewald",
"Yana Chernobilsky",
"Kaustuv DeBiswas",
"Yu Jun",
"Dave Nicponski",
"Damion Kistler",
"Jordan Scales",
"Markus Persson",
"Fela ",
"Fred Ehrsam",
"Randy C. Will",
"Britt Selvitelle",
"Jonathan Wilson",
"Ryan Atallah",
"Joseph John Cox",
"Luc Ritchie",
"Omar Zrien",
"Sindre Reino Trosterud",
"Jeff Straathof",
"Matt Langford",
"Matt Roveto",
"Marek Cirkos",
"Magister Mugit",
"Stevie Metke",
"Cooper Jones",
"James Hughes",
"John V Wertheim",
"Chris Giddings",
"Song Gao",
"Alexander Feldman",
"Richard Burgmann",
"John Griffith",
"Chris Connett",
"Steven Tomlinson",
"Jameel Syed",
"Bong Choung",
"Ignacio Freiberg",
"Zhilong Yang",
"Giovanni Filippi",
"Eric Younge",
"Prasant Jagannath",
"James H. Park",
"Norton Wang",
"Kevin Le",
"Tianyu Ge",
"David MacCumber",
"Oliver Steele",
"Yaw Etse",
"Dave B",
"Waleed Hamied",
"George Chiesa",
"supershabam ",
"Delton Ding",
"Thomas Tarler",
"1stViewMaths",
"Jacob Magnuson",
"Mark Govea",
"Clark Gaebel",
"Mathias Jansson",
"David Clark",
"Michael Gardner",
"Mads Elvheim",
"Awoo ",
"Dr . David G. Stork",
"Ted Suzman",
"Linh Tran",
"Andrew Busey",
"John Haley",
"Ankalagon ",
"Eric Lavault",
"Boris Veselinovich",
"Julian Pulgarin",
"Jeff Linse",
"Robert Teed",
"Jason Hise",
"Bernd Sing",
"Mustafa Mahdi",
"Mathew Bramson",
"Jerry Ling",
"Sh\\`im\\'in Ku\\=ang",
"Rish Kundalia",
"Achille Brighton",
"Ripta Pasay",
],
}