From ddd7ce2f1264e27d59f5fd8d9f45b0327e5b99fc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 May 2019 13:38:45 -0700 Subject: [PATCH] Animations up to the preview for the breakdown into sine curves for diffyq chapter 3 --- active_projects/ode/all_part3_scenes.py | 4 +- .../ode/part3/pi_creature_scenes.py | 41 ++ active_projects/ode/part3/staging.py | 169 ++++++-- .../ode/part3/temperature_graphs.py | 371 ++++++++++++++++-- 4 files changed, 515 insertions(+), 70 deletions(-) create mode 100644 active_projects/ode/part3/pi_creature_scenes.py diff --git a/active_projects/ode/all_part3_scenes.py b/active_projects/ode/all_part3_scenes.py index 32be5c04..dd6d024b 100644 --- a/active_projects/ode/all_part3_scenes.py +++ b/active_projects/ode/all_part3_scenes.py @@ -1,5 +1,6 @@ from active_projects.ode.part3.staging import * from active_projects.ode.part3.temperature_graphs import * +from active_projects.ode.part3.pi_creature_scenes import * OUTPUT_DIRECTORY = "ode/part3" @@ -9,5 +10,6 @@ SCENES_IN_ORDER = [ CircleAnimationOfF, LastChapterWrapper, ThreeMainObservations, - SimpleSinExpGraph, + SimpleCosExpGraph, + AddMultipleSolutions, ] diff --git a/active_projects/ode/part3/pi_creature_scenes.py b/active_projects/ode/part3/pi_creature_scenes.py new file mode 100644 index 00000000..e79bacbb --- /dev/null +++ b/active_projects/ode/part3/pi_creature_scenes.py @@ -0,0 +1,41 @@ +from manimlib.imports import * + + +class IveHeardOfThis(TeacherStudentsScene): + def construct(self): + point = VectorizedPoint() + point.move_to(3 * RIGHT + 2 * UP) + self.student_says( + "I've heard\\\\", "of this!", + student_index=1, + target_mode="hooray", + bubble_kwargs={ + "height": 3, + "width": 3, + "direction": RIGHT, + }, + run_time=1, + ) + self.change_student_modes( + "thinking", "hooray", "thinking", + look_at_arg=point, + added_anims=[self.teacher.change, "happy"] + ) + self.wait(3) + self.student_says( + "But who\\\\", "cares?", + student_index=1, + target_mode="maybe", + bubble_kwargs={ + "direction": RIGHT, + "width": 3, + "height": 3, + }, + run_time=1, + ) + self.change_student_modes( + "pondering", "maybe", "pondering", + look_at_arg=point, + added_anims=[self.teacher.change, "guilty"] + ) + self.wait(5) diff --git a/active_projects/ode/part3/staging.py b/active_projects/ode/part3/staging.py index a91de197..4bd3f8fb 100644 --- a/active_projects/ode/part3/staging.py +++ b/active_projects/ode/part3/staging.py @@ -221,32 +221,21 @@ class FourierSeriesIllustraiton(Scene): y_min=-1, y_max=1, ) - axes2 = axes1.copy() - step_func = axes2.get_graph( - lambda x: (1 if x < 0.5 else -1), - discontinuities=[0.5], - color=YELLOW, - stroke_width=3, + axes1.x_axis.add_numbers( + 0.5, 1, + number_config={"num_decimal_places": 1} ) - dot = Dot(axes2.c2p(0.5, 0), color=step_func.get_color()) - dot.scale(0.5) - step_func.add(dot) - axes2.add(step_func) + axes2 = axes1.copy() + target_func_graph = self.get_target_func_graph(axes2) + axes2.add(target_func_graph) arrow = Arrow(LEFT, RIGHT, color=WHITE) - VGroup(axes1, arrow, axes2).arrange(RIGHT).shift(UP) - - def generate_nth_func(n): - return lambda x: (4 / n / PI) * np.sin(TAU * n * x) - - def generate_kth_partial_sum_func(k): - return lambda x: np.sum([ - generate_nth_func(n)(x) - for n in n_range[:k] - ]) + group = VGroup(axes1, arrow, axes2) + group.arrange(RIGHT, buff=LARGE_BUFF) + group.shift(2 * UP) sine_graphs = VGroup(*[ - axes1.get_graph(generate_nth_func(n)) + axes1.get_graph(self.generate_nth_func(n)) for n in n_range ]) sine_graphs.set_stroke(width=3) @@ -256,44 +245,43 @@ class FourierSeriesIllustraiton(Scene): ) partial_sums = VGroup(*[ - axes1.get_graph(generate_kth_partial_sum_func(k + 1)) + axes1.get_graph(self.generate_kth_partial_sum_func(k + 1)) for k in range(len(n_range)) ]) partial_sums.match_style(sine_graphs) - sum_tex = TexMobject( - "\\frac{4}{\\pi}" - "\\sum_{1, 3, 5, \\dots}" - "\\frac{1}{n} \\sin(2\\pi \\cdot n \\cdot x)" - ) - sum_tex.next_to(partial_sums, DOWN, buff=0.7) + sum_tex = self.get_sum_tex() + sum_tex.next_to(axes1, DOWN, LARGE_BUFF) + sum_tex.shift(RIGHT) eq = TexMobject("=") - step_tex = TexMobject( - """ - 1 \\quad \\text{if $x < 0.5$} \\\\ - 0 \\quad \\text{if $x = 0.5$} \\\\ - -1 \\quad \\text{if $x > 0.5$} \\\\ - """ - ) - lb = Brace(step_tex, LEFT, buff=SMALL_BUFF) - step_tex.add(lb) - step_tex.next_to(axes2, DOWN, buff=MED_LARGE_BUFF) + target_func_tex = self.get_target_func_tex() + target_func_tex.next_to(axes2, DOWN) + target_func_tex.match_y(sum_tex) eq.move_to(midpoint( - step_tex.get_left(), + target_func_tex.get_left(), sum_tex.get_right() )) + range_words = TextMobject( + "For $0 \\le x \\le 1$" + ) + range_words.next_to( + VGroup(sum_tex, target_func_tex), + DOWN, + ) + rects = it.chain( [ - SurroundingRectangle(sum_tex[0][i]) - for i in [4, 6, 8] + SurroundingRectangle(piece) + for piece in self.get_sum_tex_pieces(sum_tex) ], it.cycle([None]) ) self.add(axes1, arrow, axes2) - self.add(step_func) - self.add(sum_tex, eq, step_tex) + self.add(target_func_graph) + self.add(sum_tex, eq, target_func_tex) + self.add(range_words) curr_partial_sum = axes1.get_graph(lambda x: 0) curr_partial_sum.set_stroke(width=1) @@ -320,6 +308,101 @@ class FourierSeriesIllustraiton(Scene): self.play(*anims2) curr_partial_sum = partial_sum + def get_sum_tex(self): + return TexMobject( + "\\frac{4}{\\pi} \\left(", + "\\frac{\\cos(\\pi x)}{1}", + "-\\frac{\\cos(3\\pi x)}{3}", + "+\\frac{\\cos(5\\pi x)}{5}", + "- \\cdots \\right)" + ).scale(0.75) + + def get_sum_tex_pieces(self, sum_tex): + return sum_tex[1:4] + + def get_target_func_tex(self): + step_tex = TexMobject( + """ + 1 \\quad \\text{if $x < 0.5$} \\\\ + 0 \\quad \\text{if $x = 0.5$} \\\\ + -1 \\quad \\text{if $x > 0.5$} \\\\ + """ + ) + lb = Brace(step_tex, LEFT, buff=SMALL_BUFF) + step_tex.add(lb) + return step_tex + + def get_target_func_graph(self, axes): + step_func = axes.get_graph( + lambda x: (1 if x < 0.5 else -1), + discontinuities=[0.5], + color=YELLOW, + stroke_width=3, + ) + dot = Dot(axes.c2p(0.5, 0), color=step_func.get_color()) + dot.scale(0.5) + step_func.add(dot) + return step_func + + # def generate_nth_func(self, n): + # return lambda x: (4 / n / PI) * np.sin(TAU * n * x) + + def generate_nth_func(self, n): + return lambda x: np.prod([ + (4 / PI), + (1 / n) * (-1)**((n - 1) / 2), + np.cos(PI * n * x) + ]) + + def generate_kth_partial_sum_func(self, k): + return lambda x: np.sum([ + self.generate_nth_func(n)(x) + for n in self.n_range[:k] + ]) + + +class FourierSeriesOfLineIllustration(FourierSeriesIllustraiton): + CONFIG = { + "n_range": range(1, 31, 2) + } + + def get_sum_tex(self): + return TexMobject( + "\\frac{8}{\\pi^2} \\left(", + "\\frac{\\cos(\\pi x)}{1^2}", + "+\\frac{\\cos(3\\pi x)}{3^2}", + "+\\frac{\\cos(5\\pi x)}{5^2}", + "+ \\cdots \\right)" + ).scale(0.75) + + # def get_sum_tex_pieces(self, sum_tex): + # return sum_tex[1:4] + + def get_target_func_tex(self): + result = TexMobject("1 - 2x") + result.scale(1.5) + point = VectorizedPoint() + point.next_to(result, RIGHT, 1.5 * LARGE_BUFF) + # result.add(point) + return result + + def get_target_func_graph(self, axes): + return axes.get_graph( + lambda x: 1 - 2 * x, + color=YELLOW, + stroke_width=3, + ) + + # def generate_nth_func(self, n): + # return lambda x: (4 / n / PI) * np.sin(TAU * n * x) + + def generate_nth_func(self, n): + return lambda x: np.prod([ + (8 / PI**2), + (1 / n**2), + np.cos(PI * n * x) + ]) + class CircleAnimationOfF(FourierOfTrebleClef): CONFIG = { diff --git a/active_projects/ode/part3/temperature_graphs.py b/active_projects/ode/part3/temperature_graphs.py index 35f1b472..4d0df958 100644 --- a/active_projects/ode/part3/temperature_graphs.py +++ b/active_projects/ode/part3/temperature_graphs.py @@ -1,3 +1,5 @@ +from scipy import integrate + from manimlib.imports import * @@ -30,8 +32,10 @@ class TemperatureGraphScene(SpecialThreeDScene): }, } - def get_three_d_axes(self, include_labels=True): - axes = ThreeDAxes(**self.axes_config) + def get_three_d_axes(self, include_labels=True, **kwargs): + config = dict(self.axes_config) + config.update(kwargs) + axes = ThreeDAxes(**config) axes.set_stroke(width=2) # Add number labels @@ -41,17 +45,19 @@ class TemperatureGraphScene(SpecialThreeDScene): if include_labels: x_label = TexMobject("x") x_label.next_to(axes.x_axis.get_right(), DOWN) - axes.x_axis.add(x_label) + axes.x_axis.label = x_label t_label = TextMobject("Time") t_label.rotate(90 * DEGREES, OUT) t_label.next_to(axes.y_axis.get_top(), DL) - axes.y_axis.add(t_label) + axes.y_axis.label = t_label temp_label = TextMobject("Temperature") temp_label.rotate(90 * DEGREES, RIGHT) temp_label.next_to(axes.z_axis.get_zenith(), RIGHT) - axes.z_axis.add(temp_label) + axes.z_axis.label = temp_label + for axis in axes: + axis.add(axis.label) # Adjust axis orinetations axes.x_axis.rotate( @@ -88,19 +94,24 @@ class TemperatureGraphScene(SpecialThreeDScene): return axes - def get_initial_state_graph(self, axes, func, **kwargs): + def get_time_slice_graph(self, axes, func, t, **kwargs): config = dict() config.update(self.default_graph_style) config.update(kwargs) return ParametricFunction( lambda x: axes.c2p( - x, 0, func(x) + x, t, func(x) ), t_min=axes.x_min, t_max=axes.x_max, **config, ) + def get_initial_state_graph(self, axes, func, **kwargs): + return self.get_time_slice_graph( + axes, func, t=0, **kwargs + ) + def get_surface(self, axes, func, **kwargs): config = dict() config.update(axes.surface_config) @@ -121,11 +132,11 @@ class TemperatureGraphScene(SpecialThreeDScene): return mobject -class SimpleSinExpGraph(TemperatureGraphScene): +class SimpleCosExpGraph(TemperatureGraphScene): def construct(self): axes = self.get_three_d_axes() - sine_graph = self.get_sine_graph(axes) - sine_exp_surface = self.get_sine_exp_surface(axes) + cos_graph = self.get_cos_graph(axes) + cos_exp_surface = self.get_cos_exp_surface(axes) self.set_camera_orientation( phi=80 * DEGREES, @@ -135,36 +146,73 @@ class SimpleSinExpGraph(TemperatureGraphScene): self.begin_ambient_camera_rotation(rate=0.01) self.add(axes) - self.play(ShowCreation(sine_graph)) + self.play(ShowCreation(cos_graph)) self.play(UpdateFromAlphaFunc( - sine_exp_surface, + cos_exp_surface, lambda m, a: m.become( - self.get_sine_exp_surface(axes, v_max=a * 10) + self.get_cos_exp_surface(axes, v_max=a * 10) ), run_time=3 )) - self.wait(20) + + self.add(cos_graph.copy()) + + t_tracker = ValueTracker(0) + get_t = t_tracker.get_value + cos_graph.add_updater( + lambda m: m.become(self.get_time_slice_graph( + axes, + lambda x: self.cos_exp(x, get_t()), + t=get_t() + )) + ) + + plane = Rectangle( + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.1, + ) + plane.rotate(90 * DEGREES, RIGHT) + plane.match_width(axes.x_axis) + plane.match_depth(axes.z_axis, stretch=True) + plane.move_to(axes.c2p(0, 0, 0), LEFT) + + self.add(plane, cos_graph) + self.play( + ApplyMethod( + t_tracker.set_value, 10, + run_time=10, + rate_func=linear, + ), + ApplyMethod( + plane.shift, 10 * UP, + run_time=10, + rate_func=linear, + ), + VFadeIn(plane), + ) + self.wait(10) # - def sin_exp(self, x, t, A=2, omega=1, k=0.25): - return A * np.sin(omega * x) * np.exp(-k * (omega**2) * t) + def cos_exp(self, x, t, A=2, omega=1.5, k=0.1): + return A * np.cos(omega * x) * np.exp(-k * (omega**2) * t) - def get_sine_graph(self, axes, **config): + def get_cos_graph(self, axes, **config): return self.get_initial_state_graph( axes, - lambda x: self.sin_exp(x, 0), + lambda x: self.cos_exp(x, 0), **config ) - def get_sine_exp_surface(self, axes, **config): + def get_cos_exp_surface(self, axes, **config): return self.get_surface( axes, - lambda x, t: self.sin_exp(x, t), + lambda x, t: self.cos_exp(x, t), **config ) -class AddMultipleSolutions(SimpleSinExpGraph): +class AddMultipleSolutions(SimpleCosExpGraph): CONFIG = { "axes_config": { "x_axis_config": { @@ -184,8 +232,8 @@ class AddMultipleSolutions(SimpleSinExpGraph): self.orient_three_d_mobject(all_axes) As = [1.5, 1.5] - omegas = [1, 2] - ks = [0.25, 0.01] + omegas = [1.5, 2.5] + ks = [0.1, 0.1] quads = [ (axes1, [As[0]], [omegas[0]], [ks[0]]), (axes2, [As[1]], [omegas[1]], [ks[1]]), @@ -196,15 +244,15 @@ class AddMultipleSolutions(SimpleSinExpGraph): graph = self.get_initial_state_graph( axes, lambda x: np.sum([ - self.sin_exp(x, 0, A, omega, k) + self.cos_exp(x, 0, A, omega, k) for A, omega, k in zip(As, omegas, ks) ]) ) surface = self.get_surface( axes, lambda x, t: np.sum([ - self.sin_exp(x, t, A, omega) - for A, omega in zip(As, omegas) + self.cos_exp(x, t, A, omega, k) + for A, omega, k in zip(As, omegas, ks) ]) ) surface.sort(lambda p: -p[2]) @@ -259,3 +307,274 @@ class AddMultipleSolutions(SimpleSinExpGraph): FadeInFrom(axes3.checkmark, DOWN), ) self.wait() + + +class BreakDownAFunction(SimpleCosExpGraph): + CONFIG = { + "axes_config": { + "z_axis_config": { + "unit_size": 0.75, + "include_tip": False, + }, + "z_min": 0, + }, + "n_low_axes": 4, + } + + def construct(self): + self.set_camera_orientation(distance=100) + self.set_axes() + self.setup_graphs() + self.show_break_down() + self.show_solutions_for_waves() + + def set_axes(self): + top_axes = self.get_three_d_axes() + top_axes.z_axis.label.next_to( + top_axes.z_axis.get_end(), OUT, SMALL_BUFF + ) + top_axes.y_axis.set_opacity(0) + self.orient_three_d_mobject(top_axes) + top_axes.y_axis.label.rotate(-10 * DEGREES, UP) + top_axes.scale(0.75) + top_axes.center() + top_axes.to_edge(UP) + + low_axes = self.get_three_d_axes( + z_min=-3, + z_axis_config={"unit_size": 1} + ) + low_axes.y_axis.set_opacity(0) + for axis in low_axes: + axis.label.fade(1) + # low_axes.add(low_axes.input_plane) + # low_axes.input_plane.set_opacity(0) + + self.orient_three_d_mobject(low_axes) + low_axes_group = VGroup(*[ + low_axes.deepcopy() + for x in range(self.n_low_axes) + ]) + low_axes_group.arrange( + RIGHT, buff=low_axes.get_width() / 3 + ) + low_axes_group.set_width(FRAME_WIDTH - 2.5) + low_axes_group.next_to(top_axes, DOWN, LARGE_BUFF) + low_axes_group.to_edge(LEFT) + + self.top_axes = top_axes + self.low_axes_group = low_axes_group + + def setup_graphs(self): + top_axes = self.top_axes + low_axes_group = self.low_axes_group + + top_graph = self.get_initial_state_graph( + top_axes, + self.initial_func, + discontinuities=self.get_initial_func_discontinuities(), + color=YELLOW, + ) + + fourier_terms = self.get_fourier_cosine_terms( + self.initial_func + ) + + low_graphs = VGroup(*[ + self.get_initial_state_graph( + axes, + lambda x: A * np.cos(n * x / 2) + ) + for n, axes, A in zip( + it.count(0, 2), + low_axes_group, + fourier_terms[::2], + ) + ]) + k = 0.1 + low_surfaces = VGroup(*[ + self.get_surface( + axes, + lambda x, t: np.prod([ + A, + np.cos(n * x / 2), + np.exp(-k * (n / 2)**2 * t) + ]) + ) + for n, axes, A in zip( + it.count(0, 2), + low_axes_group, + fourier_terms[::2], + ) + ]) + top_surface = self.get_surface( + top_axes, + lambda x, t: np.sum([ + np.prod([ + A, + np.cos(n * x / 2), + np.exp(-k * (n / 2)**2 * t) + ]) + for n, A in zip( + it.count(0, 2), + fourier_terms[::2] + ) + ]) + ) + + self.top_graph = top_graph + self.low_graphs = low_graphs + self.low_surfaces = low_surfaces + self.top_surface = top_surface + + def show_break_down(self): + top_axes = self.top_axes + low_axes_group = self.low_axes_group + top_graph = self.top_graph + low_graphs = self.low_graphs + + plusses = VGroup(*[ + TexMobject("+").next_to( + axes.x_axis.get_end(), + RIGHT, MED_LARGE_BUFF + ) + for axes in low_axes_group + ]) + dots = TexMobject("\\cdots") + dots.next_to(plusses, RIGHT, MED_SMALL_BUFF) + arrow = Arrow( + dots.get_right(), + top_axes.get_right(), + path_arc=110 * DEGREES, + ) + + top_words = TextMobject("Arbitrary\\\\function") + top_words.next_to(top_axes, LEFT, MED_LARGE_BUFF) + top_words.set_color(YELLOW) + top_arrow = Arrow( + top_words.get_right(), + top_graph.get_center() + LEFT, + ) + + low_words = TextMobject("Sine curves") + low_words.set_color(BLUE) + low_words.next_to(low_axes_group, DOWN, MED_LARGE_BUFF) + + self.add(top_axes) + self.play(ShowCreation(top_graph)) + self.play( + FadeInFrom(top_words, RIGHT), + ShowCreation(top_arrow) + ) + self.wait() + self.play( + LaggedStartMap(FadeIn, low_axes_group), + *[ + TransformFromCopy(top_graph, low_graph) + for low_graph in low_graphs + ] + ) + self.play(FadeInFrom(low_words, UP)) + self.wait() + self.play( + LaggedStartMap(FadeInFromDown, plusses), + Write(dots) + ) + self.play(ShowCreation(arrow)) + self.wait() + + def show_solutions_for_waves(self): + low_axes_group = self.low_axes_group + top_axes = self.top_axes + low_graphs = self.low_graphs + low_surfaces = self.low_surfaces + top_surface = self.top_surface + top_graph = self.top_graph + + for surface in [top_surface, *low_surfaces]: + surface.sort(lambda p: -p[2]) + + anims1 = [] + anims2 = [] + for axes, surface, graph in zip(low_axes_group, low_surfaces, low_graphs): + axes.y_axis.set_opacity(1) + axes.y_axis.label.fade(1) + anims1 += [ + ShowCreation(axes.y_axis), + Write(surface, run_time=2), + ] + anims2.append(AnimationGroup( + TransformFromCopy(graph, top_graph.copy()), + Transform( + surface.copy().fade(1), + top_surface, + ) + )) + + self.play(*anims1) + self.wait() + self.play(LaggedStart(*anims2, run_time=2)) + self.wait() + + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + low_checkmarks = VGroup(*[ + checkmark.copy().next_to( + surface.get_top(), UP, SMALL_BUFF + ) + for surface in low_surfaces + ]) + top_checkmark = checkmark.copy() + top_checkmark.scale(1.5) + top_checkmark.move_to(top_axes.get_corner(UR)) + + self.play(LaggedStartMap(FadeInFromDown, low_checkmarks)) + self.wait() + self.play(TransformFromCopy( + low_checkmarks, VGroup(top_checkmark) + )) + self.wait() + + # + def initial_func(self, x): + return 3 * np.exp(-(x - PI)**2) + + x1 = TAU / 4 - 0.1 + x2 = TAU / 4 + 0.1 + x3 = 3 * TAU / 4 - 0.1 + x4 = 3 * TAU / 4 + 0.1 + + T0 = -2 + T1 = 2 + + if x < x1: + return T0 + elif x < x2: + return interpolate( + T0, T1, + inverse_interpolate(x1, x2, x) + ) + elif x < x3: + return T1 + elif x < x4: + return interpolate( + T1, T0, + inverse_interpolate(x3, x4, x) + ) + else: + return T0 + + def get_initial_func_discontinuities(self): + # return [TAU / 4, 3 * TAU / 4] + return [] + + def get_fourier_cosine_terms(self, func, n_terms=20): + result = [ + integrate.quad( + lambda x: (1 / PI) * func(x) * np.cos(n * x / 2), + 0, TAU + )[0] + for n in range(n_terms) + ] + result[0] = result[0] / 2 + return result