mirror of
https://github.com/3b1b/manim.git
synced 2025-07-31 14:03:59 +08:00
1732 lines
53 KiB
Python
1732 lines
53 KiB
Python
from big_ol_pile_of_manim_imports import *
|
|
|
|
DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED]
|
|
|
|
# Quick note to anyone coming to this file with the
|
|
# intent of recreating animations from the video. Some
|
|
# of these, espeically those involving StreamLineAnimation,
|
|
# 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.
|
|
|
|
|
|
# Helper functions
|
|
def get_flow_start_points(x_min=-8, x_max=8,
|
|
y_min=-5, y_max=5,
|
|
delta_x=0.5, delta_y=0.5,
|
|
n_repeats=1,
|
|
noise_factor=None
|
|
):
|
|
if noise_factor is None:
|
|
noise_factor = delta_y / 2
|
|
return np.array([
|
|
x * RIGHT + y * UP + noise_factor * np.random.random(3)
|
|
for n in xrange(n_repeats)
|
|
for x in np.arange(x_min, x_max + delta_x, delta_x)
|
|
for y in np.arange(y_min, y_max + delta_y, delta_y)
|
|
])
|
|
|
|
|
|
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-1):
|
|
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 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 np.linalg.norm(cylinder_flow_vector_field(point))
|
|
|
|
|
|
def get_colored_background_image(scalar_field_func,
|
|
number_to_rgb_func,
|
|
pixel_height=DEFAULT_PIXEL_HEIGHT,
|
|
pixel_width=DEFAULT_PIXEL_WIDTH,
|
|
):
|
|
ph = pixel_height
|
|
pw = pixel_width
|
|
fw = FRAME_WIDTH
|
|
fh = FRAME_HEIGHT
|
|
points_array = np.zeros((ph, pw, 3))
|
|
x_array = np.linspace(-fw / 2, fw / 2, pw)
|
|
x_array = x_array.reshape((1, len(x_array)))
|
|
x_array = x_array.repeat(ph, axis=0)
|
|
|
|
y_array = np.linspace(fh / 2, -fh / 2, ph)
|
|
y_array = y_array.reshape((len(y_array), 1))
|
|
y_array.repeat(pw, axis=1)
|
|
points_array[:, :, 0] = x_array
|
|
points_array[:, :, 1] = y_array
|
|
scalars = np.apply_along_axis(scalar_field_func, 2, points_array)
|
|
rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3))
|
|
return Image.fromarray((rgb_array * 255).astype('uint8'))
|
|
|
|
|
|
def get_rgb_gradient_function(min_value=0, max_value=1,
|
|
colors=[BLUE, RED],
|
|
flip_alphas=True, # Why?
|
|
):
|
|
rgbs = np.array(map(color_to_rgb, colors))
|
|
|
|
def func(values):
|
|
alphas = inverse_interpolate(min_value, max_value, values)
|
|
alphas = np.clip(alphas, 0, 1)
|
|
# if flip_alphas:
|
|
# alphas = 1 - alphas
|
|
scaled_alphas = alphas * (len(rgbs) - 1)
|
|
indices = scaled_alphas.astype(int)
|
|
next_indices = np.clip(indices + 1, 0, len(rgbs) - 1)
|
|
inter_alphas = scaled_alphas % 1
|
|
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
|
|
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
|
|
return result
|
|
|
|
return func
|
|
|
|
|
|
def get_color_field_image_file(scalar_func,
|
|
min_value=0, max_value=2,
|
|
colors=DEFAULT_SCALAR_FIELD_COLORS
|
|
):
|
|
# try_hash
|
|
np.random.seed(0)
|
|
sample_inputs = 5 * np.random.random(size=(10, 3)) - 10
|
|
sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs)
|
|
func_hash = hash(
|
|
str(min_value) + str(max_value) + str(colors) + str(sample_outputs)
|
|
)
|
|
file_name = "%d.png" % func_hash
|
|
full_path = os.path.join(RASTER_IMAGE_DIR, file_name)
|
|
if not os.path.exists(full_path):
|
|
print "Rendering color field image " + str(func_hash)
|
|
rgb_gradient_func = get_rgb_gradient_function(
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
colors=colors
|
|
)
|
|
image = get_colored_background_image(scalar_func, rgb_gradient_func)
|
|
image.save(full_path)
|
|
return full_path
|
|
|
|
|
|
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 = np.linalg.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 = np.linalg.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_chraged_particle(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.scale_to_fit_width(0.5 * result.get_width())
|
|
sign.move_to(result)
|
|
result.add(sign)
|
|
return result
|
|
|
|
|
|
def get_proton(radius=0.1):
|
|
return get_chraged_particle(RED, "+", radius)
|
|
|
|
|
|
def get_electron(radius=0.05):
|
|
return get_chraged_particle(BLUE, "-", radius)
|
|
|
|
|
|
# Mobjects
|
|
|
|
|
|
class StreamLines(VGroup):
|
|
CONFIG = {
|
|
"start_points_generator": get_flow_start_points,
|
|
"start_points_generator_config": {},
|
|
"dt": 0.05,
|
|
"virtual_time": 3,
|
|
"n_anchors_per_line": 100,
|
|
"stroke_width": 1,
|
|
"stroke_color": WHITE,
|
|
"color_lines_by_magnitude": True,
|
|
"min_magnitude": 0.5,
|
|
"max_magnitude": 1.5,
|
|
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
|
"cutoff_norm": 15,
|
|
}
|
|
|
|
def __init__(self, func, **kwargs):
|
|
VGroup.__init__(self, **kwargs)
|
|
self.func = func
|
|
dt = self.dt
|
|
|
|
start_points = self.start_points_generator(
|
|
**self.start_points_generator_config
|
|
)
|
|
for point in start_points:
|
|
points = [point]
|
|
for t in np.arange(0, self.virtual_time, dt):
|
|
last_point = points[-1]
|
|
points.append(last_point + dt * func(last_point))
|
|
if np.linalg.norm(last_point) > self.cutoff_norm:
|
|
break
|
|
line = VMobject()
|
|
step = max(1, len(points) / self.n_anchors_per_line)
|
|
line.set_points_smoothly(points[::step])
|
|
self.add(line)
|
|
|
|
self.set_stroke(self.stroke_color, self.stroke_width)
|
|
|
|
if self.color_lines_by_magnitude:
|
|
image_file = get_color_field_image_file(
|
|
lambda p: np.linalg.norm(func(p)),
|
|
min_value=self.min_magnitude,
|
|
max_value=self.max_magnitude,
|
|
colors=self.colors,
|
|
)
|
|
self.color_using_background_image(image_file)
|
|
|
|
|
|
class VectorField(VGroup):
|
|
CONFIG = {
|
|
"delta_x": 0.5,
|
|
"delta_y": 0.5,
|
|
"x_min": int(np.floor(-FRAME_WIDTH / 2)),
|
|
"x_max": int(np.ceil(FRAME_WIDTH / 2)),
|
|
"y_min": int(np.floor(-FRAME_HEIGHT / 2)),
|
|
"y_max": int(np.ceil(FRAME_HEIGHT / 2)),
|
|
"min_magnitude": 0,
|
|
"max_magnitude": 2,
|
|
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
|
# Takes in actual norm, spits out displayed norm
|
|
"length_func": lambda norm: 0.5 * sigmoid(norm),
|
|
"stroke_color": BLACK,
|
|
"stroke_width": 0.5,
|
|
}
|
|
|
|
def __init__(self, func, **kwargs):
|
|
VGroup.__init__(self, **kwargs)
|
|
self.func = func
|
|
self.rgb_gradient_function = get_rgb_gradient_function(
|
|
self.min_magnitude,
|
|
self.max_magnitude,
|
|
self.colors,
|
|
flip_alphas=False
|
|
)
|
|
for x in np.arange(self.x_min, self.x_max, self.delta_x):
|
|
for y in np.arange(self.y_min, self.y_max, self.delta_y):
|
|
point = x * RIGHT + y * UP
|
|
self.add(self.get_vector(point))
|
|
|
|
def get_vector(self, point):
|
|
output = np.array(self.func(point))
|
|
norm = np.linalg.norm(output)
|
|
if norm == 0:
|
|
output *= 0
|
|
else:
|
|
output *= self.length_func(norm) / norm
|
|
vect = Vector(output)
|
|
vect.shift(point)
|
|
vect.set_fill(rgb_to_color(
|
|
self.rgb_gradient_function(np.array([norm]))[0]
|
|
))
|
|
vect.set_stroke(
|
|
self.stroke_color,
|
|
self.stroke_width
|
|
)
|
|
return vect
|
|
|
|
|
|
# Continual animations
|
|
|
|
|
|
class VectorFieldFlow(ContinualAnimation):
|
|
CONFIG = {
|
|
"mode": None,
|
|
}
|
|
|
|
def __init__(self, mobject, func, **kwargs):
|
|
"""
|
|
Func should take in a vector in R3, and output a vector in R3
|
|
"""
|
|
self.func = func
|
|
ContinualAnimation.__init__(self, mobject, **kwargs)
|
|
|
|
def update_mobject(self, dt):
|
|
self.apply_nudge(dt)
|
|
|
|
def apply_nudge(self):
|
|
self.mobject.shift(self.func(self.mobject.get_center()) * dt)
|
|
|
|
|
|
class VectorFieldSubmobjectFlow(VectorFieldFlow):
|
|
def apply_nudge(self, dt):
|
|
for submob in self.mobject:
|
|
submob.shift(self.func(submob.get_center()) * dt)
|
|
|
|
|
|
class VectorFieldPointFlow(VectorFieldFlow):
|
|
def apply_nudge(self, dt):
|
|
self.mobject.apply_function(
|
|
lambda p: p + self.func(p) * dt
|
|
)
|
|
|
|
|
|
# TODO: Make it so that you can have a group of streamlines
|
|
# varying in response to a changing vector field, and still
|
|
# animate the resulting flow
|
|
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
|
CONFIG = {
|
|
"n_segments": 10,
|
|
"time_width": 0.1,
|
|
"remover": True
|
|
}
|
|
|
|
def __init__(self, vmobject, **kwargs):
|
|
digest_config(self, kwargs)
|
|
max_stroke_width = vmobject.get_stroke_width()
|
|
max_time_width = kwargs.pop("time_width", self.time_width)
|
|
AnimationGroup.__init__(self, *[
|
|
ShowPassingFlash(
|
|
vmobject.deepcopy().set_stroke(width=stroke_width),
|
|
time_width=time_width,
|
|
**kwargs
|
|
)
|
|
for stroke_width, time_width in zip(
|
|
np.linspace(0, max_stroke_width, self.n_segments),
|
|
np.linspace(max_time_width, 0, self.n_segments)
|
|
)
|
|
])
|
|
|
|
|
|
class StreamLineAnimation(ContinualAnimation):
|
|
CONFIG = {
|
|
"lag_range": 4,
|
|
"line_anim_class": ShowPassingFlash,
|
|
"line_anim_config": {
|
|
"run_time": 4,
|
|
"rate_func": None,
|
|
"time_width": 0.3,
|
|
},
|
|
}
|
|
|
|
def __init__(self, stream_lines, **kwargs):
|
|
digest_config(self, kwargs)
|
|
self.stream_lines = stream_lines
|
|
group = VGroup()
|
|
for line in stream_lines:
|
|
line.anim = self.line_anim_class(line, **self.line_anim_config)
|
|
line.time = -self.lag_range * random.random()
|
|
group.add(line.anim.mobject)
|
|
ContinualAnimation.__init__(self, group, **kwargs)
|
|
|
|
def update_mobject(self, dt):
|
|
stream_lines = self.stream_lines
|
|
for line in stream_lines:
|
|
line.time += dt
|
|
adjusted_time = max(line.time, 0) % line.anim.run_time
|
|
line.anim.update(adjusted_time / line.anim.run_time)
|
|
|
|
|
|
class JigglingSubmobjects(ContinualAnimation):
|
|
CONFIG = {
|
|
"amplitude": 0.05,
|
|
"jiggles_per_second": 1,
|
|
}
|
|
|
|
def __init__(self, group, **kwargs):
|
|
for submob in group.submobjects:
|
|
submob.jiggling_direction = rotate_vector(
|
|
RIGHT, np.random.random() * TAU,
|
|
)
|
|
submob.jiggling_phase = np.random.random() * TAU
|
|
ContinualAnimation.__init__(self, group, **kwargs)
|
|
|
|
def update_mobject(self, dt):
|
|
for submob in self.mobject.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 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(StreamLineAnimation(
|
|
lines,
|
|
line_anim_class=ShowPassingFlash
|
|
))
|
|
self.wait(10)
|
|
|
|
|
|
class Introduction(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,
|
|
LaggedStart(
|
|
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,
|
|
submobject_mode="all_at_once",
|
|
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 = np.linalg.norm(RIGHT - shift_val)
|
|
movers = VGroup(self.warped_grid, self.unit_circle)
|
|
self.unit_circle.insert_n_anchor_points(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,
|
|
CircleThenFadeAround(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 StreamLineAnimation(
|
|
stream_lines,
|
|
line_anim_class=line_anim_class,
|
|
)
|
|
|
|
|
|
class ElectricField(Introduction, 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_submobjects(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),
|
|
LaggedStart(GrowArrow, vector_field)
|
|
)
|
|
self.remove_foreground_mobjects(self.title, self.func_label)
|
|
self.wait()
|
|
for group, vect in (protons, UP), (electrons, DOWN):
|
|
self.play(LaggedStart(
|
|
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 np.linalg.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(
|
|
LaggedStart(VFadeIn, protons),
|
|
LaggedStart(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)
|
|
|
|
self.play(ShowCreation(
|
|
h_lines,
|
|
run_time=2,
|
|
submobject_mode="all_at_once"
|
|
))
|
|
for x in range(4):
|
|
self.play(LaggedStart(
|
|
ApplyMethod, h_lines,
|
|
lambda m: (m.set_stroke, TEAL, 4),
|
|
rate_func=there_and_back,
|
|
))
|
|
|
|
|
|
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_submobjects(DOWN)
|
|
topics = VGroup(div, curl)
|
|
topics.arrange_submobjects(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 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(LaggedStart(GrowFromCenter, dots))
|
|
self.wait()
|
|
self.play(LaggedStart(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 = StreamLineAnimation(
|
|
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_submobjects(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(LaggedStart(
|
|
MoveToTarget, adjusted,
|
|
run_time=3
|
|
))
|
|
self.wait()
|
|
self.play(LaggedStart(
|
|
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(
|
|
ContinualUpdateFromFunc(vector_field, update_vector_field),
|
|
ContinualUpdateFromTimeFunc(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_submobjects_in_grid(buff=LARGE_BUFF)
|
|
return particles
|
|
|
|
def get_vector_field(self):
|
|
func = get_force_field_func(*zip(
|
|
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 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.scale_to_fit_width(FRAME_WIDTH - 1)
|
|
words.add_background_rectangle()
|
|
self.add(words)
|
|
self.wait()
|
|
|
|
|
|
# TODO: Revisit this
|
|
class FluidFlowAsHillGradient(Introduction, 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,
|
|
},
|
|
"stream_line_config": {
|
|
"start_points_generator_config": {
|
|
"delta_x": 0.125,
|
|
"delta_y": 0.125,
|
|
},
|
|
"virtual_time": 2,
|
|
"n_anchors_per_line": 10,
|
|
"min_magnitude": 0,
|
|
"max_magnitude": 6,
|
|
"stroke_width": 2,
|
|
},
|
|
"stream_line_animation_config": {
|
|
"line_anim_class": ShowPassingFlash,
|
|
},
|
|
"flow_time": 10,
|
|
}
|
|
|
|
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_submobjects_in_grid(
|
|
n_cols=4, buff=3
|
|
)
|
|
for particle in particles:
|
|
particle.shift(
|
|
np.random.normal(0, 1, 3)
|
|
)
|
|
particle.shift(particle.get_center()[2] * IN)
|
|
particle.charge *= random.random()
|
|
vector_field = self.get_vector_field()
|
|
|
|
self.play(
|
|
LaggedStart(GrowArrow, vector_field),
|
|
LaggedStart(GrowFromCenter, particles),
|
|
run_time=4
|
|
)
|
|
self.wait()
|
|
self.play(LaggedStart(FadeOut, particles))
|
|
|
|
def show_flow(self):
|
|
stream_lines = StreamLines(
|
|
self.vector_field.func,
|
|
**self.stream_line_config
|
|
)
|
|
stream_line_animation = StreamLineAnimation(
|
|
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},
|
|
LaggedStart(
|
|
LaggedStart, 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 = ContinualUpdateFromFunc(
|
|
div_tex, lambda m: m.next_to(circle, UP, SMALL_BUFF)
|
|
)
|
|
|
|
div_value = DecimalNumber(
|
|
0,
|
|
include_background_rectangle=True,
|
|
include_sign=True,
|
|
)
|
|
div_value_update = ContinualChangingDecimal(
|
|
div_value,
|
|
lambda a: div_func(circle.get_center()),
|
|
position_update_func=lambda m: m.next_to(div_tex, RIGHT, SMALL_BUFF)
|
|
)
|
|
|
|
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 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 = StreamLineAnimation(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(ContinualUpdateFromFunc(
|
|
# 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 = ContinualUpdateFromFunc(
|
|
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(ContinualUpdateFromFunc(
|
|
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
|
|
)
|
|
|
|
def show_flow():
|
|
stream_lines = get_stream_lines()
|
|
random.shuffle(stream_lines.submobjects)
|
|
self.play(LaggedStart(
|
|
ShowCreationThenDestruction,
|
|
stream_lines,
|
|
remover=True
|
|
))
|
|
|
|
vector_ring = get_vector_ring()
|
|
vector_ring_update = ContinualUpdateFromFunc(
|
|
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(*[
|
|
ContinualUpdateFromFunc(
|
|
mob, lambda m: m.set_fill(None, 0)
|
|
)
|
|
for mob in out_x, out_y
|
|
])
|
|
self.add_foreground_mobjects(div_tex)
|
|
self.play(
|
|
LaggedStart(GrowArrow, vector_field),
|
|
LaggedStart(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:
|
|
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):
|
|
self.add_title()
|
|
self.begin_flow()
|
|
|
|
def add_title(self):
|
|
pass
|
|
|
|
def begin_flow(self):
|
|
pass
|