3b1b-manim/active_projects/fourier.py

680 lines
21 KiB
Python
Raw Normal View History

2018-01-11 18:13:50 -08:00
from helpers import *
from animation.animation import Animation
from animation.transform import *
from animation.simple_animations import *
from animation.playground import *
from animation.continual_animation import *
from topics.geometry import *
from topics.characters import *
from topics.functions import *
from topics.fractals import *
from topics.number_line import *
from topics.combinatorics import *
from topics.numerals import *
from topics.three_dimensions import *
from topics.objects import *
from topics.probability import *
from topics.complex_numbers import *
from scene import Scene
from scene.reconfigurable_scene import ReconfigurableScene
from scene.zoomed_scene import *
from camera import Camera
from mobject import *
from mobject.image_mobject import *
from mobject.vectorized_mobject import *
from mobject.svg_mobject import *
from mobject.tex_mobject import *
from topics.graph_scene import *
#revert_to_original_skipping_status
2018-01-11 18:13:50 -08:00
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,
}
2018-01-11 18:13:50 -08:00
def construct(self):
self.add_speaker()
self.play_A440()
2018-01-11 18:13:50 -08:00
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()
2018-01-11 18:13:50 -08:00
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):
2018-01-11 18:13:50 -08:00
randy = self.pi_creature
A_label = TextMobject("A440")
A_label.highlight(self.A_color)
A_label.next_to(self.speaker, UP)
2018-01-11 18:13:50 -08:00
self.broadcast(
FadeIn(A_label),
Succession(
ApplyMethod, randy.change, "pondering",
Animation, randy,
Blink, randy
)
2018-01-11 18:13:50 -08:00
)
self.set_variables_as_attrs(A_label)
2018-01-11 18:13:50 -08:00
def measure_air_pressure(self):
randy = self.pi_creature
axes = Axes(
y_min = -2, y_max = 2,
2018-01-11 18:13:50 -08:00
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
)
2018-01-11 18:13:50 -08:00
frequency = self.A_frequency
graph = self.get_wave_graph(frequency, axes)
func = graph.underlying_function
graph.highlight(self.A_color)
2018-01-11 18:13:50 -08:00
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)
2018-01-11 18:13:50 -08:00
time.next_to(axes.x_axis.get_right(), DOWN+LEFT)
axes.labels = VGroup(pressure, time)
n = 10
2018-01-11 18:13:50 -08:00
brace = Brace(Line(
axes.coords_to_point(n/frequency, func(n/frequency)),
axes.coords_to_point((n+1)/frequency, func((n+1)/frequency)),
2018-01-11 18:13:50 -08:00
), UP)
words = brace.get_text("Imagine 440 per second", buff = SMALL_BUFF)
words.scale(0.8, about_point = words.get_bottom())
2018-01-11 18:13:50 -08:00
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 = None),
ShowCreation(equilibrium_line),
2018-01-11 18:13:50 -08:00
)
axes.add(equilibrium_line)
2018-01-11 18:13:50 -08:00
self.play(
randy.change, "erm", graph,
2018-01-11 18:13:50 -08:00
GrowFromCenter(brace),
Write(words)
)
self.dither()
2018-01-11 18:13:50 -08:00
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,
2018-01-11 18:13:50 -08:00
)
graph.saved_state.move_to(graph)
self.set_variables_as_attrs(axes, A_graph = graph)
2018-01-11 18:13:50 -08:00
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.highlight(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 = None),
randy.change, "happy",
n_circles = 6,
)
self.play(randy.change, "confused", graph)
self.dither(2)
self.set_variables_as_attrs(
D_label,
D_graph = graph
)
2018-01-11 18:13:50 -08:00
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 = None)
for graph in self.A_graph, self.D_graph
]
)
self.dither()
2018-01-11 18:13:50 -08:00
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(
map(MoveToTarget, movers),
[
ApplyMethod(mob.shift, SPACE_HEIGHT*DOWN, remover = True)
for mob in randy, speaker
]
))
self.dither()
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(SPACE_HEIGHT).move_to(x_point)
self.revert_to_original_skipping_status()
self.play(GrowFromCenter(v_line))
self.play(FadeOut(v_line))
self.play(*map(ShowCreation, lines))
self.dither()
self.play(MoveToTarget(sum_lines, path_arc = np.pi/4))
self.dither(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),
]
])
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.highlight(self.sum_color)
##TODO
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 = None
)
self.dither()
2018-01-11 18:13:50 -08:00
self.sum_graph = sum_graph
2018-01-11 18:13:50 -08:00
def add_more_notes(self):
axes = self.axes
2018-01-11 18:13:50 -08:00
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))
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.highlight(self.F_color)
C_graph = self.get_wave_graph(self.A_frequency*6.0/5, C_axes)
C_graph.highlight(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.highlight(graph.get_stroke_color())
label.next_to(graph, UP, SMALL_BUFF)
graphs = [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.highlight(BLUE_C)
self.play(*it.chain(
map(ShowCreation, [F_axes, C_axes, F_graph, C_graph,]),
map(Write, [F_label, C_label]),
[FadeOut(self.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.dither()
2018-01-11 18:13:50 -08:00
####
def broadcast(self, *added_anims, **kwargs):
self.play(self.get_broadcast_animation(**kwargs), *added_anims)
def get_broadcast_animation(self, **kwargs):
2018-01-11 18:13:50 -08:00
kwargs["run_time"] = kwargs.get("run_time", 5)
kwargs["n_circles"] = kwargs.get("n_circles", 10)
return Broadcast(self.speaker[1], **kwargs)
2018-01-11 18:13:50 -08:00
def get_wave_graph(self, frequency, axes):
2018-01-11 18:13:50 -08:00
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)
2018-01-11 18:13:50 -08:00
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 = 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.highlight(self.A_color)
D_line.highlight(self.D_color)
return lines
2018-01-11 18:13:50 -08:00
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
2018-01-11 18:13:50 -08:00
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(2*SPACE_WIDTH - 2)
axes.stretch_to_fit_height(3)
axes.center()
axes.to_edge(LEFT)
graph = axes.get_graph(func, num_graph_points = 200)
graph.highlight(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 = None,
)
self.play(*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.gradient_highlight(BLUE, RED)
pure_graphs.arrange_submobjects(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.dither()
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)
2018-01-11 18:13:50 -08:00
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),
LaggedStart(
ApplyFunction, pure_graphs,
lambda g : (lambda m : m.shift(SMALL_BUFF*UP).highlight(YELLOW), g),
rate_func = wiggle
)
)
self.play(FadeOut(rect))
self.play(Blink(randy))
self.dither()
2018-01-11 18:13:50 -08:00
class Quadrant(Mobject1D):
CONFIG = {
"radius" : 2,
"stroke_width": 2,
2018-01-15 18:49:58 -08:00
"density" : 45,
}
def generate_points(self):
self.add_points([
r*(np.cos(theta)*RIGHT + np.sin(theta)*UP)
for r in np.arange(0, self.radius, self.epsilon)
for theta in np.arange(0, np.pi/2, self.epsilon/r)
])
class UnmixMixedPaint(Scene):
CONFIG = {
"colors" : [BLUE, RED, YELLOW, GREEN],
}
def construct(self):
angles = np.arange(4)*np.pi/2
quadrants = PMobject(*[
Quadrant().rotate(angle).highlight(color)
for color, angle in zip(self.colors, angles)
])
quadrants.ingest_submobjects()
quadrants.sort_points(lambda p : np.random.random())
2018-01-15 18:49:58 -08:00
mud_color = average_color(*self.colors)
background_circle = Circle(
radius = 2,
stroke_width = 0,
fill_color = mud_color,
fill_opacity = 1
)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
# start_rgbas = np.array(quadrants.rgbas)
# target_rgbas = np.zeros(start_rgbas.shape)
# target_rgbas[:,:] = color_to_rgba(mud_color)
# def update_color(quadrants, alpha):
# quadrants.rgbas = interpolate(start_rgbas, target_rgbas, alpha)
dot = Dot()
permutation = range(len(quadrants.points))
random.shuffle(permutation)
def update_quadrants(quadrants, alpha):
points = quadrants.points
points += dot.get_center()
points[:,0] -= points[:,1]
points -= dot.get_center()
quadrants.points = interpolate(
quadrants.points, quadrants.points[permutation],
0.01,
)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
self.add(quadrants)
self.play(
2018-01-15 18:49:58 -08:00
# PhaseFlow(
# lambda p : rotate_vector(p, np.pi/2+0.01)/(max(np.linalg.norm(p), 0.001)),
# quadrants,
# virtual_time = 20,
# ),
# UpdateFromAlphaFunc(quadrants, update_color),
UpdateFromAlphaFunc(quadrants, update_quadrants),
run_time = 3,
)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
#Incomplete, and probably not useful
class MachineThatTreatsOneFrequencyDifferently(Scene):
def construct(self):
graph = self.get_cosine_graph(0.5)
frequency_mob = DecimalNumber(220, num_decimal_points = 0)
frequency_mob.next_to(graph, UP, buff = MED_LARGE_BUFF)
self.graph = graph
self.frequency_mob = frequency_mob
self.add(graph, frequency_mob)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
arrow1, q_marks, arrow2 = group = VGroup(
Vector(DOWN), TextMobject("???").scale(1.5), Vector(DOWN)
)
group.highlight(WHITE)
group.arrange_submobjects(DOWN)
group.next_to(graph, DOWN)
self.add(group)
2018-01-15 18:49:58 -08:00
self.change_graph_frequency(1)
graph.highlight(GREEN)
self.dither()
graph.highlight(YELLOW)
self.change_graph_frequency(2)
self.dither()
2018-01-15 18:49:58 -08:00
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
2018-01-15 18:49:58 -08:00
class FourierMachineScene(Scene):
CONFIG = {
2018-01-15 18:49:58 -08:00
}
def get_time_graph_axes(self):
pass
2018-01-15 18:49:58 -08:00
class FourierMachineSceneTest(FourierMachineScene):
def construct(self):
pass