mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
4291 lines
138 KiB
Python
4291 lines
138 KiB
Python
# -*- coding: utf-8 -*-
|
|
from constants import *
|
|
import scipy.integrate
|
|
|
|
from manimlib.imports import *
|
|
|
|
USE_ALMOST_FOURIER_BY_DEFAULT = True
|
|
NUM_SAMPLES_FOR_FFT = 1000
|
|
DEFAULT_COMPLEX_TO_REAL_FUNC = lambda z : z.real
|
|
|
|
|
|
def get_fourier_graph(
|
|
axes, time_func, t_min, t_max,
|
|
n_samples = NUM_SAMPLES_FOR_FFT,
|
|
complex_to_real_func = lambda z : z.real,
|
|
color = RED,
|
|
):
|
|
# N = n_samples
|
|
# T = time_range/n_samples
|
|
time_range = float(t_max - t_min)
|
|
time_step_size = time_range/n_samples
|
|
time_samples = np.vectorize(time_func)(np.linspace(t_min, t_max, n_samples))
|
|
fft_output = np.fft.fft(time_samples)
|
|
frequencies = np.linspace(0.0, n_samples/(2.0*time_range), n_samples//2)
|
|
# #Cycles per second of fouier_samples[1]
|
|
# (1/time_range)*n_samples
|
|
# freq_step_size = 1./time_range
|
|
graph = VMobject()
|
|
graph.set_points_smoothly([
|
|
axes.coords_to_point(
|
|
x, complex_to_real_func(y)/n_samples,
|
|
)
|
|
for x, y in zip(frequencies, fft_output[:n_samples//2])
|
|
])
|
|
graph.set_color(color)
|
|
f_min, f_max = [
|
|
axes.x_axis.point_to_number(graph.points[i])
|
|
for i in (0, -1)
|
|
]
|
|
graph.underlying_function = lambda f : axes.y_axis.point_to_number(
|
|
graph.point_from_proportion((f - f_min)/(f_max - f_min))
|
|
)
|
|
return graph
|
|
|
|
def get_fourier_transform(
|
|
func, t_min, t_max,
|
|
complex_to_real_func = DEFAULT_COMPLEX_TO_REAL_FUNC,
|
|
use_almost_fourier = USE_ALMOST_FOURIER_BY_DEFAULT,
|
|
**kwargs ##Just eats these
|
|
):
|
|
scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0
|
|
def fourier_transform(f):
|
|
z = scalar*scipy.integrate.quad(
|
|
lambda t : func(t)*np.exp(complex(0, -TAU*f*t)),
|
|
t_min, t_max
|
|
)[0]
|
|
return complex_to_real_func(z)
|
|
return fourier_transform
|
|
|
|
##
|
|
|
|
class Introduction(TeacherStudentsScene):
|
|
def construct(self):
|
|
title = TextMobject("Fourier Transform")
|
|
title.scale(1.2)
|
|
title.to_edge(UP, buff = MED_SMALL_BUFF)
|
|
|
|
func = lambda t : np.cos(2*TAU*t) + np.cos(3*TAU*t)
|
|
graph = FunctionGraph(func, x_min = 0, x_max = 5)
|
|
graph.stretch(0.25, 1)
|
|
graph.next_to(title, DOWN)
|
|
graph.to_edge(LEFT)
|
|
graph.set_color(BLUE)
|
|
fourier_graph = FunctionGraph(
|
|
get_fourier_transform(func, 0, 5),
|
|
x_min = 0, x_max = 5
|
|
)
|
|
fourier_graph.move_to(graph)
|
|
fourier_graph.to_edge(RIGHT)
|
|
fourier_graph.set_color(RED)
|
|
arrow = Arrow(graph, fourier_graph, color = WHITE)
|
|
self.add(title, graph)
|
|
|
|
self.student_thinks(
|
|
"What's that?",
|
|
look_at_arg = title,
|
|
target_mode = "confused",
|
|
student_index = 1,
|
|
)
|
|
self.play(
|
|
GrowArrow(arrow),
|
|
ReplacementTransform(graph.copy(), fourier_graph)
|
|
)
|
|
self.wait(2)
|
|
self.student_thinks(
|
|
"Pssht, I got this",
|
|
target_mode = "tease",
|
|
student_index = 2,
|
|
added_anims = [RemovePiCreatureBubble(self.students[1])]
|
|
)
|
|
self.play(self.teacher.change, "hesitant")
|
|
self.wait(2)
|
|
|
|
class TODOInsertUnmixingSound(TODOStub):
|
|
CONFIG = {
|
|
"message" : "Show unmixing sound"
|
|
}
|
|
|
|
class OtherContexts(PiCreatureScene):
|
|
def construct(self):
|
|
items = VGroup(*list(map(TextMobject, [
|
|
"Extracting frequencies from sound",
|
|
"Uncertainty principle",
|
|
"Riemann Zeta function and primes",
|
|
"Differential equations",
|
|
])))
|
|
items.arrange(
|
|
DOWN, buff = MED_LARGE_BUFF,
|
|
aligned_edge = LEFT
|
|
)
|
|
items.to_corner(UP+LEFT)
|
|
items[1:].set_fill(opacity = 0.2)
|
|
|
|
morty = self.pi_creature
|
|
morty.to_corner(UP+RIGHT)
|
|
|
|
self.add(items)
|
|
for item in items[1:]:
|
|
self.play(
|
|
LaggedStartMap(
|
|
ApplyMethod, item,
|
|
lambda m : (m.set_fill, {"opacity" : 1}),
|
|
),
|
|
morty.change, "thinking",
|
|
)
|
|
self.wait()
|
|
|
|
class TODOInsertCosineWrappingAroundCircle(TODOStub):
|
|
CONFIG = {
|
|
"message" : "Give a picture-in-picture \\\\ of cosine wrapping around circle",
|
|
}
|
|
|
|
class AddingPureFrequencies(PiCreatureScene):
|
|
CONFIG = {
|
|
"A_frequency" : 2.1,
|
|
"A_color" : YELLOW,
|
|
"D_color" : PINK,
|
|
"F_color" : TEAL,
|
|
"C_color" : RED,
|
|
"sum_color" : GREEN,
|
|
"equilibrium_height" : 1.5,
|
|
}
|
|
def construct(self):
|
|
self.add_speaker()
|
|
self.play_A440()
|
|
self.measure_air_pressure()
|
|
self.play_lower_pitch()
|
|
self.play_mix()
|
|
self.separate_out_parts()
|
|
self.draw_sum_at_single_point()
|
|
self.draw_full_sum()
|
|
self.add_more_notes()
|
|
|
|
def add_speaker(self):
|
|
speaker = SVGMobject(file_name = "speaker")
|
|
speaker.to_edge(DOWN)
|
|
|
|
self.add(speaker)
|
|
self.speaker = speaker
|
|
|
|
def play_A440(self):
|
|
randy = self.pi_creature
|
|
A_label = TextMobject("A440")
|
|
A_label.set_color(self.A_color)
|
|
A_label.next_to(self.speaker, UP)
|
|
|
|
self.broadcast(
|
|
FadeIn(A_label),
|
|
Succession(
|
|
ApplyMethod, randy.change, "pondering",
|
|
Animation, randy,
|
|
Blink, randy
|
|
)
|
|
)
|
|
|
|
self.set_variables_as_attrs(A_label)
|
|
|
|
def measure_air_pressure(self):
|
|
randy = self.pi_creature
|
|
axes = Axes(
|
|
y_min = -2, y_max = 2,
|
|
x_min = 0, x_max = 10,
|
|
number_line_config = {"include_tip" : False},
|
|
)
|
|
axes.stretch_to_fit_height(2)
|
|
axes.to_corner(UP+LEFT)
|
|
axes.shift(LARGE_BUFF*DOWN)
|
|
eh = self.equilibrium_height
|
|
equilibrium_line = DashedLine(
|
|
axes.coords_to_point(0, eh),
|
|
axes.coords_to_point(axes.x_max, eh),
|
|
stroke_width = 2,
|
|
stroke_color = LIGHT_GREY
|
|
)
|
|
|
|
frequency = self.A_frequency
|
|
graph = self.get_wave_graph(frequency, axes)
|
|
func = graph.underlying_function
|
|
graph.set_color(self.A_color)
|
|
pressure = TextMobject("Pressure")
|
|
time = TextMobject("Time")
|
|
for label in pressure, time:
|
|
label.scale_in_place(0.8)
|
|
pressure.next_to(axes.y_axis, UP)
|
|
pressure.to_edge(LEFT, buff = MED_SMALL_BUFF)
|
|
time.next_to(axes.x_axis.get_right(), DOWN+LEFT)
|
|
axes.labels = VGroup(pressure, time)
|
|
|
|
n = 10
|
|
brace = Brace(Line(
|
|
axes.coords_to_point(n/frequency, func(n/frequency)),
|
|
axes.coords_to_point((n+1)/frequency, func((n+1)/frequency)),
|
|
), UP)
|
|
words = brace.get_text("Imagine 440 per second", buff = SMALL_BUFF)
|
|
words.scale(0.8, about_point = words.get_bottom())
|
|
|
|
self.play(
|
|
FadeIn(pressure),
|
|
ShowCreation(axes.y_axis)
|
|
)
|
|
self.play(
|
|
Write(time),
|
|
ShowCreation(axes.x_axis)
|
|
)
|
|
self.broadcast(
|
|
ShowCreation(graph, run_time = 4, rate_func=linear),
|
|
ShowCreation(equilibrium_line),
|
|
)
|
|
axes.add(equilibrium_line)
|
|
self.play(
|
|
randy.change, "erm", graph,
|
|
GrowFromCenter(brace),
|
|
Write(words)
|
|
)
|
|
self.wait()
|
|
graph.save_state()
|
|
self.play(
|
|
FadeOut(brace),
|
|
FadeOut(words),
|
|
VGroup(axes, graph, axes.labels).shift, 0.8*UP,
|
|
graph.fade, 0.85,
|
|
graph.shift, 0.8*UP,
|
|
)
|
|
|
|
graph.saved_state.move_to(graph)
|
|
self.set_variables_as_attrs(axes, A_graph = graph)
|
|
|
|
def play_lower_pitch(self):
|
|
axes = self.axes
|
|
randy = self.pi_creature
|
|
|
|
frequency = self.A_frequency*(2.0/3.0)
|
|
graph = self.get_wave_graph(frequency, axes)
|
|
graph.set_color(self.D_color)
|
|
|
|
D_label = TextMobject("D294")
|
|
D_label.set_color(self.D_color)
|
|
D_label.move_to(self.A_label)
|
|
|
|
self.play(
|
|
FadeOut(self.A_label),
|
|
GrowFromCenter(D_label),
|
|
)
|
|
self.broadcast(
|
|
ShowCreation(graph, run_time = 4, rate_func=linear),
|
|
randy.change, "happy",
|
|
n_circles = 6,
|
|
)
|
|
self.play(randy.change, "confused", graph)
|
|
self.wait(2)
|
|
|
|
self.set_variables_as_attrs(
|
|
D_label,
|
|
D_graph = graph
|
|
)
|
|
|
|
def play_mix(self):
|
|
self.A_graph.restore()
|
|
self.broadcast(
|
|
self.get_broadcast_animation(n_circles = 6),
|
|
self.pi_creature.change, "thinking",
|
|
*[
|
|
ShowCreation(graph, run_time = 4, rate_func=linear)
|
|
for graph in (self.A_graph, self.D_graph)
|
|
]
|
|
)
|
|
self.wait()
|
|
|
|
def separate_out_parts(self):
|
|
axes = self.axes
|
|
speaker = self.speaker
|
|
randy = self.pi_creature
|
|
|
|
A_axes = axes.deepcopy()
|
|
A_graph = self.A_graph
|
|
A_label = self.A_label
|
|
D_axes = axes.deepcopy()
|
|
D_graph = self.D_graph
|
|
D_label = self.D_label
|
|
movers = [A_axes, A_graph, A_label, D_axes, D_graph, D_label]
|
|
for mover in movers:
|
|
mover.generate_target()
|
|
D_target_group = VGroup(D_axes.target, D_graph.target)
|
|
A_target_group = VGroup(A_axes.target, A_graph.target)
|
|
D_target_group.next_to(axes, DOWN, MED_LARGE_BUFF)
|
|
A_target_group.next_to(D_target_group, DOWN, MED_LARGE_BUFF)
|
|
A_label.fade(1)
|
|
A_label.target.next_to(A_graph.target, UP)
|
|
D_label.target.next_to(D_graph.target, UP)
|
|
|
|
self.play(*it.chain(
|
|
list(map(MoveToTarget, movers)),
|
|
[
|
|
ApplyMethod(mob.shift, FRAME_Y_RADIUS*DOWN, remover = True)
|
|
for mob in (randy, speaker)
|
|
]
|
|
))
|
|
self.wait()
|
|
|
|
self.set_variables_as_attrs(A_axes, D_axes)
|
|
|
|
def draw_sum_at_single_point(self):
|
|
axes = self.axes
|
|
A_axes = self.A_axes
|
|
D_axes = self.D_axes
|
|
A_graph = self.A_graph
|
|
D_graph = self.D_graph
|
|
|
|
x = 2.85
|
|
A_line = self.get_A_graph_v_line(x)
|
|
D_line = self.get_D_graph_v_line(x)
|
|
lines = VGroup(A_line, D_line)
|
|
sum_lines = lines.copy()
|
|
sum_lines.generate_target()
|
|
self.stack_v_lines(x, sum_lines.target)
|
|
|
|
top_axes_point = axes.coords_to_point(x, self.equilibrium_height)
|
|
x_point = np.array(top_axes_point)
|
|
x_point[1] = 0
|
|
v_line = Line(UP, DOWN).scale(FRAME_Y_RADIUS).move_to(x_point)
|
|
|
|
self.revert_to_original_skipping_status()
|
|
self.play(GrowFromCenter(v_line))
|
|
self.play(FadeOut(v_line))
|
|
self.play(*list(map(ShowCreation, lines)))
|
|
self.wait()
|
|
self.play(MoveToTarget(sum_lines, path_arc = np.pi/4))
|
|
self.wait(2)
|
|
# self.play(*[
|
|
# Transform(
|
|
# line,
|
|
# VectorizedPoint(axes.coords_to_point(0, self.equilibrium_height)),
|
|
# remover = True
|
|
# )
|
|
# for line, axes in [
|
|
# (A_line, A_axes),
|
|
# (D_line, D_axes),
|
|
# (sum_lines, axes),
|
|
# ]
|
|
# ])
|
|
self.lines_to_fade = VGroup(A_line, D_line, sum_lines)
|
|
|
|
def draw_full_sum(self):
|
|
axes = self.axes
|
|
|
|
def new_func(x):
|
|
result = self.A_graph.underlying_function(x)
|
|
result += self.D_graph.underlying_function(x)
|
|
result -= self.equilibrium_height
|
|
return result
|
|
|
|
sum_graph = axes.get_graph(new_func)
|
|
sum_graph.set_color(self.sum_color)
|
|
thin_sum_graph = sum_graph.copy().fade()
|
|
|
|
A_graph = self.A_graph
|
|
D_graph = self.D_graph
|
|
D_axes = self.D_axes
|
|
|
|
rect = Rectangle(
|
|
height = 2.5*FRAME_Y_RADIUS,
|
|
width = MED_SMALL_BUFF,
|
|
stroke_width = 0,
|
|
fill_color = YELLOW,
|
|
fill_opacity = 0.4
|
|
)
|
|
|
|
self.play(
|
|
ReplacementTransform(A_graph.copy(), thin_sum_graph),
|
|
ReplacementTransform(D_graph.copy(), thin_sum_graph),
|
|
# FadeOut(self.lines_to_fade)
|
|
)
|
|
self.play(
|
|
self.get_graph_line_animation(self.A_axes, self.A_graph),
|
|
self.get_graph_line_animation(self.D_axes, self.D_graph),
|
|
self.get_graph_line_animation(axes, sum_graph.deepcopy()),
|
|
ShowCreation(sum_graph),
|
|
run_time = 15,
|
|
rate_func=linear
|
|
)
|
|
self.remove(thin_sum_graph)
|
|
self.wait()
|
|
for x in 2.85, 3.57:
|
|
rect.move_to(D_axes.coords_to_point(x, 0))
|
|
self.play(GrowFromPoint(rect, rect.get_top()))
|
|
self.wait()
|
|
self.play(FadeOut(rect))
|
|
|
|
self.sum_graph = sum_graph
|
|
|
|
def add_more_notes(self):
|
|
axes = self.axes
|
|
|
|
A_group = VGroup(self.A_axes, self.A_graph, self.A_label)
|
|
D_group = VGroup(self.D_axes, self.D_graph, self.D_label)
|
|
squish_group = VGroup(A_group, D_group)
|
|
squish_group.generate_target()
|
|
squish_group.target.stretch(0.5, 1)
|
|
squish_group.target.next_to(axes, DOWN, buff = -SMALL_BUFF)
|
|
for group in squish_group.target:
|
|
label = group[-1]
|
|
bottom = label.get_bottom()
|
|
label.stretch_in_place(0.5, 0)
|
|
label.move_to(bottom, DOWN)
|
|
|
|
self.play(
|
|
MoveToTarget(squish_group),
|
|
FadeOut(self.lines_to_fade),
|
|
)
|
|
|
|
F_axes = self.D_axes.deepcopy()
|
|
C_axes = self.A_axes.deepcopy()
|
|
VGroup(F_axes, C_axes).next_to(squish_group, DOWN)
|
|
F_graph = self.get_wave_graph(self.A_frequency*4.0/5, F_axes)
|
|
F_graph.set_color(self.F_color)
|
|
C_graph = self.get_wave_graph(self.A_frequency*6.0/5, C_axes)
|
|
C_graph.set_color(self.C_color)
|
|
|
|
F_label = TextMobject("F349")
|
|
C_label = TextMobject("C523")
|
|
for label, graph in (F_label, F_graph), (C_label, C_graph):
|
|
label.scale(0.5)
|
|
label.set_color(graph.get_stroke_color())
|
|
label.next_to(graph, UP, SMALL_BUFF)
|
|
|
|
graphs = VGroup(self.A_graph, self.D_graph, F_graph, C_graph)
|
|
def new_sum_func(x):
|
|
result = sum([
|
|
graph.underlying_function(x) - self.equilibrium_height
|
|
for graph in graphs
|
|
])
|
|
result *= 0.5
|
|
return result + self.equilibrium_height
|
|
new_sum_graph = self.axes.get_graph(
|
|
new_sum_func,
|
|
num_graph_points = 200
|
|
)
|
|
new_sum_graph.set_color(BLUE_C)
|
|
thin_new_sum_graph = new_sum_graph.copy().fade()
|
|
|
|
self.play(*it.chain(
|
|
list(map(ShowCreation, [F_axes, C_axes, F_graph, C_graph])),
|
|
list(map(Write, [F_label, C_label])),
|
|
list(map(FadeOut, [self.sum_graph]))
|
|
))
|
|
self.play(ReplacementTransform(
|
|
graphs.copy(), thin_new_sum_graph
|
|
))
|
|
kwargs = {"rate_func" : None, "run_time" : 10}
|
|
self.play(ShowCreation(new_sum_graph.copy(), **kwargs), *[
|
|
self.get_graph_line_animation(curr_axes, graph, **kwargs)
|
|
for curr_axes, graph in [
|
|
(self.A_axes, self.A_graph),
|
|
(self.D_axes, self.D_graph),
|
|
(F_axes, F_graph),
|
|
(C_axes, C_graph),
|
|
(axes, new_sum_graph),
|
|
]
|
|
])
|
|
self.wait()
|
|
|
|
####
|
|
|
|
def broadcast(self, *added_anims, **kwargs):
|
|
self.play(self.get_broadcast_animation(**kwargs), *added_anims)
|
|
|
|
def get_broadcast_animation(self, **kwargs):
|
|
kwargs["run_time"] = kwargs.get("run_time", 5)
|
|
kwargs["n_circles"] = kwargs.get("n_circles", 10)
|
|
return Broadcast(self.speaker[1], **kwargs)
|
|
|
|
def get_wave_graph(self, frequency, axes):
|
|
tail_len = 3.0
|
|
x_min, x_max = axes.x_min, axes.x_max
|
|
def func(x):
|
|
value = 0.7*np.cos(2*np.pi*frequency*x)
|
|
if x - x_min < tail_len:
|
|
value *= smooth((x-x_min)/tail_len)
|
|
if x_max - x < tail_len:
|
|
value *= smooth((x_max - x )/tail_len)
|
|
return value + self.equilibrium_height
|
|
ngp = 2*(x_max - x_min)*frequency + 1
|
|
graph = axes.get_graph(func, num_graph_points = int(ngp))
|
|
return graph
|
|
|
|
def get_A_graph_v_line(self, x):
|
|
return self.get_graph_v_line(x, self.A_axes, self.A_graph)
|
|
|
|
def get_D_graph_v_line(self, x):
|
|
return self.get_graph_v_line(x, self.D_axes, self.D_graph)
|
|
|
|
def get_graph_v_line(self, x, axes, graph):
|
|
result = Line(
|
|
axes.coords_to_point(x, self.equilibrium_height),
|
|
# axes.coords_to_point(x, graph.underlying_function(x)),
|
|
graph.point_from_proportion(float(x)/axes.x_max),
|
|
color = WHITE,
|
|
buff = 0,
|
|
)
|
|
return result
|
|
|
|
def stack_v_lines(self, x, lines):
|
|
point = self.axes.coords_to_point(x, self.equilibrium_height)
|
|
A_line, D_line = lines
|
|
A_line.shift(point - A_line.get_start())
|
|
D_line.shift(A_line.get_end()-D_line.get_start())
|
|
A_line.set_color(self.A_color)
|
|
D_line.set_color(self.D_color)
|
|
return lines
|
|
|
|
def create_pi_creature(self):
|
|
return Randolph().to_corner(DOWN+LEFT)
|
|
|
|
def get_graph_line_animation(self, axes, graph, **kwargs):
|
|
line = self.get_graph_v_line(0, axes, graph)
|
|
x_max = axes.x_max
|
|
def update_line(line, alpha):
|
|
x = alpha*x_max
|
|
Transform(line, self.get_graph_v_line(x, axes, graph)).update(1)
|
|
return line
|
|
|
|
return UpdateFromAlphaFunc(line, update_line, **kwargs)
|
|
|
|
class BreakApartSum(Scene):
|
|
CONFIG = {
|
|
"frequencies" : [0.5, 1.5, 2, 2.5, 5],
|
|
"equilibrium_height" : 2.0,
|
|
}
|
|
def construct(self):
|
|
self.show_initial_sound()
|
|
self.decompose_sound()
|
|
self.ponder_question()
|
|
|
|
def show_initial_sound(self):
|
|
def func(x):
|
|
return self.equilibrium_height + 0.2*np.sum([
|
|
np.cos(2*np.pi*f*x)
|
|
for f in self.frequencies
|
|
])
|
|
axes = Axes(
|
|
x_min = 0, x_max = 5,
|
|
y_min = -1, y_max = 5,
|
|
x_axis_config = {
|
|
"include_tip" : False,
|
|
"unit_size" : 2.0,
|
|
},
|
|
y_axis_config = {
|
|
"include_tip" : False,
|
|
"unit_size" : 0.5,
|
|
},
|
|
)
|
|
axes.stretch_to_fit_width(FRAME_WIDTH - 2)
|
|
axes.stretch_to_fit_height(3)
|
|
axes.center()
|
|
axes.to_edge(LEFT)
|
|
graph = axes.get_graph(func, num_graph_points = 200)
|
|
graph.set_color(YELLOW)
|
|
|
|
v_line = Line(ORIGIN, 4*UP)
|
|
v_line.move_to(axes.coords_to_point(0, 0), DOWN)
|
|
dot = Dot(color = PINK)
|
|
dot.move_to(graph.point_from_proportion(0))
|
|
|
|
self.add(axes, graph)
|
|
self.play(
|
|
v_line.move_to, axes.coords_to_point(5, 0), DOWN,
|
|
MoveAlongPath(dot, graph),
|
|
run_time = 8,
|
|
rate_func=linear,
|
|
)
|
|
self.play(*list(map(FadeOut, [dot, v_line])))
|
|
|
|
self.set_variables_as_attrs(axes, graph)
|
|
|
|
def decompose_sound(self):
|
|
axes, graph = self.axes, self.graph
|
|
|
|
pure_graphs = VGroup(*[
|
|
axes.get_graph(
|
|
lambda x : 0.5*np.cos(2*np.pi*freq*x),
|
|
num_graph_points = 100,
|
|
)
|
|
for freq in self.frequencies
|
|
])
|
|
pure_graphs.set_color_by_gradient(BLUE, RED)
|
|
pure_graphs.arrange(DOWN, buff = MED_LARGE_BUFF)
|
|
h_line = DashedLine(6*LEFT, 6*RIGHT)
|
|
|
|
self.play(
|
|
FadeOut(axes),
|
|
graph.to_edge, UP
|
|
)
|
|
pure_graphs.next_to(graph, DOWN, LARGE_BUFF)
|
|
h_line.next_to(graph, DOWN, MED_LARGE_BUFF)
|
|
self.play(ShowCreation(h_line))
|
|
for pure_graph in reversed(pure_graphs):
|
|
self.play(ReplacementTransform(graph.copy(), pure_graph))
|
|
self.wait()
|
|
|
|
self.all_graphs = VGroup(graph, h_line, pure_graphs)
|
|
self.pure_graphs = pure_graphs
|
|
|
|
def ponder_question(self):
|
|
all_graphs = self.all_graphs
|
|
pure_graphs = self.pure_graphs
|
|
randy = Randolph()
|
|
randy.to_corner(DOWN+LEFT)
|
|
|
|
self.play(
|
|
FadeIn(randy),
|
|
all_graphs.scale, 0.75,
|
|
all_graphs.to_corner, UP+RIGHT,
|
|
)
|
|
self.play(randy.change, "pondering", all_graphs)
|
|
self.play(Blink(randy))
|
|
rect = SurroundingRectangle(pure_graphs, color = WHITE)
|
|
self.play(
|
|
ShowCreation(rect),
|
|
LaggedStartMap(
|
|
ApplyFunction, pure_graphs,
|
|
lambda g : (lambda m : m.shift(SMALL_BUFF*UP).set_color(YELLOW), g),
|
|
rate_func = wiggle
|
|
)
|
|
)
|
|
self.play(FadeOut(rect))
|
|
self.play(Blink(randy))
|
|
self.wait()
|
|
|
|
class Quadrant(VMobject):
|
|
CONFIG = {
|
|
"radius" : 2,
|
|
"stroke_width": 0,
|
|
"fill_opacity" : 1,
|
|
"density" : 50,
|
|
"density_exp" : 2.0,
|
|
}
|
|
def generate_points(self):
|
|
points = [r*RIGHT for r in np.arange(0, self.radius, 1./self.density)]
|
|
points += [
|
|
self.radius*(np.cos(theta)*RIGHT + np.sin(theta)*UP)
|
|
for theta in np.arange(0, TAU/4, 1./(self.radius*self.density))
|
|
]
|
|
points += [r*UP for r in np.arange(self.radius, 0, -1./self.density)]
|
|
self.set_points_smoothly(points)
|
|
|
|
class UnmixMixedPaint(Scene):
|
|
CONFIG = {
|
|
"colors" : [BLUE, RED, YELLOW, GREEN],
|
|
}
|
|
def construct(self):
|
|
angles = np.arange(4)*np.pi/2
|
|
quadrants = VGroup(*[
|
|
Quadrant().rotate(angle, about_point = ORIGIN).set_color(color)
|
|
for color, angle in zip(self.colors, angles)
|
|
])
|
|
quadrants.add(*it.chain(*[
|
|
quadrants.copy().rotate(angle)
|
|
for angle in np.linspace(0, 0.02*TAU, 10)
|
|
]))
|
|
quadrants.set_fill(opacity = 0.5)
|
|
|
|
mud_color = average_color(*self.colors)
|
|
mud_circle = Circle(radius = 2, stroke_width = 0)
|
|
mud_circle.set_fill(mud_color, 1)
|
|
mud_circle.save_state()
|
|
mud_circle.scale(0)
|
|
|
|
def update_quadrant(quadrant, alpha):
|
|
points = quadrant.get_anchors()
|
|
dt = 0.03 #Hmm, this has no dependency on frame rate...
|
|
norms = np.apply_along_axis(get_norm, 1, points)
|
|
|
|
points[:,0] -= dt*points[:,1]/np.clip(norms, 0.1, np.inf)
|
|
points[:,1] += dt*points[:,0]/np.clip(norms, 0.1, np.inf)
|
|
|
|
new_norms = np.apply_along_axis(get_norm, 1, points)
|
|
new_norms = np.clip(new_norms, 0.001, np.inf)
|
|
radius = np.max(norms)
|
|
multiplier = norms/new_norms
|
|
multiplier = multiplier.reshape((len(multiplier), 1))
|
|
multiplier.repeat(points.shape[1], axis = 1)
|
|
points *= multiplier
|
|
quadrant.set_points_smoothly(points)
|
|
|
|
self.add(quadrants)
|
|
run_time = 30
|
|
self.play(
|
|
*[
|
|
UpdateFromAlphaFunc(quadrant, update_quadrant)
|
|
for quadrant in quadrants
|
|
] + [
|
|
ApplyMethod(mud_circle.restore, rate_func=linear)
|
|
],
|
|
run_time = run_time
|
|
)
|
|
|
|
#Incomplete, and probably not useful
|
|
class MachineThatTreatsOneFrequencyDifferently(Scene):
|
|
def construct(self):
|
|
graph = self.get_cosine_graph(0.5)
|
|
frequency_mob = DecimalNumber(220, num_decimal_places = 0)
|
|
frequency_mob.next_to(graph, UP, buff = MED_LARGE_BUFF)
|
|
|
|
self.graph = graph
|
|
self.frequency_mob = frequency_mob
|
|
self.add(graph, frequency_mob)
|
|
|
|
arrow1, q_marks, arrow2 = group = VGroup(
|
|
Vector(DOWN), TextMobject("???").scale(1.5), Vector(DOWN)
|
|
)
|
|
group.set_color(WHITE)
|
|
group.arrange(DOWN)
|
|
group.next_to(graph, DOWN)
|
|
self.add(group)
|
|
|
|
self.change_graph_frequency(1)
|
|
graph.set_color(GREEN)
|
|
self.wait()
|
|
graph.set_color(YELLOW)
|
|
self.change_graph_frequency(2)
|
|
self.wait()
|
|
|
|
|
|
def change_graph_frequency(self, frequency, run_time = 2):
|
|
graph = self.graph
|
|
frequency_mob = self.frequency_mob
|
|
curr_frequency = graph.frequency
|
|
self.play(
|
|
UpdateFromAlphaFunc(
|
|
graph, self.get_signal_update_func(graph, frequency),
|
|
),
|
|
ChangingDecimal(
|
|
frequency_mob,
|
|
lambda a : 440*interpolate(curr_frequency, frequency, a)
|
|
),
|
|
run_time = run_time,
|
|
)
|
|
graph.frequency = frequency
|
|
|
|
def get_signal_update_func(self, graph, target_frequency):
|
|
curr_frequency = graph.frequency
|
|
def update(graph, alpha):
|
|
frequency = interpolate(curr_frequency, target_frequency, alpha)
|
|
new_graph = self.get_cosine_graph(frequency)
|
|
Transform(graph, new_graph).update(1)
|
|
return graph
|
|
return update
|
|
|
|
def get_cosine_graph(self, frequency, num_steps = 200, color = YELLOW):
|
|
result = FunctionGraph(
|
|
lambda x : 0.5*np.cos(2*np.pi*frequency*x),
|
|
num_steps = num_steps
|
|
)
|
|
result.frequency = frequency
|
|
result.shift(2*UP)
|
|
return result
|
|
|
|
class FourierMachineScene(Scene):
|
|
CONFIG = {
|
|
"time_axes_config" : {
|
|
"x_min" : 0,
|
|
"x_max" : 4.4,
|
|
"x_axis_config" : {
|
|
"unit_size" : 3,
|
|
"tick_frequency" : 0.25,
|
|
"numbers_with_elongated_ticks" : [1, 2, 3],
|
|
},
|
|
"y_min" : 0,
|
|
"y_max" : 2,
|
|
"y_axis_config" : {"unit_size" : 0.8},
|
|
},
|
|
"time_label_t" : 3.4,
|
|
"circle_plane_config" : {
|
|
"x_radius" : 2.1,
|
|
"y_radius" : 2.1,
|
|
"x_unit_size" : 1,
|
|
"y_unit_size" : 1,
|
|
},
|
|
"frequency_axes_config" : {
|
|
"number_line_config" : {
|
|
"color" : TEAL,
|
|
},
|
|
"x_min" : 0,
|
|
"x_max" : 5.0,
|
|
"x_axis_config" : {
|
|
"unit_size" : 1.4,
|
|
"numbers_to_show" : list(range(1, 6)),
|
|
},
|
|
"y_min" : -1.0,
|
|
"y_max" : 1.0,
|
|
"y_axis_config" : {
|
|
"unit_size" : 1.8,
|
|
"tick_frequency" : 0.5,
|
|
"label_direction" : LEFT,
|
|
},
|
|
"color" : TEAL,
|
|
},
|
|
"frequency_axes_box_color" : TEAL_E,
|
|
"text_scale_val" : 0.75,
|
|
"default_graph_config" : {
|
|
"num_graph_points" : 100,
|
|
"color" : YELLOW,
|
|
},
|
|
"equilibrium_height" : 1,
|
|
"default_y_vector_animation_config" : {
|
|
"run_time" : 5,
|
|
"rate_func" : None,
|
|
"remover" : True,
|
|
},
|
|
"default_time_sweep_config" : {
|
|
"rate_func" : None,
|
|
"run_time" : 5,
|
|
},
|
|
"default_num_v_lines_indicating_periods" : 20,
|
|
}
|
|
|
|
def get_time_axes(self):
|
|
time_axes = Axes(**self.time_axes_config)
|
|
time_axes.x_axis.add_numbers()
|
|
time_label = TextMobject("Time")
|
|
intensity_label = TextMobject("Intensity")
|
|
labels = VGroup(time_label, intensity_label)
|
|
for label in labels:
|
|
label.scale(self.text_scale_val)
|
|
time_label.next_to(
|
|
time_axes.coords_to_point(self.time_label_t,0),
|
|
DOWN
|
|
)
|
|
intensity_label.next_to(time_axes.y_axis.get_top(), RIGHT)
|
|
time_axes.labels = labels
|
|
time_axes.add(labels)
|
|
time_axes.to_corner(UP+LEFT)
|
|
self.time_axes = time_axes
|
|
return time_axes
|
|
|
|
def get_circle_plane(self):
|
|
circle_plane = NumberPlane(**self.circle_plane_config)
|
|
circle_plane.to_corner(DOWN+LEFT)
|
|
circle = DashedLine(ORIGIN, TAU*UP).apply_complex_function(np.exp)
|
|
circle.scale(circle_plane.x_unit_size)
|
|
circle.move_to(circle_plane.coords_to_point(0, 0))
|
|
circle_plane.circle = circle
|
|
circle_plane.add(circle)
|
|
circle_plane.fade()
|
|
self.circle_plane = circle_plane
|
|
return circle_plane
|
|
|
|
def get_frequency_axes(self):
|
|
frequency_axes = Axes(**self.frequency_axes_config)
|
|
frequency_axes.x_axis.add_numbers()
|
|
frequency_axes.y_axis.add_numbers(
|
|
*frequency_axes.y_axis.get_tick_numbers()
|
|
)
|
|
box = SurroundingRectangle(
|
|
frequency_axes,
|
|
buff = MED_SMALL_BUFF,
|
|
color = self.frequency_axes_box_color,
|
|
)
|
|
frequency_axes.box = box
|
|
frequency_axes.add(box)
|
|
frequency_axes.to_corner(DOWN+RIGHT, buff = MED_SMALL_BUFF)
|
|
|
|
frequency_label = TextMobject("Frequency")
|
|
frequency_label.scale(self.text_scale_val)
|
|
frequency_label.next_to(
|
|
frequency_axes.x_axis.get_right(), DOWN,
|
|
buff = MED_LARGE_BUFF,
|
|
aligned_edge = RIGHT,
|
|
)
|
|
frequency_axes.label = frequency_label
|
|
frequency_axes.add(frequency_label)
|
|
|
|
self.frequency_axes = frequency_axes
|
|
return frequency_axes
|
|
|
|
def get_time_graph(self, func, **kwargs):
|
|
if not hasattr(self, "time_axes"):
|
|
self.get_time_axes()
|
|
config = dict(self.default_graph_config)
|
|
config.update(kwargs)
|
|
graph = self.time_axes.get_graph(func, **config)
|
|
return graph
|
|
|
|
def get_cosine_wave(self, freq = 1, shift_val = 1, scale_val = 0.9):
|
|
return self.get_time_graph(
|
|
lambda t : shift_val + scale_val*np.cos(TAU*freq*t)
|
|
)
|
|
|
|
def get_fourier_transform_graph(self, time_graph, **kwargs):
|
|
if not hasattr(self, "frequency_axes"):
|
|
self.get_frequency_axes()
|
|
func = time_graph.underlying_function
|
|
t_axis = self.time_axes.x_axis
|
|
t_min = t_axis.point_to_number(time_graph.points[0])
|
|
t_max = t_axis.point_to_number(time_graph.points[-1])
|
|
f_max = self.frequency_axes.x_max
|
|
# result = get_fourier_graph(
|
|
# self.frequency_axes, func, t_min, t_max,
|
|
# **kwargs
|
|
# )
|
|
# too_far_right_point_indices = [
|
|
# i
|
|
# for i, point in enumerate(result.points)
|
|
# if self.frequency_axes.x_axis.point_to_number(point) > f_max
|
|
# ]
|
|
# if too_far_right_point_indices:
|
|
# i = min(too_far_right_point_indices)
|
|
# prop = float(i)/len(result.points)
|
|
# result.pointwise_become_partial(result, 0, prop)
|
|
# return result
|
|
return self.frequency_axes.get_graph(
|
|
get_fourier_transform(func, t_min, t_max, **kwargs),
|
|
color = self.center_of_mass_color,
|
|
**kwargs
|
|
)
|
|
|
|
def get_polarized_mobject(self, mobject, freq = 1.0):
|
|
if not hasattr(self, "circle_plane"):
|
|
self.get_circle_plane()
|
|
polarized_mobject = mobject.copy()
|
|
polarized_mobject.apply_function(lambda p : self.polarize_point(p, freq))
|
|
# polarized_mobject.make_smooth()
|
|
mobject.polarized_mobject = polarized_mobject
|
|
polarized_mobject.frequency = freq
|
|
return polarized_mobject
|
|
|
|
def polarize_point(self, point, freq = 1.0):
|
|
t, y = self.time_axes.point_to_coords(point)
|
|
z = y*np.exp(complex(0, -2*np.pi*freq*t))
|
|
return self.circle_plane.coords_to_point(z.real, z.imag)
|
|
|
|
def get_polarized_animation(self, mobject, freq = 1.0):
|
|
p_mob = self.get_polarized_mobject(mobject, freq = freq)
|
|
def update_p_mob(p_mob):
|
|
Transform(
|
|
p_mob,
|
|
self.get_polarized_mobject(mobject, freq = freq)
|
|
).update(1)
|
|
mobject.polarized_mobject = p_mob
|
|
return p_mob
|
|
return UpdateFromFunc(p_mob, update_p_mob)
|
|
|
|
def animate_frequency_change(self, mobjects, new_freq, **kwargs):
|
|
kwargs["run_time"] = kwargs.get("run_time", 3.0)
|
|
added_anims = kwargs.get("added_anims", [])
|
|
self.play(*[
|
|
self.get_frequency_change_animation(mob, new_freq, **kwargs)
|
|
for mob in mobjects
|
|
] + added_anims)
|
|
|
|
def get_frequency_change_animation(self, mobject, new_freq, **kwargs):
|
|
if not hasattr(mobject, "polarized_mobject"):
|
|
mobject.polarized_mobject = self.get_polarized_mobject(mobject)
|
|
start_freq = mobject.polarized_mobject.frequency
|
|
def update(pm, alpha):
|
|
freq = interpolate(start_freq, new_freq, alpha)
|
|
new_pm = self.get_polarized_mobject(mobject, freq)
|
|
Transform(pm, new_pm).update(1)
|
|
mobject.polarized_mobject = pm
|
|
mobject.polarized_mobject.frequency = freq
|
|
return pm
|
|
return UpdateFromAlphaFunc(mobject.polarized_mobject, update, **kwargs)
|
|
|
|
def get_time_graph_y_vector_animation(self, graph, **kwargs):
|
|
config = dict(self.default_y_vector_animation_config)
|
|
config.update(kwargs)
|
|
vector = Vector(UP, color = WHITE)
|
|
graph_copy = graph.copy()
|
|
x_axis = self.time_axes.x_axis
|
|
x_min = x_axis.point_to_number(graph.points[0])
|
|
x_max = x_axis.point_to_number(graph.points[-1])
|
|
def update_vector(vector, alpha):
|
|
x = interpolate(x_min, x_max, alpha)
|
|
vector.put_start_and_end_on(
|
|
self.time_axes.coords_to_point(x, 0),
|
|
self.time_axes.input_to_graph_point(x, graph_copy)
|
|
)
|
|
return vector
|
|
return UpdateFromAlphaFunc(vector, update_vector, **config)
|
|
|
|
def get_polarized_vector_animation(self, polarized_graph, **kwargs):
|
|
config = dict(self.default_y_vector_animation_config)
|
|
config.update(kwargs)
|
|
vector = Vector(RIGHT, color = WHITE)
|
|
origin = self.circle_plane.coords_to_point(0, 0)
|
|
graph_copy = polarized_graph.copy()
|
|
def update_vector(vector, alpha):
|
|
# Not sure why this is needed, but without smoothing
|
|
# out the alpha like this, the vector would occasionally
|
|
# jump around
|
|
point = center_of_mass([
|
|
graph_copy.point_from_proportion(alpha+d)
|
|
for d in np.linspace(-0.001, 0.001, 5)
|
|
])
|
|
vector.put_start_and_end_on_with_projection(origin, point)
|
|
return vector
|
|
return UpdateFromAlphaFunc(vector, update_vector, **config)
|
|
|
|
def get_vector_animations(self, graph, draw_polarized_graph = True, **kwargs):
|
|
config = dict(self.default_y_vector_animation_config)
|
|
config.update(kwargs)
|
|
anims = [
|
|
self.get_time_graph_y_vector_animation(graph, **config),
|
|
self.get_polarized_vector_animation(graph.polarized_mobject, **config),
|
|
]
|
|
if draw_polarized_graph:
|
|
new_config = dict(config)
|
|
new_config["remover"] = False
|
|
anims.append(ShowCreation(graph.polarized_mobject, **new_config))
|
|
return anims
|
|
|
|
def animate_time_sweep(self, freq, n_repeats = 1, t_max = None, **kwargs):
|
|
added_anims = kwargs.pop("added_anims", [])
|
|
config = dict(self.default_time_sweep_config)
|
|
config.update(kwargs)
|
|
circle_plane = self.circle_plane
|
|
time_axes = self.time_axes
|
|
ctp = time_axes.coords_to_point
|
|
t_max = t_max or time_axes.x_max
|
|
v_line = DashedLine(
|
|
ctp(0, 0), ctp(0, time_axes.y_max),
|
|
stroke_width = 6,
|
|
)
|
|
v_line.set_color(RED)
|
|
|
|
for x in range(n_repeats):
|
|
v_line.move_to(ctp(0, 0), DOWN)
|
|
self.play(
|
|
ApplyMethod(
|
|
v_line.move_to,
|
|
ctp(t_max, 0), DOWN
|
|
),
|
|
self.get_polarized_animation(v_line, freq = freq),
|
|
*added_anims,
|
|
**config
|
|
)
|
|
self.remove(v_line.polarized_mobject)
|
|
self.play(FadeOut(VGroup(v_line, v_line.polarized_mobject)))
|
|
|
|
def get_v_lines_indicating_periods(self, freq, n_lines = None):
|
|
if n_lines is None:
|
|
n_lines = self.default_num_v_lines_indicating_periods
|
|
period = np.divide(1., max(freq, 0.01))
|
|
v_lines = VGroup(*[
|
|
DashedLine(ORIGIN, 1.5*UP).move_to(
|
|
self.time_axes.coords_to_point(n*period, 0),
|
|
DOWN
|
|
)
|
|
for n in range(1, n_lines + 1)
|
|
])
|
|
v_lines.set_stroke(LIGHT_GREY)
|
|
return v_lines
|
|
|
|
def get_period_v_lines_update_anim(self):
|
|
def update_v_lines(v_lines):
|
|
freq = self.graph.polarized_mobject.frequency
|
|
Transform(
|
|
v_lines,
|
|
self.get_v_lines_indicating_periods(freq)
|
|
).update(1)
|
|
return UpdateFromFunc(
|
|
self.v_lines_indicating_periods, update_v_lines
|
|
)
|
|
|
|
class WrapCosineGraphAroundCircle(FourierMachineScene):
|
|
CONFIG = {
|
|
"initial_winding_frequency" : 0.5,
|
|
"signal_frequency" : 3.0,
|
|
}
|
|
def construct(self):
|
|
self.show_initial_signal()
|
|
self.show_finite_interval()
|
|
self.wrap_around_circle()
|
|
self.show_time_sweeps()
|
|
self.compare_two_frequencies()
|
|
self.change_wrapping_frequency()
|
|
|
|
def show_initial_signal(self):
|
|
axes = self.get_time_axes()
|
|
graph = self.get_cosine_wave(freq = self.signal_frequency)
|
|
self.graph = graph
|
|
braces = VGroup(*self.get_peak_braces()[3:6])
|
|
v_lines = VGroup(*[
|
|
DashedLine(
|
|
ORIGIN, 2*UP, color = RED
|
|
).move_to(axes.coords_to_point(x, 0), DOWN)
|
|
for x in (1, 2)
|
|
])
|
|
words = self.get_bps_label()
|
|
words.save_state()
|
|
words.next_to(axes.coords_to_point(1.5, 0), DOWN, MED_LARGE_BUFF)
|
|
|
|
self.add(axes)
|
|
self.play(ShowCreation(graph, run_time = 2, rate_func=linear))
|
|
self.play(
|
|
FadeIn(words),
|
|
LaggedStartMap(FadeIn, braces),
|
|
*list(map(ShowCreation, v_lines))
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
FadeOut(VGroup(braces, v_lines)),
|
|
words.restore,
|
|
)
|
|
self.wait()
|
|
|
|
self.beats_per_second_label = words
|
|
self.graph = graph
|
|
|
|
def show_finite_interval(self):
|
|
axes = self.time_axes
|
|
v_line = DashedLine(
|
|
axes.coords_to_point(0, 0),
|
|
axes.coords_to_point(0, axes.y_max),
|
|
color = RED,
|
|
stroke_width = 6,
|
|
)
|
|
h_line = Line(
|
|
axes.coords_to_point(0, 0),
|
|
axes.coords_to_point(axes.x_max, 0),
|
|
)
|
|
rect = Rectangle(
|
|
stroke_width = 0,
|
|
fill_color = TEAL,
|
|
fill_opacity = 0.5,
|
|
)
|
|
rect.match_height(v_line)
|
|
rect.match_width(h_line, stretch = True)
|
|
rect.move_to(v_line, DOWN+LEFT)
|
|
right_v_line = v_line.copy()
|
|
right_v_line.move_to(rect, RIGHT)
|
|
|
|
rect.save_state()
|
|
rect.stretch(0, 0, about_edge = ORIGIN)
|
|
self.play(rect.restore, run_time = 2)
|
|
self.play(FadeOut(rect))
|
|
for line in v_line, right_v_line:
|
|
self.play(ShowCreation(line))
|
|
self.play(FadeOut(line))
|
|
self.wait()
|
|
|
|
def wrap_around_circle(self):
|
|
graph = self.graph
|
|
freq = self.initial_winding_frequency
|
|
low_freq = freq/3
|
|
polarized_graph = self.get_polarized_mobject(graph, low_freq)
|
|
circle_plane = self.get_circle_plane()
|
|
moving_graph = graph.copy()
|
|
|
|
self.play(ShowCreation(circle_plane, lag_ratio = 0))
|
|
self.play(ReplacementTransform(
|
|
moving_graph,
|
|
polarized_graph,
|
|
run_time = 3,
|
|
path_arc = -TAU/2
|
|
))
|
|
self.animate_frequency_change([graph], freq)
|
|
self.wait()
|
|
pg_copy = polarized_graph.copy()
|
|
self.remove(polarized_graph)
|
|
self.play(pg_copy.fade, 0.75)
|
|
self.play(*self.get_vector_animations(graph), run_time = 15)
|
|
self.remove(pg_copy)
|
|
self.wait()
|
|
|
|
def show_time_sweeps(self):
|
|
freq = self.initial_winding_frequency
|
|
graph = self.graph
|
|
|
|
v_lines = self.get_v_lines_indicating_periods(freq)
|
|
winding_freq_label = self.get_winding_frequency_label()
|
|
|
|
self.animate_time_sweep(
|
|
freq = freq,
|
|
t_max = 4,
|
|
run_time = 6,
|
|
added_anims = [FadeIn(v_lines)]
|
|
)
|
|
self.play(
|
|
FadeIn(winding_freq_label),
|
|
*self.get_vector_animations(graph)
|
|
)
|
|
self.wait()
|
|
|
|
self.v_lines_indicating_periods = v_lines
|
|
|
|
def compare_two_frequencies(self):
|
|
bps_label = self.beats_per_second_label
|
|
wps_label = self.winding_freq_label
|
|
for label in bps_label, wps_label:
|
|
label.rect = SurroundingRectangle(
|
|
label, color = RED
|
|
)
|
|
graph = self.graph
|
|
freq = self.initial_winding_frequency
|
|
braces = self.get_peak_braces(buff = 0)
|
|
|
|
self.play(ShowCreation(bps_label.rect))
|
|
self.play(FadeOut(bps_label.rect))
|
|
self.play(LaggedStartMap(FadeIn, braces, run_time = 3))
|
|
self.play(FadeOut(braces))
|
|
self.play(ShowCreation(wps_label.rect))
|
|
self.play(FadeOut(wps_label.rect))
|
|
self.animate_time_sweep(freq = freq, t_max = 4)
|
|
self.wait()
|
|
|
|
def change_wrapping_frequency(self):
|
|
graph = self.graph
|
|
v_lines = self.v_lines_indicating_periods
|
|
freq_label = self.winding_freq_label[0]
|
|
|
|
count = 0
|
|
for target_freq in [1.23, 0.2, 0.79, 1.55, self.signal_frequency]:
|
|
self.play(
|
|
Transform(
|
|
v_lines,
|
|
self.get_v_lines_indicating_periods(target_freq)
|
|
),
|
|
ChangeDecimalToValue(freq_label, target_freq),
|
|
self.get_frequency_change_animation(graph, target_freq),
|
|
run_time = 4,
|
|
)
|
|
self.wait()
|
|
if count == 2:
|
|
self.play(LaggedStartMap(
|
|
ApplyFunction, v_lines,
|
|
lambda mob : (
|
|
lambda m : m.shift(0.25*UP).set_color(YELLOW),
|
|
mob
|
|
),
|
|
rate_func = there_and_back
|
|
))
|
|
self.animate_time_sweep(target_freq, t_max = 2)
|
|
count += 1
|
|
self.wait()
|
|
self.play(
|
|
*self.get_vector_animations(graph, False),
|
|
run_time = 15
|
|
)
|
|
|
|
##
|
|
|
|
def get_winding_frequency_label(self):
|
|
freq = self.initial_winding_frequency
|
|
winding_freq_label = VGroup(
|
|
DecimalNumber(freq, num_decimal_places=2),
|
|
TextMobject("cycles/second")
|
|
)
|
|
winding_freq_label.arrange(RIGHT)
|
|
winding_freq_label.next_to(
|
|
self.circle_plane, RIGHT, aligned_edge = UP
|
|
)
|
|
self.winding_freq_label = winding_freq_label
|
|
return winding_freq_label
|
|
|
|
def get_peak_braces(self, **kwargs):
|
|
peak_points = [
|
|
self.time_axes.input_to_graph_point(x, self.graph)
|
|
for x in np.arange(0, 3.5, 1./self.signal_frequency)
|
|
]
|
|
return VGroup(*[
|
|
Brace(Line(p1, p2), UP, **kwargs)
|
|
for p1, p2 in zip(peak_points, peak_points[1:])
|
|
])
|
|
|
|
def get_bps_label(self, freq = 3):
|
|
braces = VGroup(*self.get_peak_braces()[freq:2*freq])
|
|
words = TextMobject("%d beats/second"%freq)
|
|
words.set_width(0.9*braces.get_width())
|
|
words.move_to(braces, DOWN)
|
|
return words
|
|
|
|
class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
|
|
CONFIG = {
|
|
"initial_winding_frequency" : 3.0,
|
|
"center_of_mass_color" : RED,
|
|
"center_of_mass_multiple" : 1,
|
|
}
|
|
def construct(self):
|
|
self.remove(self.pi_creature)
|
|
self.setup_graph()
|
|
self.indicate_weight_of_wire()
|
|
self.show_center_of_mass_dot()
|
|
self.change_to_various_frequencies()
|
|
self.introduce_frequency_plot()
|
|
self.draw_full_frequency_plot()
|
|
self.recap_objects_on_screen()
|
|
self.lower_graph()
|
|
self.label_as_almost_fourier()
|
|
|
|
def setup_graph(self):
|
|
self.add(self.get_time_axes())
|
|
self.add(self.get_circle_plane())
|
|
self.graph = self.get_cosine_wave(self.signal_frequency)
|
|
self.add(self.graph)
|
|
self.add(self.get_polarized_mobject(
|
|
self.graph, self.initial_winding_frequency
|
|
))
|
|
self.add(self.get_winding_frequency_label())
|
|
self.beats_per_second_label = self.get_bps_label()
|
|
self.add(self.beats_per_second_label)
|
|
self.v_lines_indicating_periods = self.get_v_lines_indicating_periods(
|
|
self.initial_winding_frequency
|
|
)
|
|
self.add(self.v_lines_indicating_periods)
|
|
self.change_frequency(1.03)
|
|
self.wait()
|
|
|
|
def indicate_weight_of_wire(self):
|
|
graph = self.graph
|
|
pol_graph = graph.polarized_mobject.copy()
|
|
pol_graph.save_state()
|
|
morty = self.pi_creature
|
|
morty.change("raise_right_hand")
|
|
morty.save_state()
|
|
morty.change("plain")
|
|
morty.fade(1)
|
|
|
|
self.play(
|
|
morty.restore,
|
|
pol_graph.scale, 0.5,
|
|
pol_graph.next_to, morty.get_corner(UP+LEFT), UP, -SMALL_BUFF,
|
|
)
|
|
self.play(
|
|
morty.change, "lower_right_hand", pol_graph.get_bottom(),
|
|
pol_graph.shift, 0.45*DOWN,
|
|
rate_func = there_and_back,
|
|
run_time = 2,
|
|
)
|
|
self.wait()
|
|
|
|
metal_wire = pol_graph.copy().set_stroke(LIGHT_GREY)
|
|
self.play(
|
|
ShowCreationThenDestruction(metal_wire),
|
|
run_time = 2,
|
|
)
|
|
self.play(
|
|
pol_graph.restore,
|
|
morty.change, "pondering"
|
|
)
|
|
self.remove(pol_graph)
|
|
|
|
def show_center_of_mass_dot(self):
|
|
color = self.center_of_mass_color
|
|
dot = self.get_center_of_mass_dot()
|
|
dot.save_state()
|
|
arrow = Vector(DOWN+2*LEFT, color = color)
|
|
arrow.next_to(dot.get_center(), UP+RIGHT, buff = SMALL_BUFF)
|
|
dot.move_to(arrow.get_start())
|
|
words = TextMobject("Center of mass")
|
|
words.next_to(arrow.get_start(), RIGHT)
|
|
words.set_color(color)
|
|
|
|
self.play(
|
|
GrowArrow(arrow),
|
|
dot.restore,
|
|
)
|
|
self.play(Write(words))
|
|
self.play(FadeOut(arrow), FadeOut(self.pi_creature))
|
|
self.wait()
|
|
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
self.center_of_mass_label = words
|
|
|
|
def change_to_various_frequencies(self):
|
|
self.change_frequency(
|
|
3.0, run_time = 30,
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
*self.get_vector_animations(self.graph),
|
|
run_time = 15
|
|
)
|
|
|
|
def introduce_frequency_plot(self):
|
|
wps_label = self.winding_freq_label
|
|
wps_label.add_to_back(BackgroundRectangle(wps_label))
|
|
com_label = self.center_of_mass_label
|
|
com_label.add_background_rectangle()
|
|
frequency_axes = self.get_frequency_axes()
|
|
x_coord_label = TextMobject("$x$-coordinate for center of mass")
|
|
x_coord_label.set_color(self.center_of_mass_color)
|
|
x_coord_label.scale(self.text_scale_val)
|
|
x_coord_label.next_to(
|
|
frequency_axes.y_axis.get_top(),
|
|
RIGHT, aligned_edge = UP, buff = LARGE_BUFF
|
|
)
|
|
x_coord_label.add_background_rectangle()
|
|
flower_path = ParametricFunction(
|
|
lambda t : self.circle_plane.coords_to_point(
|
|
np.sin(2*t)*np.cos(t),
|
|
np.sin(2*t)*np.sin(t),
|
|
),
|
|
t_min = 0, t_max = TAU,
|
|
)
|
|
flower_path.move_to(self.center_of_mass_dot)
|
|
|
|
self.play(
|
|
wps_label.move_to, self.circle_plane.get_top(),
|
|
com_label.move_to, self.circle_plane, DOWN,
|
|
)
|
|
self.play(LaggedStartMap(FadeIn, frequency_axes))
|
|
self.wait()
|
|
self.play(MoveAlongPath(
|
|
self.center_of_mass_dot, flower_path,
|
|
run_time = 4,
|
|
))
|
|
self.play(ReplacementTransform(
|
|
com_label.copy(), x_coord_label
|
|
))
|
|
self.wait()
|
|
|
|
self.x_coord_label = x_coord_label
|
|
|
|
def draw_full_frequency_plot(self):
|
|
graph = self.graph
|
|
fourier_graph = self.get_fourier_transform_graph(graph)
|
|
fourier_graph.save_state()
|
|
fourier_graph_update = self.get_fourier_graph_drawing_update_anim(
|
|
fourier_graph
|
|
)
|
|
v_line = DashedLine(
|
|
self.frequency_axes.coords_to_point(0, 0),
|
|
self.frequency_axes.coords_to_point(0, 1),
|
|
stroke_width = 6,
|
|
color = fourier_graph.get_color()
|
|
)
|
|
|
|
self.change_frequency(0.0)
|
|
self.generate_fourier_dot_transform(fourier_graph)
|
|
self.wait()
|
|
self.play(ShowCreation(v_line))
|
|
self.play(
|
|
GrowFromCenter(self.fourier_graph_dot),
|
|
FadeOut(v_line)
|
|
)
|
|
f_max = int(self.frequency_axes.x_max)
|
|
for freq in [0.2, 1.5, 3.0, 4.0, 5.0]:
|
|
fourier_graph.restore()
|
|
self.change_frequency(
|
|
freq,
|
|
added_anims = [fourier_graph_update],
|
|
run_time = 8,
|
|
)
|
|
self.wait()
|
|
self.fourier_graph = fourier_graph
|
|
|
|
def recap_objects_on_screen(self):
|
|
rect = FullScreenFadeRectangle()
|
|
time_group = VGroup(
|
|
self.graph,
|
|
self.time_axes,
|
|
self.beats_per_second_label,
|
|
).copy()
|
|
circle_group = VGroup(
|
|
self.graph.polarized_mobject,
|
|
self.circle_plane,
|
|
self.winding_freq_label,
|
|
self.center_of_mass_label,
|
|
self.center_of_mass_dot,
|
|
).copy()
|
|
frequency_group = VGroup(
|
|
self.fourier_graph,
|
|
self.frequency_axes,
|
|
self.x_coord_label,
|
|
).copy()
|
|
groups = [time_group, circle_group, frequency_group]
|
|
|
|
self.play(FadeIn(rect))
|
|
self.wait()
|
|
for group in groups:
|
|
graph_copy = group[0].copy().set_color(PINK)
|
|
self.play(FadeIn(group))
|
|
self.play(ShowCreation(graph_copy))
|
|
self.play(FadeOut(graph_copy))
|
|
self.wait()
|
|
self.play(FadeOut(group))
|
|
self.wait()
|
|
self.play(FadeOut(rect))
|
|
|
|
def lower_graph(self):
|
|
graph = self.graph
|
|
time_axes = self.time_axes
|
|
shift_vect = time_axes.coords_to_point(0, 1)
|
|
shift_vect -= time_axes.coords_to_point(0, 0)
|
|
fourier_graph = self.fourier_graph
|
|
new_graph = self.get_cosine_wave(
|
|
self.signal_frequency, shift_val = 0
|
|
)
|
|
new_fourier_graph = self.get_fourier_transform_graph(new_graph)
|
|
for mob in graph, time_axes, fourier_graph:
|
|
mob.save_state()
|
|
|
|
new_freq = 0.03
|
|
self.change_frequency(new_freq)
|
|
self.wait()
|
|
self.play(
|
|
time_axes.shift, shift_vect/2,
|
|
graph.shift, -shift_vect/2,
|
|
self.get_frequency_change_animation(
|
|
self.graph, new_freq
|
|
),
|
|
self.center_of_mass_dot_anim,
|
|
self.get_period_v_lines_update_anim(),
|
|
Transform(fourier_graph, new_fourier_graph),
|
|
self.fourier_graph_dot.move_to,
|
|
self.frequency_axes.coords_to_point(new_freq, 0),
|
|
run_time = 2
|
|
)
|
|
self.wait()
|
|
self.remove(self.fourier_graph_dot)
|
|
self.generate_fourier_dot_transform(new_fourier_graph)
|
|
self.change_frequency(3.0, run_time = 15, rate_func=linear)
|
|
self.wait()
|
|
self.play(
|
|
graph.restore,
|
|
time_axes.restore,
|
|
self.get_frequency_change_animation(
|
|
self.graph, 3.0
|
|
),
|
|
self.center_of_mass_dot_anim,
|
|
self.get_period_v_lines_update_anim(),
|
|
fourier_graph.restore,
|
|
Animation(self.fourier_graph_dot),
|
|
run_time = 2
|
|
)
|
|
self.generate_fourier_dot_transform(self.fourier_graph)
|
|
self.wait()
|
|
self.play(FocusOn(self.fourier_graph_dot))
|
|
self.wait()
|
|
|
|
def label_as_almost_fourier(self):
|
|
x_coord_label = self.x_coord_label
|
|
almost_fourier_label = TextMobject(
|
|
"``Almost Fourier Transform''",
|
|
)
|
|
almost_fourier_label.move_to(x_coord_label, UP+LEFT)
|
|
x_coord_label.generate_target()
|
|
x_coord_label.target.next_to(almost_fourier_label, DOWN)
|
|
|
|
self.play(
|
|
MoveToTarget(x_coord_label),
|
|
Write(almost_fourier_label)
|
|
)
|
|
self.wait(2)
|
|
|
|
##
|
|
|
|
def get_center_of_mass_dot(self):
|
|
dot = Dot(
|
|
self.get_pol_graph_center_of_mass(),
|
|
color = self.center_of_mass_color
|
|
)
|
|
self.center_of_mass_dot = dot
|
|
return dot
|
|
|
|
def get_pol_graph_center_of_mass(self):
|
|
pg = self.graph.polarized_mobject
|
|
result = center_of_mass(pg.get_anchors())
|
|
if self.center_of_mass_multiple != 1:
|
|
mult = self.center_of_mass_multiple
|
|
origin = self.circle_plane.coords_to_point(0, 0)
|
|
result = mult*(result - origin) + origin
|
|
return result
|
|
|
|
def generate_fourier_dot_transform(self, fourier_graph):
|
|
self.fourier_graph_dot = Dot(color = WHITE, radius = 0.05)
|
|
def update_dot(dot):
|
|
f = self.graph.polarized_mobject.frequency
|
|
dot.move_to(self.frequency_axes.input_to_graph_point(
|
|
f, fourier_graph
|
|
))
|
|
self.fourier_graph_dot_anim = UpdateFromFunc(
|
|
self.fourier_graph_dot, update_dot
|
|
)
|
|
self.fourier_graph_dot_anim.update(0)
|
|
|
|
def get_fourier_graph_drawing_update_anim(self, fourier_graph):
|
|
fourier_graph_copy = fourier_graph.copy()
|
|
max_freq = self.frequency_axes.x_max
|
|
def update_fourier_graph(fg):
|
|
freq = self.graph.polarized_mobject.frequency
|
|
fg.pointwise_become_partial(
|
|
fourier_graph_copy,
|
|
0, freq/max_freq
|
|
)
|
|
return fg
|
|
self.fourier_graph_drawing_update_anim = UpdateFromFunc(
|
|
fourier_graph, update_fourier_graph
|
|
)
|
|
return self.fourier_graph_drawing_update_anim
|
|
|
|
def generate_center_of_mass_dot_update_anim(self, multiplier = 1):
|
|
origin = self.circle_plane.coords_to_point(0, 0)
|
|
com = self.get_pol_graph_center_of_mass
|
|
self.center_of_mass_dot_anim = UpdateFromFunc(
|
|
self.center_of_mass_dot,
|
|
lambda d : d.move_to(
|
|
multiplier*(com()-origin)+origin
|
|
)
|
|
)
|
|
|
|
def change_frequency(self, new_freq, **kwargs):
|
|
kwargs["run_time"] = kwargs.get("run_time", 3)
|
|
rate_func = kwargs.pop("rate_func", None)
|
|
if rate_func is None:
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
added_anims = kwargs.get("added_anims", [])
|
|
anims = [self.get_frequency_change_animation(self.graph, new_freq)]
|
|
if hasattr(self, "winding_freq_label"):
|
|
freq_label = [
|
|
sm for sm in self.winding_freq_label
|
|
if isinstance(sm, DecimalNumber)
|
|
][0]
|
|
self.add(freq_label)
|
|
anims.append(
|
|
ChangeDecimalToValue(freq_label, new_freq)
|
|
)
|
|
if hasattr(self, "v_lines_indicating_periods"):
|
|
anims.append(self.get_period_v_lines_update_anim())
|
|
if hasattr(self, "center_of_mass_dot"):
|
|
anims.append(self.center_of_mass_dot_anim)
|
|
if hasattr(self, "fourier_graph_dot"):
|
|
anims.append(self.fourier_graph_dot_anim)
|
|
if hasattr(self, "fourier_graph_drawing_update_anim"):
|
|
anims.append(self.fourier_graph_drawing_update_anim)
|
|
for anim in anims:
|
|
anim.rate_func = rate_func
|
|
anims += added_anims
|
|
self.play(*anims, **kwargs)
|
|
|
|
def create_pi_creature(self):
|
|
return Mortimer().to_corner(DOWN+RIGHT)
|
|
|
|
class StudentsHorrifiedAtScene(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.change_student_modes(
|
|
*3*["horrified"],
|
|
look_at_arg = 2*UP + 3*LEFT
|
|
)
|
|
self.wait(4)
|
|
|
|
class AskAboutAlmostFouierName(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.student_says(
|
|
"``Almost'' Fourier transform?",
|
|
target_mode = "sassy"
|
|
)
|
|
self.change_student_modes("confused", "sassy", "confused")
|
|
self.wait()
|
|
self.teacher_says(
|
|
"We'll get to the real \\\\ one in a few minutes",
|
|
added_anims = [self.get_student_changes(*["plain"]*3)]
|
|
)
|
|
self.wait(2)
|
|
|
|
class ShowLowerFrequency(DrawFrequencyPlot):
|
|
CONFIG = {
|
|
"signal_frequency" : 2.0,
|
|
"higher_signal_frequency" : 3.0,
|
|
"lower_signal_color" : PINK,
|
|
}
|
|
def construct(self):
|
|
self.setup_all_axes()
|
|
self.show_lower_frequency_signal()
|
|
self.play_with_lower_frequency_signal()
|
|
|
|
def setup_all_axes(self):
|
|
self.add(self.get_time_axes())
|
|
self.add(self.get_circle_plane())
|
|
self.add(self.get_frequency_axes())
|
|
self.remove(self.pi_creature)
|
|
|
|
def show_lower_frequency_signal(self):
|
|
axes = self.time_axes
|
|
start_graph = self.get_cosine_wave(freq = self.higher_signal_frequency)
|
|
graph = self.get_cosine_wave(
|
|
freq = self.signal_frequency,
|
|
)
|
|
graph.set_color(self.lower_signal_color)
|
|
self.graph = graph
|
|
ratio = float(self.higher_signal_frequency)/self.signal_frequency
|
|
|
|
braces = VGroup(*self.get_peak_braces()[2:4])
|
|
v_lines = VGroup(*[
|
|
DashedLine(ORIGIN, 1.5*UP).move_to(
|
|
axes.coords_to_point(x, 0), DOWN
|
|
)
|
|
for x in (1, 2)
|
|
])
|
|
bps_label = self.get_bps_label(2)
|
|
bps_label.save_state()
|
|
bps_label.next_to(braces, UP, SMALL_BUFF)
|
|
|
|
|
|
# self.add(start_graph)
|
|
self.play(
|
|
start_graph.stretch, ratio, 0, {"about_edge" : LEFT},
|
|
start_graph.set_color, graph.get_color(),
|
|
)
|
|
self.play(FadeOut(start_graph), Animation(graph))
|
|
self.remove(start_graph)
|
|
self.play(
|
|
Write(bps_label),
|
|
LaggedStartMap(FadeIn, braces),
|
|
*list(map(ShowCreation, v_lines)),
|
|
run_time = 1
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
FadeOut(v_lines),
|
|
FadeOut(braces),
|
|
bps_label.restore,
|
|
)
|
|
|
|
def play_with_lower_frequency_signal(self):
|
|
freq = 0.1
|
|
|
|
#Wind up graph
|
|
graph = self.graph
|
|
pol_graph = self.get_polarized_mobject(graph, freq)
|
|
v_lines = self.get_v_lines_indicating_periods(freq)
|
|
self.v_lines_indicating_periods = v_lines
|
|
wps_label = self.get_winding_frequency_label()
|
|
ChangeDecimalToValue(wps_label[0], freq).update(1)
|
|
wps_label.add_to_back(BackgroundRectangle(wps_label))
|
|
wps_label.move_to(self.circle_plane, UP)
|
|
|
|
self.play(
|
|
ReplacementTransform(
|
|
graph.copy(), pol_graph,
|
|
run_time = 2,
|
|
path_arc = -TAU/4,
|
|
),
|
|
FadeIn(wps_label),
|
|
)
|
|
self.change_frequency(freq, run_time = 0)
|
|
self.change_frequency(0.7)
|
|
self.wait()
|
|
|
|
#Show center of mass
|
|
dot = Dot(
|
|
self.get_pol_graph_center_of_mass(),
|
|
color = self.center_of_mass_color
|
|
)
|
|
dot.save_state()
|
|
self.center_of_mass_dot = dot
|
|
com_words = TextMobject("Center of mass")
|
|
com_words.add_background_rectangle()
|
|
com_words.move_to(self.circle_plane, DOWN)
|
|
arrow = Arrow(
|
|
com_words.get_top(),
|
|
dot.get_center(),
|
|
buff = SMALL_BUFF,
|
|
color = self.center_of_mass_color
|
|
)
|
|
dot.move_to(arrow.get_start())
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
|
|
self.play(
|
|
GrowArrow(arrow),
|
|
dot.restore,
|
|
Write(com_words)
|
|
)
|
|
self.wait()
|
|
self.play(*list(map(FadeOut, [arrow, com_words])))
|
|
self.change_frequency(0.0)
|
|
self.wait()
|
|
|
|
#Show fourier graph
|
|
fourier_graph = self.get_fourier_transform_graph(graph)
|
|
fourier_graph_update = self.get_fourier_graph_drawing_update_anim(
|
|
fourier_graph
|
|
)
|
|
x_coord_label = TextMobject(
|
|
"x-coordinate of center of mass"
|
|
)
|
|
x_coord_label.scale(self.text_scale_val)
|
|
x_coord_label.next_to(
|
|
self.frequency_axes.input_to_graph_point(
|
|
self.signal_frequency, fourier_graph
|
|
), UP
|
|
)
|
|
x_coord_label.set_color(self.center_of_mass_color)
|
|
self.generate_fourier_dot_transform(fourier_graph)
|
|
|
|
self.play(Write(x_coord_label))
|
|
self.change_frequency(
|
|
self.signal_frequency,
|
|
run_time = 10,
|
|
rate_func = smooth,
|
|
)
|
|
self.wait()
|
|
self.change_frequency(
|
|
self.frequency_axes.x_max,
|
|
run_time = 15,
|
|
rate_func = smooth,
|
|
)
|
|
self.wait()
|
|
|
|
self.set_variables_as_attrs(
|
|
fourier_graph,
|
|
fourier_graph_update,
|
|
)
|
|
|
|
class MixingUnmixingTODOStub(TODOStub):
|
|
CONFIG = {
|
|
"message" : "Insert mixing and unmixing of signals"
|
|
}
|
|
|
|
class ShowLinearity(DrawFrequencyPlot):
|
|
CONFIG = {
|
|
"high_freq_color": YELLOW,
|
|
"low_freq_color": PINK,
|
|
"sum_color": GREEN,
|
|
"low_freq" : 3.0,
|
|
"high_freq" : 4.0,
|
|
"circle_plane_config" : {
|
|
"x_radius" : 2.5,
|
|
"y_radius" : 2.7,
|
|
"x_unit_size" : 0.8,
|
|
"y_unit_size" : 0.8,
|
|
},
|
|
}
|
|
def construct(self):
|
|
self.remove(self.pi_creature)
|
|
self.show_sum_of_signals()
|
|
self.show_winding_with_sum_graph()
|
|
self.show_vector_rotation()
|
|
|
|
def show_sum_of_signals(self):
|
|
low_freq, high_freq = self.low_freq, self.high_freq
|
|
axes = self.get_time_axes()
|
|
axes_copy = axes.copy()
|
|
low_freq_graph, high_freq_graph = [
|
|
self.get_cosine_wave(
|
|
freq = freq,
|
|
scale_val = 0.5,
|
|
shift_val = 0.55,
|
|
)
|
|
for freq in (low_freq, high_freq)
|
|
]
|
|
sum_graph = self.get_time_graph(
|
|
lambda t : sum([
|
|
low_freq_graph.underlying_function(t),
|
|
high_freq_graph.underlying_function(t),
|
|
])
|
|
)
|
|
VGroup(axes_copy, high_freq_graph).next_to(
|
|
axes, DOWN, MED_LARGE_BUFF
|
|
)
|
|
|
|
low_freq_label = TextMobject("%d Hz"%int(low_freq))
|
|
high_freq_label = TextMobject("%d Hz"%int(high_freq))
|
|
sum_label = TextMobject(
|
|
"%d Hz"%int(low_freq), "+",
|
|
"%d Hz"%int(high_freq)
|
|
)
|
|
trips = [
|
|
(low_freq_label, low_freq_graph, self.low_freq_color),
|
|
(high_freq_label, high_freq_graph, self.high_freq_color),
|
|
(sum_label, sum_graph, self.sum_color),
|
|
]
|
|
for label, graph, color in trips:
|
|
label.next_to(graph, UP)
|
|
graph.set_color(color)
|
|
label.set_color(color)
|
|
sum_label[0].match_color(low_freq_graph)
|
|
sum_label[2].match_color(high_freq_graph)
|
|
|
|
self.add(axes, low_freq_graph)
|
|
self.play(
|
|
FadeIn(axes_copy),
|
|
ShowCreation(high_freq_graph),
|
|
)
|
|
self.play(LaggedStartMap(
|
|
FadeIn, VGroup(high_freq_label, low_freq_label)
|
|
))
|
|
self.wait()
|
|
self.play(
|
|
ReplacementTransform(axes_copy, axes),
|
|
ReplacementTransform(high_freq_graph, sum_graph),
|
|
ReplacementTransform(low_freq_graph, sum_graph),
|
|
ReplacementTransform(
|
|
VGroup(low_freq_label, high_freq_label),
|
|
sum_label
|
|
)
|
|
)
|
|
self.wait()
|
|
self.graph = graph
|
|
|
|
def show_winding_with_sum_graph(self):
|
|
graph = self.graph
|
|
circle_plane = self.get_circle_plane()
|
|
frequency_axes = self.get_frequency_axes()
|
|
pol_graph = self.get_polarized_mobject(graph, freq = 0.0)
|
|
|
|
wps_label = self.get_winding_frequency_label()
|
|
ChangeDecimalToValue(wps_label[0], 0.0).update(1)
|
|
wps_label.add_to_back(BackgroundRectangle(wps_label))
|
|
wps_label.move_to(circle_plane, UP)
|
|
|
|
v_lines = self.get_v_lines_indicating_periods(0.001)
|
|
self.v_lines_indicating_periods = v_lines
|
|
|
|
dot = Dot(
|
|
self.get_pol_graph_center_of_mass(),
|
|
color = self.center_of_mass_color
|
|
)
|
|
self.center_of_mass_dot = dot
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
|
|
fourier_graph = self.get_fourier_transform_graph(graph)
|
|
fourier_graph_update = self.get_fourier_graph_drawing_update_anim(
|
|
fourier_graph
|
|
)
|
|
x_coord_label = TextMobject(
|
|
"x-coordinate of center of mass"
|
|
)
|
|
x_coord_label.scale(self.text_scale_val)
|
|
x_coord_label.next_to(
|
|
self.frequency_axes.input_to_graph_point(
|
|
self.signal_frequency, fourier_graph
|
|
), UP
|
|
)
|
|
x_coord_label.set_color(self.center_of_mass_color)
|
|
almost_fourier_label = TextMobject(
|
|
"``Almost-Fourier transform''"
|
|
)
|
|
|
|
self.generate_fourier_dot_transform(fourier_graph)
|
|
|
|
self.play(LaggedStartMap(
|
|
FadeIn, VGroup(
|
|
circle_plane, wps_label,
|
|
frequency_axes, x_coord_label,
|
|
),
|
|
run_time = 1,
|
|
))
|
|
self.play(
|
|
ReplacementTransform(graph.copy(), pol_graph),
|
|
GrowFromCenter(dot)
|
|
)
|
|
freqs = [
|
|
self.low_freq, self.high_freq,
|
|
self.frequency_axes.x_max
|
|
]
|
|
for freq in freqs:
|
|
self.change_frequency(
|
|
freq,
|
|
run_time = 8,
|
|
rate_func = bezier([0, 0, 1, 1]),
|
|
)
|
|
|
|
def show_vector_rotation(self):
|
|
self.fourier_graph_drawing_update_anim = Animation(Mobject())
|
|
self.change_frequency(self.low_freq)
|
|
self.play(*self.get_vector_animations(
|
|
self.graph, draw_polarized_graph = False,
|
|
run_time = 20,
|
|
))
|
|
self.wait()
|
|
|
|
class ShowCommutativeDiagram(ShowLinearity):
|
|
CONFIG = {
|
|
"time_axes_config" : {
|
|
"x_max" : 1.9,
|
|
"y_max" : 2.0,
|
|
"y_min" : -2.0,
|
|
"y_axis_config" : {
|
|
"unit_size" : 0.5,
|
|
},
|
|
"x_axis_config" : {
|
|
"numbers_to_show" : [1],
|
|
}
|
|
},
|
|
"time_label_t" : 1.5,
|
|
"frequency_axes_config" : {
|
|
"x_min" : 0.0,
|
|
"x_max" : 4.0,
|
|
"y_min" : -0.1,
|
|
"y_max" : 0.5,
|
|
"y_axis_config" : {
|
|
"unit_size" : 1.5,
|
|
"tick_frequency" : 0.5,
|
|
},
|
|
}
|
|
}
|
|
def construct(self):
|
|
self.show_diagram()
|
|
self.point_out_spikes()
|
|
|
|
def show_diagram(self):
|
|
self.remove(self.pi_creature)
|
|
|
|
#Setup axes
|
|
time_axes = self.get_time_axes()
|
|
time_axes.scale(0.8)
|
|
ta_group = VGroup(
|
|
time_axes, time_axes.deepcopy(), time_axes.deepcopy(),
|
|
)
|
|
ta_group.arrange(DOWN, buff = MED_LARGE_BUFF)
|
|
ta_group.to_corner(UP+LEFT, buff = MED_SMALL_BUFF)
|
|
|
|
frequency_axes = Axes(**self.frequency_axes_config)
|
|
frequency_axes.set_color(TEAL)
|
|
freq_label = TextMobject("Frequency")
|
|
freq_label.scale(self.text_scale_val)
|
|
freq_label.next_to(frequency_axes.x_axis, DOWN, SMALL_BUFF, RIGHT)
|
|
frequency_axes.label = freq_label
|
|
frequency_axes.add(freq_label)
|
|
frequency_axes.scale(0.8)
|
|
fa_group = VGroup(
|
|
frequency_axes, frequency_axes.deepcopy(), frequency_axes.deepcopy()
|
|
)
|
|
VGroup(ta_group[1], fa_group[1]).shift(MED_LARGE_BUFF*UP)
|
|
for ta, fa in zip(ta_group, fa_group):
|
|
fa.next_to(
|
|
ta.x_axis, RIGHT,
|
|
submobject_to_align = fa.x_axis
|
|
)
|
|
fa.to_edge(RIGHT)
|
|
ta.remove(ta.labels)
|
|
fa.remove(fa.label)
|
|
|
|
## Add graphs
|
|
funcs = [
|
|
lambda t : np.cos(2*TAU*t),
|
|
lambda t : np.cos(3*TAU*t),
|
|
]
|
|
funcs.append(lambda t : funcs[0](t)+funcs[1](t))
|
|
colors = [
|
|
self.low_freq_color,
|
|
self.high_freq_color,
|
|
self.sum_color,
|
|
]
|
|
labels = [
|
|
TextMobject("2 Hz"),
|
|
TextMobject("3 Hz"),
|
|
# TextMobject("2 Hz", "+", "3 Hz"),
|
|
VectorizedPoint()
|
|
]
|
|
for func, color, label, ta, fa in zip(funcs, colors, labels, ta_group, fa_group):
|
|
time_graph = ta.get_graph(func)
|
|
time_graph.set_color(color)
|
|
label.set_color(color)
|
|
label.scale(0.75)
|
|
label.next_to(time_graph, UP, SMALL_BUFF)
|
|
fourier = get_fourier_transform(
|
|
func, ta.x_min, 4*ta.x_max
|
|
)
|
|
fourier_graph = fa.get_graph(fourier)
|
|
fourier_graph.set_color(self.center_of_mass_color)
|
|
|
|
arrow = Arrow(
|
|
ta.x_axis, fa.x_axis,
|
|
color = WHITE,
|
|
buff = MED_LARGE_BUFF,
|
|
)
|
|
words = TextMobject("Almost-Fourier \\\\ transform")
|
|
words.scale(0.6)
|
|
words.next_to(arrow, UP)
|
|
arrow.words = words
|
|
|
|
ta.graph = time_graph
|
|
ta.graph_label = label
|
|
ta.arrow = arrow
|
|
ta.add(time_graph, label)
|
|
fa.graph = fourier_graph
|
|
fa.add(fourier_graph)
|
|
# labels[-1][0].match_color(labels[0])
|
|
# labels[-1][2].match_color(labels[1])
|
|
|
|
|
|
#Add arrows
|
|
sum_arrows = VGroup()
|
|
for group in ta_group, fa_group:
|
|
arrow = Arrow(
|
|
group[1].graph, group[2].graph,
|
|
color = WHITE,
|
|
buff = SMALL_BUFF
|
|
)
|
|
arrow.scale(0.8, about_edge = UP)
|
|
arrow.words = TextMobject("Sum").scale(0.75)
|
|
arrow.words.next_to(arrow, RIGHT, buff = MED_SMALL_BUFF)
|
|
sum_arrows.add(arrow)
|
|
|
|
def apply_transform(index):
|
|
ta = ta_group[index].deepcopy()
|
|
fa = fa_group[index]
|
|
anims = [
|
|
ReplacementTransform(
|
|
getattr(ta, attr), getattr(fa, attr)
|
|
)
|
|
for attr in ("x_axis", "y_axis", "graph")
|
|
]
|
|
anims += [
|
|
GrowArrow(ta.arrow),
|
|
Write(ta.arrow.words),
|
|
]
|
|
if index == 0:
|
|
anims.append(ReplacementTransform(
|
|
ta.labels[0],
|
|
fa.label
|
|
))
|
|
self.play(*anims, run_time = 1.5)
|
|
|
|
|
|
#Animations
|
|
self.add(*ta_group[:2])
|
|
self.add(ta_group[0].labels)
|
|
self.wait()
|
|
apply_transform(0)
|
|
apply_transform(1)
|
|
self.wait()
|
|
self.play(
|
|
GrowArrow(sum_arrows[1]),
|
|
Write(sum_arrows[1].words),
|
|
*[
|
|
ReplacementTransform(
|
|
fa.copy(), fa_group[2]
|
|
)
|
|
for fa in fa_group[:2]
|
|
]
|
|
)
|
|
self.wait(2)
|
|
self.play(
|
|
GrowArrow(sum_arrows[0]),
|
|
Write(sum_arrows[0].words),
|
|
*[
|
|
ReplacementTransform(
|
|
mob.copy(), ta_group[2],
|
|
run_time = 1
|
|
)
|
|
for mob in ta_group[:2]
|
|
]
|
|
)
|
|
self.wait()
|
|
apply_transform(2)
|
|
self.wait()
|
|
|
|
self.time_axes_group = ta_group
|
|
self.frequency_axes_group = fa_group
|
|
|
|
def point_out_spikes(self):
|
|
fa_group = self.frequency_axes_group
|
|
freqs = self.low_freq, self.high_freq
|
|
flat_rects = VGroup()
|
|
for freq, axes in zip(freqs, fa_group[:2]):
|
|
flat_rect = SurroundingRectangle(axes.x_axis)
|
|
flat_rect.stretch(0.5, 1)
|
|
spike_rect = self.get_spike_rect(axes, freq)
|
|
flat_rect.match_style(spike_rect)
|
|
flat_rect.target = spike_rect
|
|
flat_rects.add(flat_rect)
|
|
|
|
self.play(LaggedStartMap(GrowFromCenter, flat_rects))
|
|
self.wait()
|
|
self.play(LaggedStartMap(MoveToTarget, flat_rects))
|
|
self.wait()
|
|
|
|
sum_spike_rects = VGroup(*[
|
|
self.get_spike_rect(fa_group[2], freq)
|
|
for freq in freqs
|
|
])
|
|
self.play(ReplacementTransform(
|
|
flat_rects, sum_spike_rects
|
|
))
|
|
self.play(LaggedStartMap(
|
|
WiggleOutThenIn, sum_spike_rects,
|
|
run_time = 1,
|
|
lag_ratio = 0.7,
|
|
))
|
|
self.wait()
|
|
|
|
##
|
|
|
|
def get_spike_rect(self, axes, freq):
|
|
peak_point = axes.input_to_graph_point(
|
|
freq, axes.graph
|
|
)
|
|
f_axis_point = axes.coords_to_point(freq, 0)
|
|
line = Line(f_axis_point, peak_point)
|
|
spike_rect = SurroundingRectangle(line)
|
|
spike_rect.set_stroke(width = 0)
|
|
spike_rect.set_fill(YELLOW, 0.5)
|
|
return spike_rect
|
|
|
|
class PauseAndPonder(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.teacher_says(
|
|
"Pause and \\\\ ponder!",
|
|
target_mode = "hooray"
|
|
)
|
|
self.change_student_modes(*["thinking"]*3)
|
|
self.wait(4)
|
|
|
|
class BeforeGettingToTheFullMath(TeacherStudentsScene):
|
|
def construct(self):
|
|
formula = TexMobject(
|
|
"\\hat{g}(f) = \\int_{-\\infty}^{\\infty}" + \
|
|
"g(t)e^{-2\\pi i f t}dt"
|
|
)
|
|
formula.next_to(self.teacher, UP+LEFT)
|
|
|
|
self.play(
|
|
Write(formula),
|
|
self.teacher.change, "raise_right_hand",
|
|
self.get_student_changes(*["confused"]*3)
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
ApplyMethod(
|
|
formula.next_to, FRAME_X_RADIUS*RIGHT, RIGHT,
|
|
path_arc = TAU/16,
|
|
rate_func = running_start,
|
|
),
|
|
self.get_student_changes(*["pondering"]*3)
|
|
)
|
|
self.teacher_says("Consider sound editing\\dots")
|
|
self.wait(3)
|
|
|
|
class FilterOutHighPitch(AddingPureFrequencies, ShowCommutativeDiagram):
|
|
def construct(self):
|
|
self.add_speaker()
|
|
self.play_sound()
|
|
self.show_intensity_vs_time_graph()
|
|
self.take_fourier_transform()
|
|
self.filter_out_high_pitch()
|
|
self.mention_inverse_transform()
|
|
|
|
def play_sound(self):
|
|
randy = self.pi_creature
|
|
|
|
self.play(
|
|
Succession(
|
|
ApplyMethod, randy.look_at, self.speaker,
|
|
Animation, randy,
|
|
ApplyMethod, randy.change, "telepath", randy,
|
|
Animation, randy,
|
|
Blink, randy,
|
|
Animation, randy, {"run_time" : 2},
|
|
),
|
|
*self.get_broadcast_anims(),
|
|
run_time = 7
|
|
)
|
|
self.play(randy.change, "angry", self.speaker)
|
|
self.wait()
|
|
|
|
def show_intensity_vs_time_graph(self):
|
|
randy = self.pi_creature
|
|
|
|
axes = Axes(
|
|
x_min = 0,
|
|
x_max = 12,
|
|
y_min = -6,
|
|
y_max = 6,
|
|
y_axis_config = {
|
|
"unit_size" : 0.15,
|
|
"tick_frequency" : 3,
|
|
}
|
|
)
|
|
axes.set_stroke(width = 2)
|
|
axes.to_corner(UP+LEFT)
|
|
time_label = TextMobject("Time")
|
|
intensity_label = TextMobject("Intensity")
|
|
labels = VGroup(time_label, intensity_label)
|
|
labels.scale(0.75)
|
|
time_label.next_to(
|
|
axes.x_axis, DOWN,
|
|
aligned_edge = RIGHT,
|
|
buff = SMALL_BUFF
|
|
)
|
|
intensity_label.next_to(
|
|
axes.y_axis, RIGHT,
|
|
aligned_edge = UP,
|
|
buff = SMALL_BUFF
|
|
)
|
|
axes.labels = labels
|
|
|
|
func = lambda t : sum([
|
|
np.cos(TAU*f*t)
|
|
for f in (0.5, 0.7, 1.0, 1.2, 3.0,)
|
|
])
|
|
graph = axes.get_graph(func)
|
|
graph.set_color(BLUE)
|
|
|
|
self.play(
|
|
FadeIn(axes),
|
|
FadeIn(axes.labels),
|
|
randy.change, "pondering", axes,
|
|
ShowCreation(
|
|
graph, run_time = 4,
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
),
|
|
*self.get_broadcast_anims(run_time = 6)
|
|
)
|
|
self.wait()
|
|
|
|
self.time_axes = axes
|
|
self.time_graph = graph
|
|
|
|
def take_fourier_transform(self):
|
|
time_axes = self.time_axes
|
|
time_graph = self.time_graph
|
|
randy = self.pi_creature
|
|
speaker = self.speaker
|
|
|
|
frequency_axes = Axes(
|
|
x_min = 0,
|
|
x_max = 3.5,
|
|
x_axis_config = {"unit_size" : 3.5},
|
|
y_min = 0,
|
|
y_max = 1,
|
|
y_axis_config = {"unit_size" : 2},
|
|
)
|
|
frequency_axes.set_color(TEAL)
|
|
frequency_axes.next_to(time_axes, DOWN, LARGE_BUFF, LEFT)
|
|
freq_label = TextMobject("Frequency")
|
|
freq_label.scale(0.75)
|
|
freq_label.next_to(frequency_axes.x_axis, DOWN, MED_SMALL_BUFF, RIGHT)
|
|
frequency_axes.label = freq_label
|
|
|
|
fourier_func = get_fourier_transform(
|
|
time_graph.underlying_function,
|
|
t_min = 0, t_max = 30,
|
|
)
|
|
# def alt_fourier_func(t):
|
|
# bell = smooth(t)*0.3*np.exp(-0.8*(t-0.9)**2)
|
|
# return bell + (smooth(t/3)+0.2)*fourier_func(t)
|
|
fourier_graph = frequency_axes.get_graph(
|
|
fourier_func, num_graph_points = 150,
|
|
)
|
|
fourier_graph.set_color(RED)
|
|
frequency_axes.graph = fourier_graph
|
|
|
|
arrow = Arrow(time_graph, fourier_graph, color = WHITE)
|
|
ft_words = TextMobject("Fourier \\\\ transform")
|
|
ft_words.next_to(arrow, RIGHT)
|
|
|
|
spike_rect = self.get_spike_rect(frequency_axes, 3)
|
|
spike_rect.stretch(2, 0)
|
|
|
|
self.play(
|
|
ReplacementTransform(time_axes.copy(), frequency_axes),
|
|
ReplacementTransform(time_graph.copy(), fourier_graph),
|
|
ReplacementTransform(time_axes.labels[0].copy(), freq_label),
|
|
GrowArrow(arrow),
|
|
Write(ft_words),
|
|
VGroup(randy, speaker).shift, FRAME_Y_RADIUS*DOWN,
|
|
)
|
|
self.remove(randy, speaker)
|
|
self.wait()
|
|
self.play(DrawBorderThenFill(spike_rect))
|
|
self.wait()
|
|
|
|
self.frequency_axes = frequency_axes
|
|
self.fourier_graph = fourier_graph
|
|
self.spike_rect = spike_rect
|
|
self.to_fourier_arrow = arrow
|
|
|
|
def filter_out_high_pitch(self):
|
|
fourier_graph = self.fourier_graph
|
|
spike_rect = self.spike_rect
|
|
frequency_axes = self.frequency_axes
|
|
|
|
def filtered_func(f):
|
|
result = fourier_graph.underlying_function(f)
|
|
result *= np.clip(smooth(3-f), 0, 1)
|
|
return result
|
|
|
|
new_graph = frequency_axes.get_graph(
|
|
filtered_func, num_graph_points = 300
|
|
)
|
|
new_graph.set_color(RED)
|
|
|
|
self.play(spike_rect.stretch, 4, 0)
|
|
self.play(
|
|
Transform(fourier_graph, new_graph),
|
|
spike_rect.stretch, 0.01, 1, {
|
|
"about_point" : frequency_axes.coords_to_point(0, 0)
|
|
},
|
|
run_time = 2
|
|
)
|
|
self.wait()
|
|
|
|
def mention_inverse_transform(self):
|
|
time_axes = self.time_axes
|
|
time_graph = self.time_graph
|
|
fourier_graph = self.fourier_graph
|
|
frequency_axes = self.frequency_axes
|
|
f_min = frequency_axes.x_min
|
|
f_max = frequency_axes.x_max
|
|
|
|
filtered_graph = time_axes.get_graph(
|
|
lambda t : time_graph.underlying_function(t)-np.cos(TAU*3*t)
|
|
)
|
|
filtered_graph.set_color(BLUE_C)
|
|
|
|
to_fourier_arrow = self.to_fourier_arrow
|
|
arrow = to_fourier_arrow.copy()
|
|
arrow.rotate(TAU/2, about_edge = LEFT)
|
|
arrow.shift(MED_SMALL_BUFF*LEFT)
|
|
inv_fourier_words = TextMobject("Inverse Fourier \\\\ transform")
|
|
inv_fourier_words.next_to(arrow, LEFT)
|
|
VGroup(arrow, inv_fourier_words).set_color(MAROON_B)
|
|
|
|
self.play(
|
|
GrowArrow(arrow),
|
|
Write(inv_fourier_words)
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
time_graph.fade, 0.9,
|
|
ReplacementTransform(
|
|
fourier_graph.copy(), filtered_graph
|
|
)
|
|
)
|
|
self.wait()
|
|
|
|
##
|
|
|
|
def get_broadcast_anims(self, run_time = 7, **kwargs):
|
|
return [
|
|
self.get_broadcast_animation(
|
|
n_circles = n,
|
|
run_time = run_time,
|
|
big_radius = 7,
|
|
start_stroke_width = 5,
|
|
**kwargs
|
|
)
|
|
for n in (5, 7, 10, 12)
|
|
]
|
|
|
|
class AskAboutInverseFourier(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.student_says("Inverse Fourier?")
|
|
self.change_student_modes("confused", "raise_right_hand", "confused")
|
|
self.wait(2)
|
|
|
|
class ApplyFourierToFourier(DrawFrequencyPlot):
|
|
CONFIG = {
|
|
"time_axes_config" : {
|
|
"y_min" : -1.5,
|
|
"y_max" : 1.5,
|
|
"x_max" : 5,
|
|
"x_axis_config" : {
|
|
"numbers_to_show" : list(range(1, 5)),
|
|
"unit_size" : 2.5,
|
|
},
|
|
},
|
|
"frequency_axes_config" : {
|
|
"y_min" : -0.6,
|
|
"y_max" : 0.6,
|
|
},
|
|
"circle_plane_config" : {
|
|
"x_radius" : 1.5,
|
|
"y_radius" : 1.35,
|
|
"x_unit_size" : 1.5,
|
|
"y_unit_size" : 1.5,
|
|
},
|
|
"default_num_v_lines_indicating_periods" : 0,
|
|
"signal_frequency" : 2,
|
|
}
|
|
def construct(self):
|
|
self.setup_fourier_display()
|
|
self.swap_graphs()
|
|
|
|
def setup_fourier_display(self):
|
|
self.force_skipping()
|
|
self.setup_graph()
|
|
self.show_center_of_mass_dot()
|
|
self.introduce_frequency_plot()
|
|
self.draw_full_frequency_plot()
|
|
self.time_axes.remove(self.time_axes.labels)
|
|
self.remove(self.beats_per_second_label)
|
|
VGroup(
|
|
self.time_axes, self.graph,
|
|
self.frequency_axes, self.fourier_graph,
|
|
self.x_coord_label,
|
|
self.fourier_graph_dot,
|
|
).to_edge(UP, buff = MED_SMALL_BUFF)
|
|
self.revert_to_original_skipping_status()
|
|
|
|
def swap_graphs(self):
|
|
fourier_graph = self.fourier_graph
|
|
time_graph = self.graph
|
|
wound_up_graph = time_graph.polarized_mobject
|
|
time_axes = self.time_axes
|
|
frequency_axes = self.frequency_axes
|
|
|
|
f_max = self.frequency_axes.x_max
|
|
new_fourier_graph = time_axes.get_graph(
|
|
lambda t : 2*fourier_graph.underlying_function(t)
|
|
)
|
|
new_fourier_graph.match_style(fourier_graph)
|
|
|
|
self.remove(fourier_graph)
|
|
self.play(
|
|
ReplacementTransform(
|
|
fourier_graph.copy(),
|
|
new_fourier_graph
|
|
),
|
|
ApplyMethod(
|
|
time_graph.shift, 3*UP+10*LEFT,
|
|
remover = True,
|
|
),
|
|
)
|
|
self.play(
|
|
wound_up_graph.next_to, FRAME_X_RADIUS*LEFT, LEFT,
|
|
remover = True
|
|
)
|
|
self.wait()
|
|
|
|
self.graph = new_fourier_graph
|
|
wound_up_graph = self.get_polarized_mobject(new_fourier_graph, freq = 0)
|
|
double_fourier_graph = frequency_axes.get_graph(
|
|
lambda t : 0.25*np.cos(TAU*2*t)
|
|
).set_color(PINK)
|
|
self.fourier_graph = double_fourier_graph
|
|
self.remove(self.fourier_graph_dot)
|
|
self.get_fourier_graph_drawing_update_anim(double_fourier_graph)
|
|
self.generate_fourier_dot_transform(double_fourier_graph)
|
|
self.center_of_mass_dot.set_color(PINK)
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
def new_get_pol_graph_center_of_mass():
|
|
result = DrawFrequencyPlot.get_pol_graph_center_of_mass(self)
|
|
result -= self.circle_plane.coords_to_point(0, 0)
|
|
result *= 25
|
|
result += self.circle_plane.coords_to_point(0, 0)
|
|
return result
|
|
self.get_pol_graph_center_of_mass = new_get_pol_graph_center_of_mass
|
|
|
|
self.play(
|
|
ReplacementTransform(self.graph.copy(), wound_up_graph),
|
|
ChangeDecimalToValue(
|
|
self.winding_freq_label[1], 0.0,
|
|
run_time = 0.2,
|
|
)
|
|
)
|
|
self.change_frequency(5.0, run_time = 15, rate_func=linear)
|
|
self.wait()
|
|
|
|
##
|
|
|
|
def get_cosine_wave(self, freq, **kwargs):
|
|
kwargs["shift_val"] = 0
|
|
kwargs["scale_val"] = 1.0
|
|
return DrawFrequencyPlot.get_cosine_wave(self, freq, **kwargs)
|
|
|
|
class WriteComplexExponentialExpression(DrawFrequencyPlot):
|
|
CONFIG = {
|
|
"signal_frequency" : 2.0,
|
|
"default_num_v_lines_indicating_periods" : 0,
|
|
"time_axes_scale_val" : 0.7,
|
|
"initial_winding_frequency" : 0.1,
|
|
"circle_plane_config" : {
|
|
"unit_size" : 2,
|
|
"y_radius" : FRAME_Y_RADIUS+LARGE_BUFF,
|
|
"x_radius" : FRAME_X_RADIUS+LARGE_BUFF
|
|
}
|
|
}
|
|
def construct(self):
|
|
self.remove(self.pi_creature)
|
|
self.setup_plane()
|
|
self.setup_graph()
|
|
self.show_winding_with_both_coordinates()
|
|
self.show_plane_as_complex_plane()
|
|
self.show_eulers_formula()
|
|
self.show_winding_graph_expression()
|
|
self.find_center_of_mass()
|
|
|
|
def setup_plane(self):
|
|
circle_plane = ComplexPlane(**self.circle_plane_config)
|
|
circle_plane.shift(DOWN+LEFT)
|
|
circle = DashedLine(ORIGIN, TAU*UP)
|
|
circle.apply_complex_function(
|
|
lambda z : R3_to_complex(
|
|
circle_plane.number_to_point(np.exp(z))
|
|
)
|
|
)
|
|
circle_plane.add(circle)
|
|
|
|
time_axes = self.get_time_axes()
|
|
time_axes.background_rectangle = BackgroundRectangle(
|
|
time_axes,
|
|
fill_opacity = 0.9,
|
|
buff = MED_SMALL_BUFF,
|
|
)
|
|
time_axes.add_to_back(time_axes.background_rectangle)
|
|
time_axes.scale(self.time_axes_scale_val)
|
|
time_axes.to_corner(UP+LEFT, buff = 0)
|
|
time_axes.set_stroke(color = WHITE, width = 1)
|
|
|
|
self.add(circle_plane)
|
|
self.add(time_axes)
|
|
|
|
self.circle_plane = circle_plane
|
|
self.time_axes = time_axes
|
|
|
|
def setup_graph(self):
|
|
plane = self.circle_plane
|
|
graph = self.graph = self.get_cosine_wave(
|
|
freq = self.signal_frequency,
|
|
scale_val = 0.5,
|
|
shift_val = 0.75,
|
|
)
|
|
freq = self.initial_winding_frequency
|
|
pol_graph = self.get_polarized_mobject(graph, freq = freq)
|
|
wps_label = self.get_winding_frequency_label()
|
|
ChangeDecimalToValue(wps_label[0], freq).update(1)
|
|
wps_label.add_to_back(BackgroundRectangle(wps_label))
|
|
wps_label.next_to(plane.coords_to_point(0, 1), DOWN)
|
|
wps_label.to_edge(LEFT)
|
|
self.get_center_of_mass_dot()
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
|
|
self.add(graph, pol_graph, wps_label)
|
|
self.set_variables_as_attrs(pol_graph, wps_label)
|
|
self.time_axes_group = VGroup(self.time_axes, graph)
|
|
|
|
def show_winding_with_both_coordinates(self):
|
|
com_dot = self.center_of_mass_dot
|
|
plane = self.circle_plane
|
|
v_line = Line(ORIGIN, UP)
|
|
h_line = Line(ORIGIN, RIGHT)
|
|
lines = VGroup(v_line, h_line)
|
|
lines.set_color(PINK)
|
|
def lines_update(lines):
|
|
point = com_dot.get_center()
|
|
x, y = plane.point_to_coords(point)
|
|
h_line.put_start_and_end_on(
|
|
plane.coords_to_point(0, y), point
|
|
)
|
|
v_line.put_start_and_end_on(
|
|
plane.coords_to_point(x, 0), point
|
|
)
|
|
lines_update_anim = Mobject.add_updater(lines, lines_update)
|
|
lines_update_anim.update(0)
|
|
self.add(lines_update_anim)
|
|
|
|
self.change_frequency(
|
|
2.04,
|
|
added_anims = [
|
|
self.center_of_mass_dot_anim,
|
|
],
|
|
run_time = 15,
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
)
|
|
self.wait()
|
|
|
|
self.dot_component_anim = lines_update_anim
|
|
|
|
def show_plane_as_complex_plane(self):
|
|
to_fade = VGroup(
|
|
self.time_axes_group, self.pol_graph, self.wps_label
|
|
)
|
|
plane = self.circle_plane
|
|
dot = self.center_of_mass_dot
|
|
complex_plane_title = TextMobject("Complex plane")
|
|
complex_plane_title.add_background_rectangle()
|
|
complex_plane_title.to_edge(UP)
|
|
coordinate_labels = plane.get_coordinate_labels()
|
|
number_label = DecimalNumber(
|
|
0, include_background_rectangle = True,
|
|
)
|
|
number_label_update_anim = ContinualChangingDecimal(
|
|
number_label,
|
|
lambda a : plane.point_to_number(dot.get_center()),
|
|
position_update_func = lambda l : l.next_to(
|
|
dot, DOWN+RIGHT,
|
|
buff = SMALL_BUFF
|
|
),
|
|
)
|
|
number_label_update_anim.update(0)
|
|
flower_path = ParametricFunction(
|
|
lambda t : plane.coords_to_point(
|
|
np.sin(2*t)*np.cos(t),
|
|
np.sin(2*t)*np.sin(t),
|
|
),
|
|
t_min = 0, t_max = TAU,
|
|
)
|
|
flower_path.move_to(self.center_of_mass_dot)
|
|
|
|
self.play(FadeOut(to_fade))
|
|
self.play(Write(complex_plane_title))
|
|
self.play(Write(coordinate_labels))
|
|
self.wait()
|
|
self.play(FadeIn(number_label))
|
|
self.add(number_label_update_anim)
|
|
self.play(MoveAlongPath(
|
|
dot, flower_path,
|
|
run_time = 10,
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
))
|
|
self.wait()
|
|
self.play(ShowCreation(
|
|
self.pol_graph, run_time = 3,
|
|
))
|
|
self.play(FadeOut(self.pol_graph))
|
|
self.wait()
|
|
self.play(FadeOut(VGroup(
|
|
dot, self.dot_component_anim.mobject, number_label
|
|
)))
|
|
self.remove(self.dot_component_anim)
|
|
self.remove(number_label_update_anim)
|
|
|
|
self.set_variables_as_attrs(
|
|
number_label,
|
|
number_label_update_anim,
|
|
complex_plane_title,
|
|
)
|
|
|
|
def show_eulers_formula(self):
|
|
plane = self.circle_plane
|
|
|
|
ghost_dot = Dot(ORIGIN, fill_opacity = 0)
|
|
def get_t():
|
|
return ghost_dot.get_center()[0]
|
|
def get_circle_point(scalar = 1, t_shift = 0):
|
|
return plane.number_to_point(
|
|
scalar*np.exp(complex(0, get_t()+t_shift))
|
|
)
|
|
vector = Vector(plane.number_to_point(1), color = GREEN)
|
|
exp_base = TexMobject("e").scale(1.3)
|
|
exp_base.add_background_rectangle()
|
|
exp_decimal = DecimalNumber(0, unit = "i", include_background_rectangle = True)
|
|
exp_decimal.scale(0.75)
|
|
VGroup(exp_base, exp_decimal).match_color(vector)
|
|
exp_decimal_update = ContinualChangingDecimal(
|
|
exp_decimal, lambda a : get_t(),
|
|
position_update_func = lambda d : d.move_to(
|
|
exp_base.get_corner(UP+RIGHT), DOWN+LEFT
|
|
)
|
|
)
|
|
exp_base_update = Mobject.add_updater(
|
|
exp_base, lambda e : e.move_to(get_circle_point(
|
|
scalar = 1.1, t_shift = 0.01*TAU
|
|
))
|
|
)
|
|
vector_update = Mobject.add_updater(
|
|
vector, lambda v : v.put_start_and_end_on(
|
|
plane.number_to_point(0), get_circle_point()
|
|
)
|
|
)
|
|
updates = [exp_base_update, exp_decimal_update, vector_update]
|
|
for update in updates:
|
|
update.update(0)
|
|
|
|
#Show initial vector
|
|
self.play(
|
|
GrowArrow(vector),
|
|
FadeIn(exp_base),
|
|
Write(exp_decimal)
|
|
)
|
|
self.add(*updates)
|
|
self.play(ghost_dot.shift, 2*RIGHT, run_time = 3)
|
|
self.wait()
|
|
|
|
#Show arc
|
|
arc, circle = [
|
|
Line(ORIGIN, t*UP)
|
|
for t in (get_t(), TAU)
|
|
]
|
|
for mob in arc, circle:
|
|
mob.insert_n_curves(20)
|
|
mob.set_stroke(RED, 4)
|
|
mob.apply_function(
|
|
lambda p : plane.number_to_point(
|
|
np.exp(R3_to_complex(p))
|
|
)
|
|
)
|
|
distance_label = DecimalNumber(
|
|
exp_decimal.number,
|
|
unit = "\\text{units}"
|
|
)
|
|
distance_label[-1].shift(SMALL_BUFF*RIGHT)
|
|
distance_label.match_color(arc)
|
|
distance_label.add_background_rectangle()
|
|
distance_label.move_to(
|
|
plane.number_to_point(
|
|
1.1*np.exp(complex(0, 0.4*get_t())),
|
|
),
|
|
DOWN+LEFT
|
|
)
|
|
|
|
self.play(ShowCreation(arc))
|
|
self.play(ReplacementTransform(
|
|
exp_decimal.copy(), distance_label
|
|
))
|
|
self.wait()
|
|
self.play(FadeOut(distance_label))
|
|
|
|
#Show full cycle
|
|
self.remove(arc)
|
|
self.play(
|
|
ghost_dot.move_to, TAU*RIGHT,
|
|
ShowCreation(
|
|
circle,
|
|
rate_func = lambda a : interpolate(
|
|
2.0/TAU, 1, smooth(a)
|
|
),
|
|
),
|
|
run_time = 6,
|
|
)
|
|
self.wait()
|
|
|
|
#Write exponential expression
|
|
exp_expression = TexMobject("e", "^{-", "2\\pi i", "f", "t}")
|
|
e, minus, two_pi_i, f, t = exp_expression
|
|
exp_expression.next_to(
|
|
plane.coords_to_point(1, 1),
|
|
UP+RIGHT
|
|
)
|
|
f.set_color(RED)
|
|
t.set_color(YELLOW)
|
|
exp_expression.add_background_rectangle()
|
|
two_pi_i_f_t_group = VGroup(two_pi_i, f, t)
|
|
two_pi_i_f_t_group.save_state()
|
|
two_pi_i_f_t_group.move_to(minus, LEFT)
|
|
exp_expression[1].remove(minus)
|
|
t.save_state()
|
|
t.align_to(f, LEFT)
|
|
exp_expression[1].remove(f)
|
|
|
|
labels = VGroup()
|
|
for sym, word in (t, "Time"), (f, "Frequency"):
|
|
label = TextMobject(word)
|
|
label.match_style(sym)
|
|
label.next_to(sym, UP, buff = MED_LARGE_BUFF)
|
|
label.add_background_rectangle()
|
|
label.arrow = Arrow(label, sym, buff = SMALL_BUFF)
|
|
label.arrow.match_style(sym)
|
|
labels.add(label)
|
|
time_label, frequency_label = labels
|
|
example_frequency = TexMobject("f = 1/10")
|
|
example_frequency.add_background_rectangle()
|
|
example_frequency.match_style(frequency_label)
|
|
example_frequency.move_to(frequency_label, DOWN)
|
|
|
|
self.play(ReplacementTransform(
|
|
VGroup(exp_base[1], exp_decimal[1]).copy(),
|
|
exp_expression
|
|
))
|
|
self.play(FadeOut(circle))
|
|
self.wait()
|
|
|
|
ghost_dot.move_to(ORIGIN)
|
|
always_shift(ghost_dot, rate = TAU)
|
|
self.add(ghost_dot)
|
|
|
|
self.play(
|
|
Write(time_label),
|
|
GrowArrow(time_label.arrow),
|
|
)
|
|
self.wait(12.5) #Leave time to say let's slow down
|
|
self.remove(ghost_dot)
|
|
self.play(
|
|
FadeOut(time_label),
|
|
FadeIn(frequency_label),
|
|
t.restore,
|
|
GrowFromPoint(f, frequency_label.get_center()),
|
|
ReplacementTransform(
|
|
time_label.arrow,
|
|
frequency_label.arrow,
|
|
)
|
|
)
|
|
ghost_dot.move_to(ORIGIN)
|
|
ghost_dot.clear_updaters()
|
|
always_shift(ghost_dot, rate=0.1*TAU)
|
|
self.add(ghost_dot)
|
|
self.wait(3)
|
|
self.play(
|
|
FadeOut(frequency_label),
|
|
FadeIn(example_frequency)
|
|
)
|
|
self.wait(15) #Give time to reference other video
|
|
#Reverse directions
|
|
ghost_dot.clear_updaters()
|
|
always_shift(ghost_dot, rate=-0.1 * TAU)
|
|
self.play(
|
|
FadeOut(example_frequency),
|
|
FadeOut(frequency_label.arrow),
|
|
GrowFromCenter(minus),
|
|
two_pi_i_f_t_group.restore
|
|
)
|
|
self.wait(4)
|
|
|
|
ghost_dot.clear_updaters()
|
|
self.remove(*updates)
|
|
self.play(*list(map(FadeOut, [
|
|
update.mobject
|
|
for update in updates
|
|
if update.mobject is not vector
|
|
])))
|
|
self.play(ghost_dot.move_to, ORIGIN)
|
|
|
|
exp_expression[1].add(minus, f)
|
|
exp_expression[1].sort(lambda p : p[0])
|
|
|
|
self.set_variables_as_attrs(
|
|
ambient_ghost_dot_movement, ghost_dot,
|
|
vector, vector_update, exp_expression
|
|
)
|
|
|
|
def show_winding_graph_expression(self):
|
|
ambient_ghost_dot_movement = self.ambient_ghost_dot_movement
|
|
ghost_dot = self.ghost_dot
|
|
vector = self.vector
|
|
exp_expression = self.exp_expression
|
|
plane = self.circle_plane
|
|
time_axes_group = self.time_axes_group
|
|
graph = self.graph
|
|
pol_graph = self.get_polarized_mobject(graph, freq = 0.2)
|
|
g_label = TexMobject("g(t)")
|
|
g_label.match_color(graph)
|
|
g_label.next_to(graph, UP)
|
|
g_label.add_background_rectangle()
|
|
g_scalar = g_label.copy()
|
|
g_scalar.move_to(exp_expression, DOWN+LEFT)
|
|
|
|
vector_animations = self.get_vector_animations(graph)
|
|
vector_animations[1].mobject = vector
|
|
graph_y_vector = vector_animations[0].mobject
|
|
|
|
self.play(
|
|
FadeIn(time_axes_group),
|
|
FadeOut(self.complex_plane_title)
|
|
)
|
|
self.play(Write(g_label))
|
|
self.wait()
|
|
self.play(
|
|
ReplacementTransform(g_label.copy(), g_scalar),
|
|
exp_expression.next_to, g_scalar, RIGHT, SMALL_BUFF,
|
|
exp_expression.shift, 0.5*SMALL_BUFF*UP,
|
|
)
|
|
self.play(*vector_animations, run_time = 15)
|
|
self.add(*self.mobjects_from_last_animation)
|
|
self.wait()
|
|
|
|
integrand = VGroup(g_scalar, exp_expression)
|
|
rect = SurroundingRectangle(integrand)
|
|
morty = Mortimer()
|
|
morty.next_to(rect, DOWN+RIGHT)
|
|
morty.shift_onto_screen()
|
|
self.play(
|
|
ShowCreation(rect),
|
|
FadeIn(morty)
|
|
)
|
|
self.play(morty.change, "raise_right_hand")
|
|
self.play(Blink(morty))
|
|
self.play(morty.change, "hooray", rect)
|
|
self.wait(2)
|
|
self.play(*list(map(FadeOut, [
|
|
morty, rect, graph_y_vector, vector
|
|
])))
|
|
|
|
self.integrand = integrand
|
|
|
|
def find_center_of_mass(self):
|
|
integrand = self.integrand
|
|
integrand.generate_target()
|
|
integrand.target.to_edge(RIGHT, buff = LARGE_BUFF)
|
|
integrand.target.shift(MED_LARGE_BUFF*DOWN)
|
|
sum_expr = TexMobject(
|
|
"{1", "\\over", "N}",
|
|
"\\sum", "_{k = 1}", "^N",
|
|
)
|
|
sum_expr.add_background_rectangle()
|
|
sum_expr.shift(SMALL_BUFF*(UP+5*RIGHT))
|
|
sum_expr.next_to(integrand.target, LEFT)
|
|
|
|
integral = TexMobject(
|
|
"{1", "\\over", "t_2 - t_1}",
|
|
"\\int", "_{t_1}", "^{t_2}"
|
|
)
|
|
integral.move_to(sum_expr, RIGHT)
|
|
time_interval_indicator = SurroundingRectangle(integral[2])
|
|
integral.add_background_rectangle()
|
|
axes = self.time_axes
|
|
time_interval = Line(
|
|
axes.coords_to_point(axes.x_min, 0),
|
|
axes.coords_to_point(axes.x_max, 0),
|
|
)
|
|
time_interval.match_style(time_interval_indicator)
|
|
time_interval_indicator.add(time_interval)
|
|
dt_mob = TexMobject("dt")
|
|
dt_mob.next_to(integrand.target, RIGHT, SMALL_BUFF, DOWN)
|
|
dt_mob.add_background_rectangle()
|
|
|
|
dots = self.show_center_of_mass_sampling(20)
|
|
self.wait()
|
|
self.play(
|
|
Write(sum_expr),
|
|
MoveToTarget(integrand),
|
|
)
|
|
|
|
#Add k subscript to t's
|
|
t1 = integrand[0][1][2]
|
|
t2 = integrand[1][1][-1]
|
|
t_mobs = VGroup(t1, t2)
|
|
t_mobs.save_state()
|
|
t_mobs.generate_target()
|
|
for i, t_mob in enumerate(t_mobs.target):
|
|
k = TexMobject("k")
|
|
k.match_style(t_mob)
|
|
k.match_height(t_mob)
|
|
k.scale(0.5)
|
|
k.move_to(t_mob.get_corner(DOWN+RIGHT), LEFT)
|
|
k.add_background_rectangle()
|
|
t_mob.add(k)
|
|
if i == 0:
|
|
t_mob.shift(0.5*SMALL_BUFF*LEFT)
|
|
|
|
self.play(MoveToTarget(t_mobs))
|
|
self.play(LaggedStartMap(
|
|
Indicate, dots[1],
|
|
rate_func = there_and_back,
|
|
color = TEAL,
|
|
))
|
|
self.show_center_of_mass_sampling(100)
|
|
dots = self.show_center_of_mass_sampling(500)
|
|
self.wait()
|
|
self.play(FadeOut(dots))
|
|
self.play(
|
|
ReplacementTransform(sum_expr, integral),
|
|
FadeIn(dt_mob),
|
|
t_mobs.restore,
|
|
)
|
|
self.wait()
|
|
self.play(ShowCreation(time_interval_indicator))
|
|
self.wait()
|
|
self.play(FadeOut(time_interval_indicator))
|
|
self.wait()
|
|
|
|
#Show confusion
|
|
randy = Randolph()
|
|
randy.flip()
|
|
randy.next_to(integrand, DOWN, LARGE_BUFF)
|
|
randy.to_edge(RIGHT)
|
|
full_expression_rect = SurroundingRectangle(
|
|
VGroup(integral, dt_mob), color = RED
|
|
)
|
|
com_dot = self.center_of_mass_dot
|
|
self.center_of_mass_dot_anim.update(0)
|
|
com_arrow = Arrow(
|
|
full_expression_rect.get_left(), com_dot,
|
|
buff = SMALL_BUFF
|
|
)
|
|
com_arrow.match_color(com_dot)
|
|
|
|
|
|
self.play(FadeIn(randy))
|
|
self.play(randy.change, "confused", integral)
|
|
self.play(Blink(randy))
|
|
self.wait(2)
|
|
self.play(ShowCreation(full_expression_rect))
|
|
self.play(
|
|
randy.change, "thinking", self.pol_graph,
|
|
GrowArrow(com_arrow),
|
|
GrowFromCenter(com_dot),
|
|
)
|
|
self.play(Blink(randy))
|
|
self.wait(2)
|
|
|
|
def show_center_of_mass_sampling(self, n_dots):
|
|
time_graph = self.graph
|
|
pol_graph = self.graph.polarized_mobject
|
|
axes = self.time_axes
|
|
|
|
dot = Dot(radius = 0.05, color = PINK)
|
|
pre_dots = VGroup(*[
|
|
dot.copy().move_to(axes.coords_to_point(t, 0))
|
|
for t in np.linspace(axes.x_min, axes.x_max, n_dots)
|
|
])
|
|
pre_dots.set_fill(opacity = 0)
|
|
for graph in time_graph, pol_graph:
|
|
if hasattr(graph, "dots"):
|
|
graph.dot_fade_anims = [FadeOut(graph.dots)]
|
|
else:
|
|
graph.dot_fade_anims = []
|
|
graph.save_state()
|
|
graph.generate_target()
|
|
if not hasattr(graph, "is_faded"):
|
|
graph.target.fade(0.7)
|
|
graph.is_faded = True
|
|
graph.dots = VGroup(*[
|
|
dot.copy().move_to(graph.point_from_proportion(a))
|
|
for a in np.linspace(0, 1, n_dots)
|
|
])
|
|
|
|
self.play(
|
|
ReplacementTransform(
|
|
pre_dots, time_graph.dots,
|
|
lag_ratio = 0.5,
|
|
run_time = 2,
|
|
),
|
|
MoveToTarget(time_graph),
|
|
*time_graph.dot_fade_anims
|
|
)
|
|
self.play(
|
|
ReplacementTransform(
|
|
time_graph.copy(), pol_graph.target
|
|
),
|
|
MoveToTarget(pol_graph),
|
|
ReplacementTransform(
|
|
time_graph.dots.copy(),
|
|
pol_graph.dots,
|
|
),
|
|
*pol_graph.dot_fade_anims,
|
|
run_time = 2
|
|
)
|
|
return VGroup(time_graph.dots, pol_graph.dots)
|
|
|
|
class EulersFormulaViaGroupTheoryWrapper(Scene):
|
|
def construct(self):
|
|
title = TextMobject("Euler's formula with introductory group theory")
|
|
title.to_edge(UP)
|
|
screen_rect = ScreenRectangle(height = 6)
|
|
screen_rect.next_to(title, DOWN)
|
|
self.add(title)
|
|
self.play(ShowCreation(screen_rect))
|
|
self.wait(2)
|
|
|
|
class WhyAreYouTellingUsThis(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.student_says("Why are you \\\\ telling us this?")
|
|
self.play(self.teacher.change, "happy")
|
|
self.wait(2)
|
|
|
|
class BuildUpExpressionStepByStep(TeacherStudentsScene):
|
|
def construct(self):
|
|
expression = TexMobject(
|
|
"\\frac{1}{t_2 - t_1}", "\\int_{t_1}^{t_2}",
|
|
"g(t)", "e", "^{-2\\pi i", "f", "t}", "dt"
|
|
)
|
|
frac, integral, g, e, two_pi_i, f, t, dt = expression
|
|
expression.next_to(self.teacher, UP+LEFT)
|
|
t.set_color(YELLOW)
|
|
g[2].set_color(YELLOW)
|
|
dt[1].set_color(YELLOW)
|
|
f.set_color(GREEN)
|
|
t.save_state()
|
|
t.move_to(f, LEFT)
|
|
|
|
self.play(
|
|
self.teacher.change, "raise_right_hand",
|
|
FadeIn(e),
|
|
FadeIn(two_pi_i),
|
|
)
|
|
self.play(
|
|
self.get_student_changes(*["pondering"]*3),
|
|
FadeIn(t),
|
|
)
|
|
self.play(
|
|
FadeIn(f),
|
|
t.restore,
|
|
)
|
|
self.wait()
|
|
self.play(FadeIn(g), Blink(self.students[1]))
|
|
self.wait()
|
|
self.play(
|
|
FadeIn(integral),
|
|
FadeIn(frac),
|
|
FadeIn(dt),
|
|
)
|
|
self.wait(3)
|
|
self.teacher_says(
|
|
"Just one final \\\\ distinction.",
|
|
bubble_kwargs = {"height" : 2.5, "width" : 3.5},
|
|
added_anims = [expression.to_corner, UP+RIGHT]
|
|
)
|
|
self.wait(3)
|
|
|
|
class ScaleUpCenterOfMass(WriteComplexExponentialExpression):
|
|
CONFIG = {
|
|
"time_axes_scale_val" : 0.6,
|
|
"initial_winding_frequency" : 2.05
|
|
}
|
|
def construct(self):
|
|
self.remove(self.pi_creature)
|
|
self.setup_plane()
|
|
self.setup_graph()
|
|
self.add_center_of_mass_dot()
|
|
self.add_expression()
|
|
|
|
self.cross_out_denominator()
|
|
self.scale_up_center_of_mass()
|
|
self.comment_on_current_signal()
|
|
|
|
def add_center_of_mass_dot(self):
|
|
self.center_of_mass_dot = self.get_center_of_mass_dot()
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
self.add(self.center_of_mass_dot)
|
|
|
|
def add_expression(self):
|
|
expression = TexMobject(
|
|
"\\frac{1}{t_2 - t_1}", "\\int_{t_1}^{t_2}",
|
|
"g(t)", "e", "^{-2\\pi i", "f", "t}", "dt"
|
|
)
|
|
frac, integral, g, e, two_pi_i, f, t, dt = expression
|
|
expression.to_corner(UP+RIGHT)
|
|
t.set_color(YELLOW)
|
|
g[2].set_color(YELLOW)
|
|
dt[1].set_color(YELLOW)
|
|
f.set_color(GREEN)
|
|
self.expression = expression
|
|
self.add(expression)
|
|
|
|
self.winding_freq_label.to_edge(RIGHT)
|
|
self.winding_freq_label[1].match_color(f)
|
|
self.winding_freq_label.align_to(
|
|
self.circle_plane.coords_to_point(0, 0.1), DOWN
|
|
)
|
|
|
|
def cross_out_denominator(self):
|
|
frac = self.expression[0]
|
|
integral = self.expression[1:]
|
|
for mob in frac, integral:
|
|
mob.add_to_back(BackgroundRectangle(mob))
|
|
self.add(mob)
|
|
cross = Cross(frac)
|
|
brace = Brace(integral, DOWN)
|
|
label = brace.get_text("The actual \\\\ Fourier transform")
|
|
label.add_background_rectangle()
|
|
label.shift_onto_screen()
|
|
rect = SurroundingRectangle(integral)
|
|
|
|
self.play(ShowCreation(cross))
|
|
self.wait()
|
|
self.play(ShowCreation(rect))
|
|
self.play(
|
|
GrowFromCenter(brace),
|
|
FadeIn(label)
|
|
)
|
|
self.wait(2)
|
|
|
|
self.integral = integral
|
|
self.frac = frac
|
|
self.frac_cross = cross
|
|
self.integral_rect = rect
|
|
self.integral_brace = brace
|
|
self.integral_label = label
|
|
|
|
def scale_up_center_of_mass(self):
|
|
plane = self.circle_plane
|
|
origin = plane.coords_to_point(0, 0)
|
|
com_dot = self.center_of_mass_dot
|
|
com_vector = Arrow(
|
|
origin, com_dot.get_center(),
|
|
buff = 0
|
|
)
|
|
com_vector.match_style(com_dot)
|
|
vector_to_scale = com_vector.copy()
|
|
def get_com_vector_copies(n):
|
|
com_vector_copies = VGroup(*[
|
|
com_vector.copy().shift(x*com_vector.get_vector())
|
|
for x in range(1, n+1)
|
|
])
|
|
com_vector_copies.set_color(TEAL)
|
|
return com_vector_copies
|
|
com_vector_update = UpdateFromFunc(
|
|
com_vector,
|
|
lambda v : v.put_start_and_end_on(origin, com_dot.get_center())
|
|
)
|
|
|
|
circle = Circle(color = TEAL)
|
|
circle.surround(com_dot, buffer_factor = 1.2)
|
|
|
|
time_span = Rectangle(
|
|
stroke_width = 0,
|
|
fill_color = TEAL,
|
|
fill_opacity = 0.4
|
|
)
|
|
axes = self.time_axes
|
|
time_span.replace(
|
|
Line(axes.coords_to_point(0, 0), axes.coords_to_point(3, 1.5)),
|
|
stretch = True
|
|
)
|
|
time_span.save_state()
|
|
time_span.stretch(0, 0, about_edge = LEFT)
|
|
|
|
graph = self.graph
|
|
short_graph, long_graph = [
|
|
axes.get_graph(
|
|
graph.underlying_function, x_min = 0, x_max = t_max,
|
|
).match_style(graph)
|
|
for t_max in (3, 6)
|
|
]
|
|
for g in short_graph, long_graph:
|
|
self.get_polarized_mobject(g, freq = self.initial_winding_frequency)
|
|
|
|
self.play(
|
|
FocusOn(circle, run_time = 2),
|
|
Succession(
|
|
ShowCreation, circle,
|
|
FadeOut, circle,
|
|
),
|
|
)
|
|
self.play(
|
|
com_dot.fade, 0.5,
|
|
FadeIn(vector_to_scale)
|
|
)
|
|
self.wait()
|
|
self.play(vector_to_scale.scale, 4, {"about_point" : origin})
|
|
self.wait()
|
|
self.play(
|
|
FadeOut(vector_to_scale),
|
|
FadeIn(com_vector),
|
|
)
|
|
self.remove(graph.polarized_mobject)
|
|
self.play(
|
|
com_dot.move_to,
|
|
center_of_mass(short_graph.polarized_mobject.points),
|
|
com_vector_update,
|
|
time_span.restore,
|
|
ShowCreation(short_graph.polarized_mobject),
|
|
)
|
|
self.wait()
|
|
# dot = Dot(fill_opacity = 0.5).move_to(time_span)
|
|
# self.play(
|
|
# dot.move_to, com_vector,
|
|
# dot.set_fill, {"opacity" : 0},
|
|
# remover = True
|
|
# )
|
|
com_vector_copies = get_com_vector_copies(2)
|
|
self.play(*[
|
|
ReplacementTransform(
|
|
com_vector.copy(), cvc,
|
|
path_arc = -TAU/10
|
|
)
|
|
for cvc in com_vector_copies
|
|
])
|
|
self.wait()
|
|
|
|
#Squish_graph
|
|
to_squish = VGroup(
|
|
axes, graph,
|
|
time_span,
|
|
)
|
|
to_squish.generate_target()
|
|
squish_factor = 0.75
|
|
to_squish.target.stretch(squish_factor, 0, about_edge = LEFT)
|
|
pairs = list(zip(
|
|
to_squish.family_members_with_points(),
|
|
to_squish.target.family_members_with_points()
|
|
))
|
|
to_unsquish = list(axes.x_axis.numbers) + list(axes.labels)
|
|
for sm, tsm in pairs:
|
|
if sm in to_unsquish:
|
|
tsm.stretch(1/squish_factor, 0)
|
|
if sm is axes.background_rectangle:
|
|
tsm.stretch(1/squish_factor, 0, about_edge = LEFT)
|
|
|
|
long_graph.stretch(squish_factor, 0)
|
|
self.play(
|
|
MoveToTarget(to_squish),
|
|
FadeOut(com_vector_copies)
|
|
)
|
|
long_graph.move_to(graph, LEFT)
|
|
self.play(
|
|
com_dot.move_to,
|
|
center_of_mass(long_graph.polarized_mobject.points),
|
|
com_vector_update,
|
|
time_span.stretch, 2, 0, {"about_edge" : LEFT},
|
|
*[
|
|
ShowCreation(
|
|
mob,
|
|
rate_func = lambda a : interpolate(
|
|
0.5, 1, smooth(a)
|
|
)
|
|
)
|
|
for mob in (long_graph, long_graph.polarized_mobject)
|
|
],
|
|
run_time = 2
|
|
)
|
|
self.remove(graph, short_graph.polarized_mobject)
|
|
self.graph = long_graph
|
|
self.wait()
|
|
self.play(FocusOn(com_dot))
|
|
com_vector_copies = get_com_vector_copies(5)
|
|
self.play(*[
|
|
ReplacementTransform(
|
|
com_vector.copy(), cvc,
|
|
path_arc = -TAU/10
|
|
)
|
|
for cvc in com_vector_copies
|
|
])
|
|
self.wait()
|
|
|
|
# Scale graph out even longer
|
|
to_shift = VGroup(self.integral, self.integral_rect)
|
|
to_fade = VGroup(
|
|
self.integral_brace, self.integral_label,
|
|
self.frac, self.frac_cross
|
|
)
|
|
self.play(
|
|
to_shift.shift, 2*DOWN,
|
|
FadeOut(to_fade),
|
|
axes.background_rectangle.stretch, 2, 0, {"about_edge" : LEFT},
|
|
Animation(axes),
|
|
Animation(self.graph),
|
|
FadeOut(com_vector_copies),
|
|
)
|
|
self.change_frequency(2.0, added_anims = [com_vector_update])
|
|
very_long_graph = axes.get_graph(
|
|
graph.underlying_function,
|
|
x_min = 0, x_max = 12,
|
|
)
|
|
very_long_graph.match_style(graph)
|
|
self.get_polarized_mobject(very_long_graph, freq = 2.0)
|
|
self.play(
|
|
com_dot.move_to,
|
|
center_of_mass(very_long_graph.polarized_mobject.points),
|
|
com_vector_update,
|
|
ShowCreation(
|
|
very_long_graph,
|
|
rate_func = lambda a : interpolate(0.5, 1, a)
|
|
),
|
|
ShowCreation(very_long_graph.polarized_mobject)
|
|
)
|
|
self.remove(graph, graph.polarized_mobject)
|
|
self.graph = very_long_graph
|
|
self.wait()
|
|
self.play(
|
|
com_vector.scale, 12, {"about_point" : origin},
|
|
run_time = 2
|
|
)
|
|
# com_vector_copies = get_com_vector_copies(11)
|
|
# self.play(ReplacementTransform(
|
|
# VGroup(com_vector.copy()),
|
|
# com_vector_copies,
|
|
# path_arc = TAU/10,
|
|
# run_time = 1.5,
|
|
# lag_ratio = 0.5
|
|
# ))
|
|
self.wait()
|
|
|
|
self.com_vector = com_vector
|
|
self.com_vector_update = com_vector_update
|
|
self.com_vector_copies = com_vector_copies
|
|
|
|
def comment_on_current_signal(self):
|
|
graph = self.graph
|
|
com_dot = self.center_of_mass_dot
|
|
com_vector = self.com_vector
|
|
com_vector_update = self.com_vector_update
|
|
axes = self.time_axes
|
|
origin = self.circle_plane.coords_to_point(0, 0)
|
|
wps_label = self.winding_freq_label
|
|
|
|
new_com_vector_update = UpdateFromFunc(
|
|
com_vector, lambda v : v.put_start_and_end_on(
|
|
origin, com_dot.get_center()
|
|
).scale(12, about_point = origin)
|
|
)
|
|
|
|
v_lines = self.get_v_lines_indicating_periods(
|
|
freq = 1.0, n_lines = 3
|
|
)[:2]
|
|
graph_portion = axes.get_graph(
|
|
graph.underlying_function, x_min = 1, x_max = 2
|
|
)
|
|
graph_portion.set_color(TEAL)
|
|
bps_label = TextMobject("2 beats per second")
|
|
bps_label.scale(0.75)
|
|
bps_label.next_to(graph_portion, UP, aligned_edge = LEFT)
|
|
bps_label.shift(SMALL_BUFF*RIGHT)
|
|
bps_label.add_background_rectangle()
|
|
|
|
self.play(
|
|
ShowCreation(v_lines, lag_ratio = 0),
|
|
ShowCreation(graph_portion),
|
|
FadeIn(bps_label),
|
|
)
|
|
self.wait()
|
|
self.play(ReplacementTransform(
|
|
bps_label[1][0].copy(), wps_label[1]
|
|
))
|
|
self.wait()
|
|
self.play(
|
|
com_vector.scale, 0.5, {"about_point" : origin},
|
|
rate_func = there_and_back,
|
|
run_time = 2
|
|
)
|
|
self.wait(2)
|
|
self.change_frequency(2.5,
|
|
added_anims = [new_com_vector_update],
|
|
run_time = 20,
|
|
rate_func=linear,
|
|
)
|
|
self.wait()
|
|
|
|
class TakeAStepBack(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.student_says(
|
|
"Hang on, go over \\\\ that again?",
|
|
target_mode = "confused"
|
|
),
|
|
self.change_student_modes(*["confused"]*3)
|
|
self.play(self.teacher.change, "happy")
|
|
self.wait(3)
|
|
|
|
class SimpleCosineWrappingAroundCircle(WriteComplexExponentialExpression):
|
|
CONFIG = {
|
|
"initial_winding_frequency" : 0,
|
|
"circle_plane_config" : {
|
|
"unit_size" : 3,
|
|
},
|
|
}
|
|
def construct(self):
|
|
self.setup_plane()
|
|
self.setup_graph()
|
|
self.remove(self.pi_creature)
|
|
self.winding_freq_label.shift(7*LEFT)
|
|
VGroup(self.time_axes, self.graph).shift(4*UP)
|
|
VGroup(
|
|
self.circle_plane,
|
|
self.graph.polarized_mobject
|
|
).move_to(ORIGIN)
|
|
self.add(self.get_center_of_mass_dot())
|
|
self.generate_center_of_mass_dot_update_anim()
|
|
|
|
self.change_frequency(
|
|
2.0,
|
|
rate_func=linear,
|
|
run_time = 30
|
|
)
|
|
self.wait()
|
|
|
|
class SummarizeTheFullTransform(DrawFrequencyPlot):
|
|
CONFIG = {
|
|
"time_axes_config" : {
|
|
"x_max" : 4.5,
|
|
"x_axis_config" : {
|
|
"unit_size" : 1.2,
|
|
"tick_frequency" : 0.5,
|
|
# "numbers_with_elongated_ticks" : range(0, 10, 2),
|
|
# "numbers_to_show" : range(0, 10, 2),
|
|
}
|
|
},
|
|
"frequency_axes_config" : {
|
|
"x_max" : 5,
|
|
"x_axis_config" : {
|
|
"unit_size" : 1,
|
|
"numbers_to_show" : list(range(1, 5)),
|
|
},
|
|
"y_max" : 2,
|
|
"y_min" : -2,
|
|
"y_axis_config" : {
|
|
"unit_size" : 0.75,
|
|
"tick_frequency" : 1,
|
|
},
|
|
},
|
|
}
|
|
def construct(self):
|
|
self.setup_all_axes()
|
|
self.show_transform_function()
|
|
self.show_winding()
|
|
|
|
def setup_all_axes(self):
|
|
time_axes = self.get_time_axes()
|
|
time_label, intensity_label = time_axes.labels
|
|
time_label.next_to(
|
|
time_axes.x_axis.get_right(),
|
|
DOWN, SMALL_BUFF
|
|
)
|
|
intensity_label.next_to(time_axes.y_axis, UP, buff = SMALL_BUFF)
|
|
intensity_label.to_edge(LEFT)
|
|
|
|
frequency_axes = self.get_frequency_axes()
|
|
frequency_axes.to_corner(UP+RIGHT)
|
|
frequency_axes.shift(RIGHT)
|
|
fy_axis = frequency_axes.y_axis
|
|
for number in fy_axis.numbers:
|
|
number.add_background_rectangle()
|
|
fy_axis.remove(*fy_axis.numbers[1::2])
|
|
frequency_axes.remove(frequency_axes.box)
|
|
frequency_axes.label.shift_onto_screen()
|
|
|
|
circle_plane = self.get_circle_plane()
|
|
|
|
self.set_variables_as_attrs(time_axes, frequency_axes, circle_plane)
|
|
self.add(time_axes)
|
|
|
|
def show_transform_function(self):
|
|
time_axes = self.time_axes
|
|
frequency_axes = self.frequency_axes
|
|
def func(t):
|
|
return 0.5*(2+np.cos(2*TAU*t) + np.cos(3*TAU*t))
|
|
fourier_func = get_fourier_transform(
|
|
func,
|
|
t_min = time_axes.x_min,
|
|
t_max = time_axes.x_max,
|
|
use_almost_fourier = False,
|
|
)
|
|
|
|
graph = time_axes.get_graph(func)
|
|
graph.set_color(GREEN)
|
|
fourier_graph = frequency_axes.get_graph(fourier_func)
|
|
fourier_graph.set_color(RED)
|
|
|
|
g_t = TexMobject("g(t)")
|
|
g_t[-2].match_color(graph)
|
|
g_t.next_to(graph, UP)
|
|
g_hat_f = TexMobject("\\hat g(f)")
|
|
g_hat_f[-2].match_color(fourier_graph)
|
|
g_hat_f.next_to(
|
|
frequency_axes.input_to_graph_point(2, fourier_graph),
|
|
UP
|
|
)
|
|
|
|
morty = self.pi_creature
|
|
|
|
time_label = time_axes.labels[0]
|
|
frequency_label = frequency_axes.label
|
|
for label in time_label, frequency_label:
|
|
label.rect = SurroundingRectangle(label)
|
|
time_label.rect.match_style(graph)
|
|
frequency_label.rect.match_style(fourier_graph)
|
|
|
|
self.add(graph)
|
|
g_t.save_state()
|
|
g_t.move_to(morty, UP+LEFT)
|
|
g_t.fade(1)
|
|
self.play(
|
|
morty.change, "raise_right_hand",
|
|
g_t.restore,
|
|
)
|
|
self.wait()
|
|
self.play(Write(frequency_axes, run_time = 1))
|
|
self.play(
|
|
ReplacementTransform(graph.copy(), fourier_graph),
|
|
ReplacementTransform(g_t.copy(), g_hat_f),
|
|
)
|
|
self.wait(2)
|
|
for label in time_label, frequency_label:
|
|
self.play(
|
|
ShowCreation(label.rect),
|
|
morty.change, "thinking"
|
|
)
|
|
self.play(FadeOut(label.rect))
|
|
self.wait()
|
|
|
|
self.set_variables_as_attrs(
|
|
graph, fourier_graph,
|
|
g_t, g_hat_f
|
|
)
|
|
|
|
def show_winding(self):
|
|
plane = self.circle_plane
|
|
graph = self.graph
|
|
fourier_graph = self.fourier_graph
|
|
morty = self.pi_creature
|
|
g_hat_f = self.g_hat_f
|
|
g_hat_f_rect = SurroundingRectangle(g_hat_f)
|
|
g_hat_f_rect.set_color(TEAL)
|
|
g_hat_rect = SurroundingRectangle(g_hat_f[0])
|
|
g_hat_rect.match_style(g_hat_f_rect)
|
|
|
|
g_hat_f.generate_target()
|
|
g_hat_f.target.next_to(plane, RIGHT)
|
|
g_hat_f.target.shift(UP)
|
|
arrow = Arrow(
|
|
g_hat_f.target.get_left(),
|
|
plane.coords_to_point(0, 0),
|
|
color = self.center_of_mass_color,
|
|
)
|
|
|
|
frequency_axes = self.frequency_axes
|
|
imaginary_fourier_graph = frequency_axes.get_graph(
|
|
get_fourier_transform(
|
|
graph.underlying_function,
|
|
t_min = self.time_axes.x_min,
|
|
t_max = self.time_axes.x_max,
|
|
real_part = False,
|
|
use_almost_fourier = False,
|
|
)
|
|
)
|
|
imaginary_fourier_graph.set_color(BLUE)
|
|
imaginary_fourier_graph.shift(
|
|
frequency_axes.x_axis.get_right() - \
|
|
imaginary_fourier_graph.points[-1],
|
|
)
|
|
|
|
real_part = TextMobject(
|
|
"Real part of", "$\\hat g(f)$"
|
|
)
|
|
real_part[1].match_style(g_hat_f)
|
|
real_part.move_to(g_hat_f)
|
|
real_part.to_edge(RIGHT)
|
|
|
|
self.get_polarized_mobject(graph, freq = 0)
|
|
update_pol_graph = UpdateFromFunc(
|
|
graph.polarized_mobject,
|
|
lambda m : m.set_stroke(width = 2)
|
|
)
|
|
com_dot = self.get_center_of_mass_dot()
|
|
|
|
winding_run_time = 40.0
|
|
g_hat_f_indication = Succession(
|
|
Animation, Mobject(), {"run_time" : 4},
|
|
FocusOn, g_hat_f,
|
|
ShowCreation, g_hat_f_rect,
|
|
Animation, Mobject(),
|
|
Transform, g_hat_f_rect, g_hat_rect,
|
|
Animation, Mobject(),
|
|
FadeOut, g_hat_f_rect,
|
|
Animation, Mobject(),
|
|
MoveToTarget, g_hat_f,
|
|
UpdateFromAlphaFunc, com_dot, lambda m, a : m.set_fill(opacity = a),
|
|
Animation, Mobject(), {"run_time" : 2},
|
|
GrowArrow, arrow,
|
|
FadeOut, arrow,
|
|
Animation, Mobject(), {"run_time" : 5},
|
|
Write, real_part, {"run_time" : 2},
|
|
Animation, Mobject(), {"run_time" : 3},
|
|
ShowCreation, imaginary_fourier_graph, {"run_time" : 3},
|
|
rate_func = squish_rate_func(
|
|
lambda x : x, 0, 31./winding_run_time
|
|
),
|
|
run_time = winding_run_time
|
|
)
|
|
|
|
self.play(
|
|
FadeIn(plane),
|
|
ReplacementTransform(
|
|
graph.copy(), graph.polarized_mobject
|
|
),
|
|
morty.change, "happy",
|
|
)
|
|
self.generate_center_of_mass_dot_update_anim(multiplier = 4.5)
|
|
self.generate_fourier_dot_transform(fourier_graph)
|
|
self.change_frequency(
|
|
5.0,
|
|
rate_func=linear,
|
|
run_time = winding_run_time,
|
|
added_anims = [
|
|
g_hat_f_indication,
|
|
update_pol_graph,
|
|
Animation(frequency_axes.x_axis.numbers),
|
|
Animation(self.fourier_graph_dot),
|
|
]
|
|
)
|
|
self.wait()
|
|
|
|
class SummarizeFormula(Scene):
|
|
def construct(self):
|
|
expression = self.get_expression()
|
|
screen_rect = ScreenRectangle(height = 5)
|
|
screen_rect.to_edge(DOWN)
|
|
|
|
exp_rect, g_exp_rect, int_rect = [
|
|
SurroundingRectangle(VGroup(
|
|
expression.get_part_by_tex(p1),
|
|
expression.get_part_by_tex(p2),
|
|
))
|
|
for p1, p2 in [("e", "t}"), ("g({}", "t}"), ("\\int", "dt")]
|
|
]
|
|
|
|
self.add(expression)
|
|
self.wait()
|
|
self.play(
|
|
ShowCreation(screen_rect),
|
|
ShowCreation(exp_rect),
|
|
)
|
|
self.wait(2)
|
|
self.play(Transform(exp_rect, g_exp_rect))
|
|
self.wait(2)
|
|
self.play(Transform(exp_rect, int_rect))
|
|
self.wait(2)
|
|
|
|
def get_expression(self):
|
|
expression = TexMobject(
|
|
"\\hat g(", "f", ")", "=", "\\int", "_{t_1}", "^{t_2}",
|
|
"g({}", "t", ")", "e", "^{-2\\pi i", "f", "t}", "dt"
|
|
)
|
|
expression.set_color_by_tex(
|
|
"t", YELLOW, substring = False,
|
|
)
|
|
expression.set_color_by_tex("t}", YELLOW)
|
|
expression.set_color_by_tex(
|
|
"f", RED, substring = False,
|
|
)
|
|
expression.scale(1.2)
|
|
expression.to_edge(UP)
|
|
return expression
|
|
|
|
class OneSmallNote(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.teacher_says(
|
|
"Just one \\\\ small note...",
|
|
# target_mode =
|
|
)
|
|
self.change_student_modes("erm", "happy", "sassy")
|
|
self.wait(2)
|
|
|
|
class BoundsAtInfinity(SummarizeFormula):
|
|
def construct(self):
|
|
expression = self.get_expression()
|
|
self.add(expression)
|
|
self.add_graph()
|
|
axes = self.axes
|
|
graph = self.graph
|
|
|
|
time_interval = self.get_time_interval(-2, 2)
|
|
wide_interval = self.get_time_interval(-FRAME_X_RADIUS, FRAME_X_RADIUS)
|
|
bounds = VGroup(*reversed(expression.get_parts_by_tex("t_")))
|
|
bound_rects = VGroup(*[
|
|
SurroundingRectangle(b, buff = 0.5*SMALL_BUFF)
|
|
for b in bounds
|
|
])
|
|
bound_rects.set_color(TEAL)
|
|
inf_bounds = VGroup(*[
|
|
VGroup(TexMobject(s + "\\infty"))
|
|
for s in ("-", "+")
|
|
])
|
|
decimal_bounds = VGroup(*[DecimalNumber(0) for x in range(2)])
|
|
for bound, inf_bound, d_bound in zip(bounds, inf_bounds, decimal_bounds):
|
|
for new_bound in inf_bound, d_bound:
|
|
new_bound.scale(0.7)
|
|
new_bound.move_to(bound, LEFT)
|
|
new_bound.bound = bound
|
|
def get_db_num_update(vect):
|
|
return lambda a : axes.x_axis.point_to_number(
|
|
time_interval.get_edge_center(vect)
|
|
)
|
|
decimal_updates = [
|
|
ChangingDecimal(
|
|
db, get_db_num_update(vect),
|
|
position_update_func = lambda m : m.move_to(
|
|
m.bound, LEFT
|
|
)
|
|
)
|
|
for db, vect in zip(decimal_bounds, [LEFT, RIGHT])
|
|
]
|
|
for update in decimal_updates:
|
|
update.update(1)
|
|
|
|
time_interval.save_state()
|
|
self.wait()
|
|
self.play(ReplacementTransform(
|
|
self.get_time_interval(0, 0.01), time_interval
|
|
))
|
|
self.play(LaggedStartMap(ShowCreation, bound_rects))
|
|
self.wait()
|
|
self.play(FadeOut(bound_rects))
|
|
self.play(ReplacementTransform(bounds, inf_bounds))
|
|
self.play(Transform(
|
|
time_interval, wide_interval,
|
|
run_time = 4,
|
|
rate_func = there_and_back
|
|
))
|
|
self.play(
|
|
ReplacementTransform(inf_bounds, decimal_bounds),
|
|
time_interval.restore,
|
|
)
|
|
self.play(
|
|
VGroup(axes, graph).stretch, 0.05, 0,
|
|
Transform(time_interval, wide_interval),
|
|
UpdateFromAlphaFunc(
|
|
axes.x_axis.numbers,
|
|
lambda m, a : m.set_fill(opacity = 1-a)
|
|
),
|
|
*decimal_updates,
|
|
run_time = 12,
|
|
rate_func = bezier([0, 0, 1, 1])
|
|
)
|
|
self.wait()
|
|
|
|
|
|
def add_graph(self):
|
|
axes = Axes(
|
|
x_min = -140,
|
|
x_max = 140,
|
|
y_min = -2,
|
|
y_max = 2,
|
|
number_line_config = {
|
|
"include_tip" : False,
|
|
},
|
|
)
|
|
axes.x_axis.add_numbers(*list(filter(
|
|
lambda x : x != 0,
|
|
list(range(-8, 10, 2)),
|
|
)))
|
|
axes.shift(DOWN)
|
|
self.add(axes)
|
|
|
|
def func(x):
|
|
return np.exp(-0.1*x**2)*(1 + np.cos(TAU*x))
|
|
graph = axes.get_graph(func)
|
|
self.add(graph)
|
|
graph.set_color(YELLOW)
|
|
|
|
self.set_variables_as_attrs(axes, graph)
|
|
|
|
def get_time_interval(self, t1, t2):
|
|
line = Line(*[
|
|
self.axes.coords_to_point(t, 0)
|
|
for t in (t1, t2)
|
|
])
|
|
rect = Rectangle(
|
|
stroke_width = 0,
|
|
fill_color = TEAL,
|
|
fill_opacity = 0.5,
|
|
)
|
|
rect.match_width(line)
|
|
rect.stretch_to_fit_height(2.5)
|
|
rect.move_to(line, DOWN)
|
|
return rect
|
|
|
|
class MoreToCover(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.teacher_says(
|
|
"Much more to say...",
|
|
target_mode = "hooray",
|
|
run_time = 1,
|
|
)
|
|
self.wait()
|
|
self.teacher_says(
|
|
"SO MUCH!",
|
|
target_mode = "surprised",
|
|
added_anims = [self.get_student_changes(*3*["happy"])],
|
|
run_time = 0.5
|
|
)
|
|
self.wait(2)
|
|
|
|
class ShowUncertaintyPrinciple(Scene):
|
|
def construct(self):
|
|
title = TextMobject("Uncertainty principle")
|
|
self.add(title)
|
|
top_axes = Axes(
|
|
x_min = -FRAME_X_RADIUS,
|
|
x_max = FRAME_X_RADIUS,
|
|
y_min = 0,
|
|
y_max = 3,
|
|
y_axis_config = {
|
|
"unit_size" : 0.6,
|
|
"include_tip" : False,
|
|
}
|
|
)
|
|
bottom_axes = top_axes.deepcopy()
|
|
arrow = Vector(DOWN, color = WHITE)
|
|
group = VGroup(
|
|
title, top_axes, arrow, bottom_axes
|
|
)
|
|
group.arrange(DOWN)
|
|
title.shift(MED_SMALL_BUFF*UP)
|
|
group.to_edge(UP)
|
|
fourier_word = TextMobject("Fourier transform")
|
|
fourier_word.next_to(arrow, RIGHT)
|
|
self.add(group, fourier_word)
|
|
|
|
ghost_dot = Dot(RIGHT, fill_opacity = 0)
|
|
def get_bell_func(factor = 1):
|
|
return lambda x : 2*np.exp(-factor*x**2)
|
|
top_graph = top_axes.get_graph(get_bell_func())
|
|
top_graph.set_color(YELLOW)
|
|
bottom_graph = bottom_axes.get_graph(get_bell_func())
|
|
bottom_graph.set_color(RED)
|
|
def get_update_func(axes):
|
|
def update_graph(graph):
|
|
f = ghost_dot.get_center()[0]
|
|
if axes == bottom_axes:
|
|
f = 1./f
|
|
new_graph = axes.get_graph(get_bell_func(f))
|
|
graph.points = new_graph.points
|
|
return update_graph
|
|
|
|
factors = [0.3, 0.1, 2, 10, 100, 0.01, 0.5]
|
|
|
|
self.play(ShowCreation(top_graph))
|
|
self.play(ReplacementTransform(
|
|
top_graph.copy(),
|
|
bottom_graph,
|
|
))
|
|
self.wait(2)
|
|
self.add(*[
|
|
Mobject.add_updater(graph, get_update_func(axes))
|
|
for graph, axes in [(top_graph, top_axes), (bottom_graph, bottom_axes)]
|
|
])
|
|
for factor in factors:
|
|
self.play(
|
|
ghost_dot.move_to, factor*RIGHT,
|
|
run_time = 2
|
|
)
|
|
self.wait()
|
|
|
|
class XCoordinateLabelTypoFix(Scene):
|
|
def construct(self):
|
|
words = TextMobject("$x$-coordinate for center of mass")
|
|
words.set_color(RED)
|
|
self.add(words)
|
|
|
|
class NextVideoWrapper(Scene):
|
|
def construct(self):
|
|
title = TextMobject("Next video")
|
|
title.to_edge(UP)
|
|
screen_rect = ScreenRectangle(height = 6)
|
|
screen_rect.next_to(title, DOWN)
|
|
self.add(title)
|
|
self.play(ShowCreation(screen_rect))
|
|
self.wait(2)
|
|
|
|
class SubscribeOrBinge(PiCreatureScene):
|
|
def construct(self):
|
|
morty = self.pi_creature
|
|
morty.center().to_edge(DOWN, LARGE_BUFF)
|
|
subscribe = TextMobject("Subscribe")
|
|
subscribe.set_color(RED)
|
|
subscribe.next_to(morty, UP+RIGHT)
|
|
binge = TextMobject("Binge")
|
|
binge.set_color(BLUE)
|
|
binge.next_to(morty, UP+LEFT)
|
|
|
|
videos = VGroup(*[VideoIcon() for x in range(30)])
|
|
colors = it.cycle([BLUE_D, BLUE_E, BLUE_C, GREY_BROWN])
|
|
for video, color in zip(videos, colors):
|
|
video.set_color(color)
|
|
videos.move_to(binge.get_bottom(), UP)
|
|
video_anim = LaggedStartMap(
|
|
Succession, videos,
|
|
lambda v : (
|
|
FadeIn, v,
|
|
ApplyMethod, v.shift, 5*DOWN, {"run_time" : 6},
|
|
),
|
|
run_time = 10
|
|
)
|
|
sub_arrow = Arrow(
|
|
subscribe.get_bottom(),
|
|
Dot().to_corner(DOWN+RIGHT, buff = LARGE_BUFF),
|
|
color = RED
|
|
)
|
|
|
|
for word in subscribe, binge:
|
|
word.save_state()
|
|
word.shift(DOWN)
|
|
word.set_fill(opacity = 0)
|
|
|
|
self.play(
|
|
subscribe.restore,
|
|
morty.change, "raise_left_hand"
|
|
)
|
|
self.play(GrowArrow(sub_arrow))
|
|
self.wait()
|
|
self.play(
|
|
video_anim,
|
|
Succession(
|
|
AnimationGroup(
|
|
ApplyMethod(binge.restore),
|
|
ApplyMethod(morty.change, "raise_right_hand", binge),
|
|
),
|
|
Blink, morty,
|
|
ApplyMethod, morty.change, "shruggie", videos,
|
|
Animation, Mobject(), {"run_time" : 2},
|
|
Blink, morty,
|
|
Animation, Mobject(), {"run_time" : 4}
|
|
)
|
|
)
|
|
|
|
class CloseWithAPuzzle(TeacherStudentsScene):
|
|
def construct(self):
|
|
self.teacher_says("Close with a puzzle!", run_time = 1)
|
|
self.change_student_modes(*["hooray"]*3)
|
|
self.wait(3)
|
|
|
|
class PuzzleDescription(Scene):
|
|
def construct(self):
|
|
lines = VGroup(
|
|
TextMobject("Convex set", "$C$", "in $\\mathds{R}^3$"),
|
|
TextMobject("Boundary", "$B$", "$=$", "$\\partial C$"),
|
|
TextMobject("$D$", "$=\\{p+q | p, q \\in B\\}$"),
|
|
TextMobject("Prove that", "$D$", "is convex")
|
|
)
|
|
for line in lines:
|
|
line.set_color_by_tex_to_color_map({
|
|
"$C$" : BLUE_D,
|
|
"\\partial C" : BLUE_D,
|
|
"$B$" : BLUE_C,
|
|
"$D$" : YELLOW,
|
|
})
|
|
VGroup(lines[2][1][2], lines[2][1][6]).set_color(RED)
|
|
VGroup(lines[2][1][4], lines[2][1][8]).set_color(MAROON_B)
|
|
lines[2][1][10].set_color(BLUE_C)
|
|
lines.scale(1.25)
|
|
lines.arrange(DOWN, buff = LARGE_BUFF, aligned_edge = LEFT)
|
|
|
|
lines.to_corner(UP+RIGHT)
|
|
|
|
for line in lines:
|
|
self.play(Write(line))
|
|
self.wait(2)
|
|
|
|
class SponsorScreenGrab(PiCreatureScene):
|
|
def construct(self):
|
|
morty = self.pi_creature
|
|
screen = ScreenRectangle(height = 5)
|
|
screen.to_corner(UP+LEFT)
|
|
screen.shift(MED_LARGE_BUFF*DOWN)
|
|
url = TextMobject("janestreet.com/3b1b")
|
|
url.next_to(screen, UP)
|
|
|
|
self.play(
|
|
morty.change, "raise_right_hand",
|
|
ShowCreation(screen)
|
|
)
|
|
self.play(Write(url))
|
|
self.wait(2)
|
|
for mode in "happy", "thinking", "pondering", "thinking":
|
|
self.play(morty.change, mode, screen)
|
|
self.wait(4)
|
|
|
|
class FourierEndScreen(PatreonEndScreen):
|
|
CONFIG = {
|
|
"specific_patrons" : [
|
|
"CrypticSwarm",
|
|
"Ali Yahya",
|
|
"Juan Benet",
|
|
"Markus Persson",
|
|
"Damion Kistler",
|
|
"Burt Humburg",
|
|
"Yu Jun",
|
|
"Dave Nicponski",
|
|
"Kaustuv DeBiswas",
|
|
"Joseph John Cox",
|
|
"Luc Ritchie",
|
|
"Achille Brighton",
|
|
"Rish Kundalia",
|
|
"Yana Chernobilsky",
|
|
"Shìmín Kuang",
|
|
"Mathew Bramson",
|
|
"Jerry Ling",
|
|
"Mustafa Mahdi",
|
|
"Meshal Alshammari",
|
|
"Mayank M. Mehrotra",
|
|
"Lukas Biewald",
|
|
"Robert Teed",
|
|
"One on Epsilon",
|
|
"Samantha D. Suplee",
|
|
"Mark Govea",
|
|
"John Haley",
|
|
"Julian Pulgarin",
|
|
"Jeff Linse",
|
|
"Cooper Jones",
|
|
"Boris Veselinovich",
|
|
"Ryan Dahl",
|
|
"Ripta Pasay",
|
|
"Eric Lavault",
|
|
"Mads Elvheim",
|
|
"Andrew Busey",
|
|
"Randall Hunt",
|
|
"Desmos",
|
|
"Tianyu Ge",
|
|
"Awoo",
|
|
"Dr David G. Stork",
|
|
"Linh Tran",
|
|
"Jason Hise",
|
|
"Bernd Sing",
|
|
"Ankalagon",
|
|
"Mathias Jansson",
|
|
"David Clark",
|
|
"Ted Suzman",
|
|
"Eric Chow",
|
|
"Michael Gardner",
|
|
"Jonathan Eppele",
|
|
"Clark Gaebel",
|
|
"David Kedmey",
|
|
"Jordan Scales",
|
|
"Ryan Atallah",
|
|
"supershabam",
|
|
"1stViewMaths",
|
|
"Jacob Magnuson",
|
|
"Thomas Tarler",
|
|
"Isak Hietala",
|
|
"James Thornton",
|
|
"Egor Gumenuk",
|
|
"Waleed Hamied",
|
|
"Oliver Steele",
|
|
"Yaw Etse",
|
|
"David B",
|
|
"Julio Cesar Campo Neto",
|
|
"Delton Ding",
|
|
"George Chiesa",
|
|
"Chloe Zhou",
|
|
"Alexander Nye",
|
|
"Ross Garber",
|
|
"Wang HaoRan",
|
|
"Felix Tripier",
|
|
"Arthur Zey",
|
|
"Norton",
|
|
"Kevin Le",
|
|
"Alexander Feldman",
|
|
"David MacCumber",
|
|
],
|
|
}
|
|
|
|
class Thumbnail(Scene):
|
|
def construct(self):
|
|
title = TextMobject("Fourier\\\\", "Visualized")
|
|
title.set_color(YELLOW)
|
|
title.set_stroke(RED, 2)
|
|
title.scale(2.5)
|
|
title.add_background_rectangle()
|
|
|
|
def func(t):
|
|
return np.cos(2*TAU*t) + np.cos(3*TAU*t) + np.cos(5*t)
|
|
fourier = get_fourier_transform(func, -5, 5)
|
|
|
|
graph = FunctionGraph(func, x_min = -5, x_max = 5)
|
|
graph.set_color(BLUE)
|
|
fourier_graph = FunctionGraph(fourier, x_min = 0, x_max = 6)
|
|
fourier_graph.set_color(YELLOW)
|
|
for g in graph, fourier_graph:
|
|
g.stretch_to_fit_height(2)
|
|
g.stretch_to_fit_width(10)
|
|
g.set_stroke(width = 8)
|
|
|
|
pol_graphs = VGroup()
|
|
for f in np.linspace(1.98, 2.02, 7):
|
|
pol_graph = ParametricFunction(
|
|
lambda t : complex_to_R3(
|
|
(2+np.cos(2*TAU*t)+np.cos(3*TAU*t))*np.exp(-complex(0, TAU*f*t))
|
|
),
|
|
t_min = -5,
|
|
t_max = 5,
|
|
num_graph_points = 200,
|
|
)
|
|
pol_graph.match_color(graph)
|
|
pol_graph.set_height(2)
|
|
pol_graphs.add(pol_graph)
|
|
pol_graphs.arrange(RIGHT, buff = LARGE_BUFF)
|
|
pol_graphs.set_color_by_gradient(BLUE_C, YELLOW)
|
|
pol_graphs.match_width(graph)
|
|
pol_graphs.set_stroke(width = 2)
|
|
|
|
|
|
self.clear()
|
|
title.center().to_edge(UP)
|
|
pol_graphs.set_width(FRAME_WIDTH - 1)
|
|
pol_graphs.center()
|
|
title.move_to(pol_graphs)
|
|
title.shift(SMALL_BUFF*LEFT)
|
|
graph.next_to(title, UP)
|
|
fourier_graph.next_to(title, DOWN)
|
|
self.add(pol_graphs, title, graph, fourier_graph)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|