From 828c3dcd7a2ae0249ef1eb915ef609cfc784a8f1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 27 May 2019 19:47:57 -0700 Subject: [PATCH 1/4] Added c2p and p2c abbreviations to Axes --- manimlib/mobject/coordinate_systems.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index bddf3548..177d6345 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -171,12 +171,18 @@ class Axes(VGroup, CoordinateSystem): result += (axis.number_to_point(coord) - origin) return result + def c2p(self, *coords): + return self.coords_to_point(*coords) + def point_to_coords(self, point): return tuple([ axis.point_to_number(point) for axis in self.get_axes() ]) + def p2c(self, point): + return self.point_to_coords(point) + def get_axes(self): return self.axes From d1e3e5ed208c84af92eba930fbe3e91a9af642a8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 27 May 2019 19:48:14 -0700 Subject: [PATCH 2/4] Formatting correction --- manimlib/mobject/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index 12ebb118..b081fc41 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -8,7 +8,7 @@ class ParametricFunction(VMobject): CONFIG = { "t_min": 0, "t_max": 1, - "step_size": 0.01, # use "auto" (lwoercase) for automatic step size + "step_size": 0.01, # Use "auto" (lowercase) for automatic step size "dt": 1e-8, # TODO, be smarter about figuring these out? "discontinuities": [], From 29424eb6b36c55437a9937b5e8d10a86f64ec56a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 27 May 2019 19:48:33 -0700 Subject: [PATCH 3/4] Added simple midpoint function --- manimlib/utils/space_ops.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 196707ca..b0113f92 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -205,6 +205,10 @@ def center_of_mass(points): return sum(points) / len(points) +def midpoint(point1, point2): + return center_of_mass([point1, point2]) + + def line_intersection(line1, line2): """ return intersection point of two lines, From e6eb4dd94f5e6d2ddae5a1cc8e48e8c6ad56e694 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 27 May 2019 19:48:48 -0700 Subject: [PATCH 4/4] Beginning heat equation solution animations --- active_projects/ode/all_part3_scenes.py | 13 + active_projects/ode/part2/fourier_series.py | 2 +- active_projects/ode/part3/staging.py | 427 ++++++++++++++++++ .../ode/part3/temperature_graphs.py | 261 +++++++++++ 4 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 active_projects/ode/all_part3_scenes.py create mode 100644 active_projects/ode/part3/staging.py create mode 100644 active_projects/ode/part3/temperature_graphs.py diff --git a/active_projects/ode/all_part3_scenes.py b/active_projects/ode/all_part3_scenes.py new file mode 100644 index 00000000..32be5c04 --- /dev/null +++ b/active_projects/ode/all_part3_scenes.py @@ -0,0 +1,13 @@ +from active_projects.ode.part3.staging import * +from active_projects.ode.part3.temperature_graphs import * + + +OUTPUT_DIRECTORY = "ode/part3" +SCENES_IN_ORDER = [ + FourierSeriesIllustraiton, + FourierNameIntro, + CircleAnimationOfF, + LastChapterWrapper, + ThreeMainObservations, + SimpleSinExpGraph, +] diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py index 28460f73..e9544f4c 100644 --- a/active_projects/ode/part2/fourier_series.py +++ b/active_projects/ode/part2/fourier_series.py @@ -527,7 +527,7 @@ class FourierNDQ(FourierOfTrebleClef): def get_shape(self): path = VMobject() - shape = TexMobject("Hayley") + shape = TexMobject("NDQ") for sp in shape.family_members_with_points(): path.append_points(sp.points) return path diff --git a/active_projects/ode/part3/staging.py b/active_projects/ode/part3/staging.py new file mode 100644 index 00000000..a91de197 --- /dev/null +++ b/active_projects/ode/part3/staging.py @@ -0,0 +1,427 @@ +from manimlib.imports import * + +from active_projects.ode.part2.fourier_series import FourierOfTrebleClef + + +class FourierNameIntro(Scene): + def construct(self): + self.show_two_titles() + self.transition_to_image() + self.show_paper() + + def show_two_titles(self): + lt = TextMobject("Fourier", "Series") + rt = TextMobject("Fourier", "Transform") + lt_variants = VGroup( + TextMobject("Complex", "Fourier Series"), + TextMobject("Discrete", "Fourier Series"), + ) + rt_variants = VGroup( + TextMobject("Discrete", "Fourier Transform"), + TextMobject("Fast", "Fourier Transform"), + TextMobject("Quantum", "Fourier Transform"), + ) + + titles = VGroup(lt, rt) + titles.scale(1.5) + for title, vect in (lt, LEFT), (rt, RIGHT): + title.move_to(vect * FRAME_WIDTH / 4) + title.to_edge(UP) + + for title, variants in (lt, lt_variants), (rt, rt_variants): + title.save_state() + title.target = title.copy() + title.target.scale(1 / 1.5, about_edge=RIGHT) + for variant in variants: + variant.move_to(title.target, UR) + variant[0].set_color(YELLOW) + + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.set_stroke(WHITE, 2) + + self.play( + FadeInFrom(lt, RIGHT), + ShowCreation(v_line) + ) + self.play( + FadeInFrom(rt, LEFT), + ) + # Edit in images of circle animations + # and clips from FT video + + # for title, variants in (rt, rt_variants), (lt, lt_variants): + for title, variants in [(rt, rt_variants)]: + # Maybe do it for left variant, maybe not... + self.play( + MoveToTarget(title), + FadeInFrom(variants[0][0], LEFT) + ) + for v1, v2 in zip(variants, variants[1:]): + self.play( + FadeOutAndShift(v1[0], UP), + FadeInFrom(v2[0], DOWN), + run_time=0.5, + ) + self.wait(0.5) + self.play( + Restore(title), + FadeOut(variants[-1][0]) + ) + self.wait() + + self.titles = titles + self.v_line = v_line + + def transition_to_image(self): + titles = self.titles + v_line = self.v_line + + image = ImageMobject("Joseph Fourier") + image.set_height(5) + image.to_edge(LEFT) + + frame = Rectangle() + frame.replace(image, stretch=True) + + name = TextMobject("Joseph", "Fourier") + fourier_part = name.get_part_by_tex("Fourier") + fourier_part.set_color(YELLOW) + F_sym = fourier_part[0] + name.match_width(image) + name.next_to(image, DOWN) + + self.play( + ReplacementTransform(v_line, frame), + FadeIn(image), + FadeIn(name[0]), + *[ + ReplacementTransform( + title[0].deepcopy(), + name[1] + ) + for title in titles + ], + titles.scale, 0.65, + titles.arrange, DOWN, + titles.next_to, image, UP, + ) + self.wait() + + big_F = F_sym.copy() + big_F.set_fill(opacity=0) + big_F.set_stroke(WHITE, 2) + big_F.set_height(3) + big_F.move_to(midpoint( + image.get_right(), + RIGHT_SIDE, + )) + big_F.shift(DOWN) + equivalence = VGroup( + fourier_part.copy().scale(1.25), + TexMobject("\\Leftrightarrow").scale(1.5), + TextMobject("Break down into\\\\pure frequencies"), + ) + equivalence.arrange(RIGHT) + equivalence.move_to(big_F) + equivalence.to_edge(UP) + + self.play( + FadeIn(big_F), + TransformFromCopy(fourier_part, equivalence[0]), + Write(equivalence[1:]), + ) + self.wait(3) + self.play(FadeOut(VGroup(big_F, equivalence))) + + self.image = image + self.name = name + + def show_paper(self): + image = self.image + paper = ImageMobject("Fourier paper") + paper.match_height(image) + paper.next_to(image, RIGHT, MED_LARGE_BUFF) + + date = TexMobject("1822") + date.next_to(paper, DOWN) + date_rect = SurroundingRectangle(date) + date_rect.scale(0.3) + date_rect.set_color(RED) + date_rect.shift(1.37 * UP + 0.08 * LEFT) + date_arrow = Arrow( + date_rect.get_bottom(), + date.get_top(), + buff=SMALL_BUFF, + color=date_rect.get_color(), + ) + + heat_rect = SurroundingRectangle( + TextMobject("CHALEUR") + ) + heat_rect.set_color(RED) + heat_rect.scale(0.6) + heat_rect.move_to( + paper.get_top() + + 1.22 * DOWN + 0.37 * RIGHT + ) + heat_word = TextMobject("Heat") + heat_word.scale(1.5) + heat_word.next_to(paper, UP) + heat_word.shift(paper.get_width() * RIGHT) + heat_arrow = Arrow( + heat_rect.get_top(), + heat_word.get_left(), + buff=0.1, + path_arc=-60 * DEGREES, + color=heat_rect.get_color(), + ) + + self.play(FadeInFrom(paper, LEFT)) + self.play( + ShowCreation(date_rect), + ) + self.play( + GrowFromPoint(date, date_arrow.get_start()), + ShowCreation(date_arrow), + ) + self.wait(3) + + # Insert animation of circles/sine waves + # approximating a square wave + + self.play( + ShowCreation(heat_rect), + ) + self.play( + GrowFromPoint(heat_word, heat_arrow.get_start()), + ShowCreation(heat_arrow), + ) + self.wait(3) + + +class FourierSeriesIllustraiton(Scene): + CONFIG = { + "n_range": range(1, 31, 2), + } + + def construct(self): + n_range = self.n_range + + axes1 = Axes( + number_line_config={ + "include_tip": False, + }, + x_axis_config={ + "tick_frequency": 1 / 4, + "unit_size": 4, + }, + x_min=0, + x_max=1, + 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, + ) + dot = Dot(axes2.c2p(0.5, 0), color=step_func.get_color()) + dot.scale(0.5) + step_func.add(dot) + axes2.add(step_func) + + 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] + ]) + + sine_graphs = VGroup(*[ + axes1.get_graph(generate_nth_func(n)) + for n in n_range + ]) + sine_graphs.set_stroke(width=3) + sine_graphs.set_color_by_gradient( + BLUE, GREEN, RED, YELLOW, PINK, + BLUE, GREEN, RED, YELLOW, PINK, + ) + + partial_sums = VGroup(*[ + axes1.get_graph(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) + 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) + eq.move_to(midpoint( + step_tex.get_left(), + sum_tex.get_right() + )) + + rects = it.chain( + [ + SurroundingRectangle(sum_tex[0][i]) + for i in [4, 6, 8] + ], + it.cycle([None]) + ) + + self.add(axes1, arrow, axes2) + self.add(step_func) + self.add(sum_tex, eq, step_tex) + + curr_partial_sum = axes1.get_graph(lambda x: 0) + curr_partial_sum.set_stroke(width=1) + for sine_graph, partial_sum, rect in zip(sine_graphs, partial_sums, rects): + anims1 = [ + ShowCreation(sine_graph) + ] + partial_sum.set_stroke(BLACK, 4, background=True) + anims2 = [ + curr_partial_sum.set_stroke, + {"width": 1, "opacity": 0.5}, + curr_partial_sum.set_stroke, + {"width": 0, "background": True}, + ReplacementTransform( + sine_graph, partial_sum, + remover=True + ), + ] + if rect: + rect.match_style(sine_graph) + anims1.append(ShowCreation(rect)) + anims2.append(FadeOut(rect)) + self.play(*anims1) + self.play(*anims2) + curr_partial_sum = partial_sum + + +class CircleAnimationOfF(FourierOfTrebleClef): + CONFIG = { + "height": 3, + "n_circles": 200, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + path = VMobject() + shape = TexMobject("F") + for sp in shape.family_members_with_points(): + path.append_points(sp.points) + return path + + +class LastChapterWrapper(Scene): + def construct(self): + full_rect = FullScreenFadeRectangle( + fill_color=DARK_GREY, + fill_opacity=1, + ) + rect = ScreenRectangle(height=6) + rect.set_stroke(WHITE, 2) + rect.set_fill(BLACK, 1) + title = TextMobject("Last chapter") + title.scale(2) + title.to_edge(UP) + rect.next_to(title, DOWN) + + self.add(full_rect) + self.play( + FadeIn(rect), + Write(title, run_time=2), + ) + self.wait() + + +class ThreeMainObservations(Scene): + def construct(self): + fourier = ImageMobject("Joseph Fourier") + fourier.set_height(5) + fourier.to_corner(DR) + fourier.shift(LEFT) + bubble = ThoughtBubble( + direction=RIGHT, + height=3, + width=4, + ) + bubble.move_tip_to(fourier.get_corner(UL) + 0.5 * DR) + + observations = VGroup( + TextMobject( + "1)", + # "Sine waves", + # "H", + # "Heat equation", + ), + TextMobject( + "2)", + # "Linearity" + ), + TextMobject( + "3)", + # "Any$^{*}$ function is\\\\", + # "a sum of sine waves", + ), + ) + # heart = SuitSymbol("hearts") + # heart.replace(observations[0][2]) + # observations[0][2].become(heart) + # observations[0][1].add(happiness) + # observations[2][2].align_to( + # observations[2][1], LEFT, + # ) + + observations.arrange( + DOWN, + aligned_edge=LEFT, + buff=LARGE_BUFF, + ) + observations.set_height(FRAME_HEIGHT - 2) + observations.to_corner(UL, buff=LARGE_BUFF) + + self.add(fourier) + self.play(ShowCreation(bubble)) + self.wait() + self.play(LaggedStart(*[ + TransformFromCopy(bubble, observation) + for observation in observations + ], lag_ratio=0.2)) + self.play( + FadeOut(fourier), + FadeOut(bubble), + ) + self.wait() + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part3/temperature_graphs.py b/active_projects/ode/part3/temperature_graphs.py new file mode 100644 index 00000000..35f1b472 --- /dev/null +++ b/active_projects/ode/part3/temperature_graphs.py @@ -0,0 +1,261 @@ +from manimlib.imports import * + + +class TemperatureGraphScene(SpecialThreeDScene): + CONFIG = { + "axes_config": { + "x_min": 0, + "x_max": TAU, + "y_min": 0, + "y_max": 10, + "z_min": -3, + "z_max": 3, + "x_axis_config": { + "tick_frequency": TAU / 8, + "include_tip": False, + }, + "num_axis_pieces": 1, + }, + "default_graph_style": { + "stroke_width": 2, + "stroke_color": WHITE, + "background_image_file": "VerticalTempGradient", + }, + "default_surface_style": { + "fill_opacity": 0.1, + "checkerboard_colors": [LIGHT_GREY], + "stroke_width": 0.5, + "stroke_color": WHITE, + "stroke_opacity": 0.5, + }, + } + + def get_three_d_axes(self, include_labels=True): + axes = ThreeDAxes(**self.axes_config) + axes.set_stroke(width=2) + + # Add number labels + # TODO? + + # Add axis labels + if include_labels: + x_label = TexMobject("x") + x_label.next_to(axes.x_axis.get_right(), DOWN) + axes.x_axis.add(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) + + 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) + + # Adjust axis orinetations + axes.x_axis.rotate( + 90 * DEGREES, RIGHT, + about_point=axes.c2p(0, 0, 0), + ) + axes.y_axis.rotate( + 90 * DEGREES, UP, + about_point=axes.c2p(0, 0, 0), + ) + + # Add xy-plane + surface_config = { + "u_min": 0, + "u_max": axes.x_max, + "v_min": 0, + "v_max": axes.y_max, + "resolution": (16, 10), + } + axes.surface_config = surface_config + input_plane = ParametricSurface( + lambda x, t: axes.c2p(x, t, 0), + # lambda x, t: np.array([x, t, 0]), + **surface_config, + ) + input_plane.set_style( + fill_opacity=0.5, + fill_color=BLUE_B, + stroke_width=0.5, + stroke_color=WHITE, + ) + + axes.input_plane = input_plane + + return axes + + def get_initial_state_graph(self, axes, func, **kwargs): + config = dict() + config.update(self.default_graph_style) + config.update(kwargs) + return ParametricFunction( + lambda x: axes.c2p( + x, 0, func(x) + ), + t_min=axes.x_min, + t_max=axes.x_max, + **config, + ) + + def get_surface(self, axes, func, **kwargs): + config = dict() + config.update(axes.surface_config) + config.update(self.default_surface_style) + config.update(kwargs) + return ParametricSurface( + lambda x, t: axes.c2p( + x, t, func(x, t) + ), + **config + ) + + def orient_three_d_mobject(self, mobject, + phi=85 * DEGREES, + theta=-80 * DEGREES): + mobject.rotate(-90 * DEGREES - theta, OUT) + mobject.rotate(phi, LEFT) + return mobject + + +class SimpleSinExpGraph(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) + + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + ) + self.camera.frame_center.shift(3 * RIGHT) + self.begin_ambient_camera_rotation(rate=0.01) + + self.add(axes) + self.play(ShowCreation(sine_graph)) + self.play(UpdateFromAlphaFunc( + sine_exp_surface, + lambda m, a: m.become( + self.get_sine_exp_surface(axes, v_max=a * 10) + ), + run_time=3 + )) + self.wait(20) + + # + 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 get_sine_graph(self, axes, **config): + return self.get_initial_state_graph( + axes, + lambda x: self.sin_exp(x, 0), + **config + ) + + def get_sine_exp_surface(self, axes, **config): + return self.get_surface( + axes, + lambda x, t: self.sin_exp(x, t), + **config + ) + + +class AddMultipleSolutions(SimpleSinExpGraph): + CONFIG = { + "axes_config": { + "x_axis_config": { + "unit_size": 0.7, + }, + } + } + + def construct(self): + axes1, axes2, axes3 = all_axes = VGroup(*[ + self.get_three_d_axes( + include_labels=False, + ) + for x in range(3) + ]) + all_axes.scale(0.5) + self.orient_three_d_mobject(all_axes) + + As = [1.5, 1.5] + omegas = [1, 2] + ks = [0.25, 0.01] + quads = [ + (axes1, [As[0]], [omegas[0]], [ks[0]]), + (axes2, [As[1]], [omegas[1]], [ks[1]]), + (axes3, As, omegas, ks), + ] + + for axes, As, omegas, ks in quads: + graph = self.get_initial_state_graph( + axes, + lambda x: np.sum([ + self.sin_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) + ]) + ) + surface.sort(lambda p: -p[2]) + + axes.add(surface, graph) + axes.graph = graph + axes.surface = surface + + self.set_camera_orientation(distance=100) + plus = TexMobject("+").scale(2) + equals = TexMobject("=").scale(2) + group = VGroup( + axes1, plus, axes2, equals, axes3, + ) + group.arrange(RIGHT, buff=SMALL_BUFF) + + for axes in all_axes: + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.scale(2) + checkmark.next_to(axes, UP) + checkmark.shift(0.7 * DOWN) + axes.checkmark = checkmark + + self.add(axes1, axes2) + self.play( + LaggedStart( + Write(axes1.surface), + Write(axes2.surface), + ), + LaggedStart( + FadeInFrom(axes1.checkmark, DOWN), + FadeInFrom(axes2.checkmark, DOWN), + ), + lag_ratio=0.2, + run_time=1, + ) + self.wait() + self.play(Write(plus)) + self.play( + Transform( + axes1.copy().set_fill(opacity=0), + axes3 + ), + Transform( + axes2.copy().set_fill(opacity=0), + axes3 + ), + FadeInFrom(equals, LEFT) + ) + self.play( + FadeInFrom(axes3.checkmark, DOWN), + ) + self.wait()