From ddd7ce2f1264e27d59f5fd8d9f45b0327e5b99fc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 May 2019 13:38:45 -0700 Subject: [PATCH 1/9] 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 From 5dcb113996d22bc02b2d286b6d0d16c903c6cf1c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 May 2019 18:28:28 -0700 Subject: [PATCH 2/9] Moving the frame_center of camera was not working. This fixes it, but I suspect there is a need for a deeper fix where everything is handled in transform_points_pre_display properly for the various camera classes --- manimlib/camera/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 77c91586..fbe16a3d 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -332,6 +332,8 @@ class Camera(object): points = self.transform_points_pre_display( vmobject, vmobject.points ) + # TODO, shouldn't this be handled in transform_points_pre_display? + points = points - self.get_frame_center() if len(points) == 0: return From ab2318ff9d005c98fe95299b81b694901e31a940 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 May 2019 18:28:40 -0700 Subject: [PATCH 3/9] New scenes for diffyq part 3 --- active_projects/ode/all_part3_scenes.py | 8 + .../ode/part3/pi_creature_scenes.py | 59 ++ active_projects/ode/part3/staging.py | 82 -- .../ode/part3/temperature_graphs.py | 781 +++++++++++++++++- active_projects/ode/part3/wordy_scenes.py | 153 ++++ 5 files changed, 956 insertions(+), 127 deletions(-) create mode 100644 active_projects/ode/part3/wordy_scenes.py diff --git a/active_projects/ode/all_part3_scenes.py b/active_projects/ode/all_part3_scenes.py index dd6d024b..58be7b50 100644 --- a/active_projects/ode/all_part3_scenes.py +++ b/active_projects/ode/all_part3_scenes.py @@ -1,6 +1,7 @@ from active_projects.ode.part3.staging import * from active_projects.ode.part3.temperature_graphs import * from active_projects.ode.part3.pi_creature_scenes import * +from active_projects.ode.part3.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part3" @@ -12,4 +13,11 @@ SCENES_IN_ORDER = [ ThreeMainObservations, SimpleCosExpGraph, AddMultipleSolutions, + IveHeardOfThis, + FourierSeriesOfLineIllustration, + BreakDownAFunction, + ThreeConstraints, + OceanOfPossibilities, + InFouriersShoes, + AnalyzeSineCurve, ] diff --git a/active_projects/ode/part3/pi_creature_scenes.py b/active_projects/ode/part3/pi_creature_scenes.py index e79bacbb..01a269a2 100644 --- a/active_projects/ode/part3/pi_creature_scenes.py +++ b/active_projects/ode/part3/pi_creature_scenes.py @@ -1,4 +1,5 @@ from manimlib.imports import * +from active_projects.ode.part2.wordy_scenes import * class IveHeardOfThis(TeacherStudentsScene): @@ -39,3 +40,61 @@ class IveHeardOfThis(TeacherStudentsScene): added_anims=[self.teacher.change, "guilty"] ) self.wait(5) + + +class InFouriersShoes(PiCreatureScene, WriteHeatEquationTemplate): + def construct(self): + randy = self.pi_creature + fourier = ImageMobject("Joseph Fourier") + fourier.set_height(4) + fourier.next_to(randy, RIGHT, LARGE_BUFF) + fourier.align_to(randy, DOWN) + + equation = self.get_d1_equation() + equation.next_to(fourier, UP, MED_LARGE_BUFF) + + decades = list(range(1740, 2040, 20)) + time_line = NumberLine( + x_min=decades[0], + x_max=decades[-1], + tick_frequency=1, + tick_size=0.05, + longer_tick_multiple=4, + unit_size=0.2, + numbers_with_elongated_ticks=decades, + numbers_to_show=decades, + decimal_number_config={ + "group_with_commas": False, + }, + stroke_width=2, + ) + time_line.add_numbers() + time_line.move_to(ORIGIN, RIGHT) + time_line.to_edge(UP) + triangle = ArrowTip(start_angle=-90 * DEGREES) + triangle.set_height(0.25) + triangle.move_to(time_line.n2p(2019), DOWN) + triangle.set_color(WHITE) + + self.play(FadeInFrom(fourier, 2 * LEFT)) + self.play(randy.change, "pondering") + self.wait() + self.play( + DrawBorderThenFill(triangle, run_time=1), + FadeInFromDown(equation), + FadeIn(time_line), + ) + self.play( + Animation(triangle), + ApplyMethod( + time_line.shift, + time_line.n2p(2019) - time_line.n2p(1822), + run_time=5 + ), + ) + self.wait() + + +class SineCurveIsUnrealistic(TeacherStudentsScene): + def construct(self): + pass diff --git a/active_projects/ode/part3/staging.py b/active_projects/ode/part3/staging.py index 4bd3f8fb..279d08b7 100644 --- a/active_projects/ode/part3/staging.py +++ b/active_projects/ode/part3/staging.py @@ -423,88 +423,6 @@ class CircleAnimationOfF(FourierOfTrebleClef): 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 index 4d0df958..daa7f574 100644 --- a/active_projects/ode/part3/temperature_graphs.py +++ b/active_projects/ode/part3/temperature_graphs.py @@ -23,7 +23,12 @@ class TemperatureGraphScene(SpecialThreeDScene): "stroke_color": WHITE, "background_image_file": "VerticalTempGradient", }, - "default_surface_style": { + "default_surface_config": { + "u_min": 0, + "u_max": TAU, + "v_min": 0, + "v_max": 10, + "resolution": (16, 10), "fill_opacity": 0.1, "checkerboard_colors": [LIGHT_GREY], "stroke_width": 0.5, @@ -32,32 +37,17 @@ class TemperatureGraphScene(SpecialThreeDScene): }, } - def get_three_d_axes(self, include_labels=True, **kwargs): + def get_three_d_axes(self, include_labels=True, include_numbers=False, **kwargs): config = dict(self.axes_config) config.update(kwargs) axes = ThreeDAxes(**config) axes.set_stroke(width=2) - # Add number labels - # TODO? + if include_numbers: + self.add_axes_numbers(axes) - # Add axis labels if include_labels: - x_label = TexMobject("x") - x_label.next_to(axes.x_axis.get_right(), DOWN) - 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.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.label = temp_label - for axis in axes: - axis.add(axis.label) + self.add_axes_labels(axes) # Adjust axis orinetations axes.x_axis.rotate( @@ -70,18 +60,8 @@ class TemperatureGraphScene(SpecialThreeDScene): ) # 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 = self.get_surface( + axes, lambda x, t: 0 ) input_plane.set_style( fill_opacity=0.5, @@ -94,13 +74,53 @@ class TemperatureGraphScene(SpecialThreeDScene): return axes + def add_axes_numbers(self, axes): + x_axis = axes.x_axis + y_axis = axes.y_axis + tex_vals = [ + ("\\pi \\over 2", PI / 2), + ("\\pi", PI), + ("3 \\pi \\over 2", 3 * PI / 2), + ("\\tau", TAU) + ] + x_labels = VGroup() + for tex, val in tex_vals: + label = TexMobject(tex) + label.scale(0.5) + label.next_to(x_axis.n2p(val), DOWN) + x_labels.add(label) + x_axis.add(x_labels) + + y_axis.add_numbers() + for number in y_axis.numbers: + number.rotate(90 * DEGREES) + return axes + + def add_axes_labels(self, axes): + x_label = TexMobject("x") + x_label.next_to(axes.x_axis.get_end(), RIGHT) + 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.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.label = temp_label + for axis in axes: + axis.add(axis.label) + return axes + 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, t, func(x) + x, t, func(x, t) ), t_min=axes.x_min, t_max=axes.x_max, @@ -113,10 +133,10 @@ class TemperatureGraphScene(SpecialThreeDScene): ) def get_surface(self, axes, func, **kwargs): - config = dict() - config.update(axes.surface_config) - config.update(self.default_surface_style) - config.update(kwargs) + config = merge_dicts_recursively( + self.default_surface_config, + kwargs + ) return ParametricSurface( lambda x, t: axes.c2p( x, t, func(x, t) @@ -375,6 +395,7 @@ class BreakDownAFunction(SimpleCosExpGraph): discontinuities=self.get_initial_func_discontinuities(), color=YELLOW, ) + top_graph.set_stroke(width=4) fourier_terms = self.get_fourier_cosine_terms( self.initial_func @@ -436,7 +457,7 @@ class BreakDownAFunction(SimpleCosExpGraph): plusses = VGroup(*[ TexMobject("+").next_to( axes.x_axis.get_end(), - RIGHT, MED_LARGE_BUFF + RIGHT, MED_SMALL_BUFF ) for axes in low_axes_group ]) @@ -469,16 +490,15 @@ class BreakDownAFunction(SimpleCosExpGraph): self.wait() self.play( LaggedStartMap(FadeIn, low_axes_group), + FadeInFrom(low_words, UP), *[ 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) + LaggedStartMap(FadeInFromDown, [*plusses, dots]), ) self.play(ShowCreation(arrow)) self.wait() @@ -506,7 +526,7 @@ class BreakDownAFunction(SimpleCosExpGraph): anims2.append(AnimationGroup( TransformFromCopy(graph, top_graph.copy()), Transform( - surface.copy().fade(1), + surface.copy().set_fill(opacity=0), top_surface, ) )) @@ -530,9 +550,10 @@ class BreakDownAFunction(SimpleCosExpGraph): self.play(LaggedStartMap(FadeInFromDown, low_checkmarks)) self.wait() - self.play(TransformFromCopy( - low_checkmarks, VGroup(top_checkmark) - )) + self.play(*[ + TransformFromCopy(low_checkmark, top_checkmark.copy()) + for low_checkmark in low_checkmarks + ]) self.wait() # @@ -578,3 +599,673 @@ class BreakDownAFunction(SimpleCosExpGraph): ] result[0] = result[0] / 2 return result + + +class OceanOfPossibilities(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_min": 0, + "z_max": 4, + }, + "k": 0.2, + "default_surface_config": { + # "resolution": (32, 20), + # "resolution": (8, 5), + } + } + + def construct(self): + self.setup_camera() + self.setup_axes() + self.setup_surface() + self.show_solution() + self.reference_boundary_conditions() + self.reference_initial_condition() + self.ambiently_change_solution() + + def setup_camera(self): + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + ) + self.camera.frame_center.move_to( + 3 * RIGHT + ) + self.begin_ambient_camera_rotation(rate=0.01) + + def setup_axes(self): + axes = self.get_three_d_axes(include_numbers=True) + axes.add(axes.input_plane) + # axes.scale(1.25) + axes.shift(1.5 * IN) + + self.add(axes) + self.axes = axes + + def setup_surface(self): + axes = self.axes + k = self.k + + # Parameters for surface function + initial_As = [2] + [ + random.choice([-1, 1]) / n + for n in range(1, 20) + ] + A_trackers = Group(*[ + ValueTracker(A) + for A in initial_As + ]) + + def get_As(): + return [At.get_value() for At in A_trackers] + + omegas = [n / 2 for n in range(0, 10)] + + def func(x, t): + return np.sum([ + np.prod([ + A * np.cos(omega * x), + np.exp(-k * omega**2 * t) + ]) + for A, omega in zip(get_As(), omegas) + ]) + + # Surface and graph + surface = always_redraw( + lambda: self.get_surface(axes, func) + ) + t_tracker = ValueTracker(0) + graph = always_redraw( + lambda: self.get_time_slice_graph( + axes, func, t_tracker.get_value(), + ) + ) + + surface.suspend_updating() + graph.suspend_updating() + + self.surface_func = func + self.surface = surface + self.graph = graph + self.t_tracker = t_tracker + self.A_trackers = A_trackers + self.omegas = omegas + + def show_solution(self): + axes = self.axes + surface = self.surface + graph = self.graph + t_tracker = self.t_tracker + get_t = t_tracker.get_value + + opacity_tracker = ValueTracker(0) + plane = always_redraw(lambda: Polygon( + *[ + axes.c2p(x, get_t(), T) + for x, T in [ + (0, 0), (TAU, 0), (TAU, 4), (0, 4) + ] + ], + stroke_width=0, + fill_color=WHITE, + fill_opacity=opacity_tracker.get_value(), + )) + + self.add(surface, plane, graph) + graph.resume_updating() + self.play( + opacity_tracker.set_value, 0.2, + ApplyMethod( + t_tracker.set_value, 1, + rate_func=linear + ), + run_time=1 + ) + self.play( + ApplyMethod( + t_tracker.set_value, 10, + rate_func=linear, + run_time=9 + ) + ) + self.wait() + + self.plane = plane + + def reference_boundary_conditions(self): + axes = self.axes + t_numbers = axes.y_axis.numbers + + lines = VGroup(*[ + Line( + axes.c2p(x, 0, 0), + axes.c2p(x, axes.y_max, 0), + stroke_width=3, + stroke_color=MAROON_B, + ) + for x in [0, axes.x_max] + ]) + surface_boundary_lines = always_redraw(lambda: VGroup(*[ + ParametricFunction( + lambda t: axes.c2p( + x, t, + self.surface_func(x, t) + ), + t_max=axes.y_max + ).match_style(self.graph) + for x in [0, axes.x_max] + ])) + # surface_boundary_lines.suspend_updating() + words = VGroup() + for line in lines: + word = TextMobject("Boundary") + word.set_stroke(BLACK, 3, background=True) + word.scale(1.5) + word.match_color(line) + word.rotate(90 * DEGREES, RIGHT) + word.rotate(90 * DEGREES, OUT) + word.next_to(line, OUT, SMALL_BUFF) + words.add(word) + + self.stop_ambient_camera_rotation() + self.move_camera( + theta=-45 * DEGREES, + frame_center=ORIGIN, + added_anims=[ + LaggedStartMap(ShowCreation, lines), + LaggedStartMap( + FadeInFrom, words, + lambda m: (m, IN) + ), + FadeOut(t_numbers), + ] + ) + self.play( + LaggedStart(*[ + TransformFromCopy(l1, l2) + for l1, l2 in zip(lines, surface_boundary_lines) + ]) + ) + self.add(surface_boundary_lines) + self.wait() + self.move_camera( + theta=-70 * DEGREES, + frame_center=3 * RIGHT, + ) + + self.surface_boundary_lines = surface_boundary_lines + + def reference_initial_condition(self): + plane = self.plane + t_tracker = self.t_tracker + + self.play( + t_tracker.set_value, 0, + run_time=2 + ) + plane.clear_updaters() + self.play(FadeOut(plane)) + + def ambiently_change_solution(self): + A_trackers = self.A_trackers + + def generate_A_updater(A, rate): + def update(m, dt): + m.total_time += dt + m.set_value( + 2 * A * np.sin(rate * m.total_time + PI / 6) + ) + return update + + rates = [0, 0.2] + [ + 0.5 + 0.5 * np.random.random() + for x in range(len(A_trackers) - 2) + ] + + for tracker, rate in zip(A_trackers, rates): + tracker.total_time = 0 + tracker.add_updater(generate_A_updater( + tracker.get_value(), + rate + )) + + self.add(*A_trackers) + self.surface_boundary_lines.resume_updating() + self.surface.resume_updating() + self.graph.resume_updating() + self.wait(30) + + +class AnalyzeSineCurve(TemperatureGraphScene): + CONFIG = { + "origin_point": 3 * LEFT, + "axes_config": { + "z_min": -1.5, + "z_max": 1.5, + "z_axis_config": { + "unit_size": 2, + "tick_frequency": 0.5, + } + }, + "tex_to_color_map": { + "{x}": GREEN, + "T": YELLOW, + "=": WHITE, + "0": WHITE, + "\\Delta t": WHITE, + "\\sin": WHITE, + "{t}": PINK, + } + } + + def construct(self): + self.setup_axes() + self.ask_about_sine_curve() + self.show_sine_wave_on_axes() + self.reference_curvature() + self.show_derivatives() + self.show_curvature_matching_height() + self.show_time_step_scalings() + self.smooth_evolution() + + def setup_axes(self): + axes = self.get_three_d_axes() + axes.rotate(90 * DEGREES, LEFT) + axes.shift(self.origin_point - axes.c2p(0, 0, 0)) + y_axis = axes.y_axis + y_axis.fade(1) + z_axis = axes.z_axis + z_axis.label.next_to(z_axis.get_end(), UP, SMALL_BUFF) + + self.add_axes_numbers(axes) + y_axis.remove(y_axis.numbers) + axes.z_axis.add_numbers( + *range(-1, 2), + direction=LEFT, + ) + + self.axes = axes + + def ask_about_sine_curve(self): + curve = FunctionGraph( + lambda t: np.sin(t), + x_min=0, + x_max=TAU, + ) + curve.move_to(DR) + curve.set_width(5) + curve.set_color(YELLOW) + question = TextMobject("What's so special?") + question.scale(1.5) + question.to_edge(UP) + question.shift(2 * LEFT) + arrow = Arrow( + question.get_bottom(), + curve.point_from_proportion(0.25) + ) + + self.play( + ShowCreation(curve), + Write(question, run_time=1), + GrowArrow(arrow), + ) + self.wait() + + self.quick_sine_curve = curve + self.question_group = VGroup(question, arrow) + + def show_sine_wave_on_axes(self): + axes = self.axes + graph = self.get_initial_state_graph( + axes, lambda x, t: np.sin(x) + ) + graph.set_stroke(width=4) + graph_label = TexMobject( + "T({x}, 0) = \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + graph_label.next_to( + graph.point_from_proportion(0.25), UR, + buff=SMALL_BUFF, + ) + + v_line, x_tracker = self.get_v_line_with_x_tracker(graph) + + xs = VGroup( + *graph_label.get_parts_by_tex("x"), + axes.x_axis.label, + ) + + self.play( + Write(axes), + self.quick_sine_curve.become, graph, + FadeOutAndShift(self.question_group, UP), + ) + self.play( + FadeInFromDown(graph_label), + FadeIn(graph), + ) + self.remove(self.quick_sine_curve) + self.add(v_line) + self.play( + ApplyMethod( + x_tracker.set_value, TAU, + rate_func=lambda t: smooth(t, 3), + run_time=5, + ), + LaggedStartMap( + ShowCreationThenFadeAround, xs, + run_time=3, + lag_ratio=0.2, + ) + ) + self.remove(v_line, x_tracker) + self.wait() + + self.graph = graph + self.graph_label = graph_label + self.v_line = v_line + self.x_tracker = x_tracker + + def reference_curvature(self): + curve_segment, curve_x_tracker = \ + self.get_curve_segment_with_x_tracker(self.graph) + + self.add(curve_segment) + self.play( + curve_x_tracker.set_value, TAU, + run_time=5, + rate_func=lambda t: smooth(t, 3), + ) + self.play(FadeOut(curve_segment)) + + self.curve_segment = curve_segment + self.curve_x_tracker = curve_x_tracker + + def show_derivatives(self): + deriv1 = TexMobject( + "{\\partial T \\over \\partial {x}}({x}, 0)", + "= \\cos\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + deriv2 = TexMobject( + "{\\partial^2 T \\over \\partial {x}^2}({x}, 0)", + "= -\\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + + deriv1.to_corner(UR) + deriv2.next_to( + deriv1, DOWN, + buff=0.75, + aligned_edge=LEFT, + ) + VGroup(deriv1, deriv2).shift(1.4 * RIGHT) + + self.play( + Animation(Group(*self.get_mobjects())), + FadeInFrom(deriv1, LEFT), + self.camera.frame_center.shift, 2 * RIGHT, + ) + self.wait() + self.play( + FadeInFrom(deriv2, UP) + ) + self.wait() + + self.deriv1 = deriv1 + self.deriv2 = deriv2 + + def show_curvature_matching_height(self): + axes = self.axes + graph = self.graph + curve_segment = self.curve_segment + curve_x_tracker = self.curve_x_tracker + + d2_graph = self.get_initial_state_graph( + axes, lambda x, t: -np.sin(x), + ) + dashed_d2_graph = DashedVMobject(d2_graph, num_dashes=50) + dashed_d2_graph.color_using_background_image(None) + dashed_d2_graph.set_stroke(RED, 2) + + vector, x_tracker = self.get_v_line_with_x_tracker( + d2_graph, + line_creator=lambda p1, p2: Arrow( + p1, p2, color=RED, buff=0 + ) + ) + + lil_vectors = self.get_many_lil_vectors(graph) + lil_vector = always_redraw( + lambda: self.get_lil_vector( + graph, x_tracker.get_value() + ) + ) + + d2_rect = SurroundingRectangle( + self.deriv2[-5:], + color=RED, + ) + self.play(ShowCreation(d2_rect)) + self.add(vector) + self.add(lil_vector) + self.add(curve_segment) + curve_x_tracker.set_value(0) + self.play( + ShowCreation(dashed_d2_graph), + x_tracker.set_value, TAU, + curve_x_tracker.set_value, TAU, + ShowIncreasingSubsets(lil_vectors[1:]), + run_time=8, + rate_func=linear, + ) + self.remove(vector) + self.remove(lil_vector) + self.add(lil_vectors) + self.play( + FadeOut(curve_segment), + FadeOut(d2_rect), + ) + + self.lil_vectors = lil_vectors + self.dashed_d2_graph = dashed_d2_graph + + def show_time_step_scalings(self): + axes = self.axes + graph_label = self.graph_label + dashed_d2_graph = self.dashed_d2_graph + lil_vectors = self.lil_vectors + graph = self.graph + + factor = 0.9 + + new_label = TexMobject( + "T({x}, \\Delta t) = c \\cdot \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + final_label = TexMobject( + "T({x}, {t}) = (\\text{something}) \\cdot \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + for label in (new_label, final_label): + label.shift( + graph_label.get_part_by_tex("=").get_center() - + label.get_part_by_tex("=").get_center() + ) + final_label.shift(1.5 * LEFT) + + h_lines = VGroup( + DashedLine(axes.c2p(0, 0, 1), axes.c2p(TAU, 0, 1)), + DashedLine(axes.c2p(0, 0, -1), axes.c2p(TAU, 0, -1)), + ) + + lil_vectors.add_updater(lambda m: m.become( + self.get_many_lil_vectors(graph) + )) + + i = 4 + self.play( + ReplacementTransform( + graph_label[:i], new_label[:i], + ), + ReplacementTransform( + graph_label[i + 1:i + 3], + new_label[i + 1:i + 3], + ), + FadeOutAndShift(graph_label[i], UP), + FadeInFrom(new_label[i], DOWN), + ) + self.play( + ReplacementTransform( + graph_label[i + 3:], + new_label[i + 4:] + ), + FadeInFromDown(new_label[i + 3]) + ) + self.play( + FadeOut(dashed_d2_graph), + FadeIn(h_lines), + ) + self.play( + graph.stretch, factor, 1, + h_lines.stretch, factor, 1, + ) + self.wait() + + # Repeat + last_coef = None + last_exp = None + delta_T = new_label.get_part_by_tex("\\Delta t") + c = new_label.get_part_by_tex("c")[0] + prefix = new_label[:4] + prefix.generate_target() + for x in range(5): + coef = Integer(x + 2) + exp = coef.copy().scale(0.7) + coef.next_to( + delta_T, LEFT, SMALL_BUFF, + aligned_edge=DOWN, + ) + exp.move_to(c.get_corner(UR), DL) + anims1 = [FadeInFrom(coef, 0.25 * DOWN)] + anims2 = [FadeInFrom(exp, 0.25 * DOWN)] + if last_coef: + anims1.append( + FadeOutAndShift(last_coef, 0.25 * UP) + ) + anims2.append( + FadeOutAndShift(last_exp, 0.25 * UP) + ) + last_coef = coef + last_exp = exp + prefix.target.next_to(coef, LEFT, SMALL_BUFF) + prefix.target.match_y(prefix) + anims1.append(MoveToTarget(prefix)) + + self.play(*anims1) + self.play( + graph.stretch, factor, 1, + h_lines.stretch, factor, 1, + *anims2, + ) + self.play( + ReplacementTransform( + new_label[:4], + final_label[:4], + ), + ReplacementTransform( + VGroup(last_coef, delta_T), + final_label.get_part_by_tex("{t}"), + ), + ReplacementTransform( + last_exp, + final_label.get_part_by_tex("something"), + ), + FadeOut(new_label.get_part_by_tex("\\cdot"), UP), + ReplacementTransform( + new_label[-4:], + final_label[-4:], + ), + ReplacementTransform( + new_label.get_part_by_tex("="), + final_label.get_part_by_tex("="), + ), + ReplacementTransform( + new_label.get_part_by_tex(")"), + final_label.get_part_by_tex(")"), + ), + ) + final_label.add_background_rectangle(opacity=1) + self.add(final_label) + self.wait() + + group = VGroup(graph, h_lines) + group.add_updater(lambda m, dt: m.stretch( + (1 - 0.1 * dt), 1 + )) + self.add(group) + self.wait(10) + + def smooth_evolution(self): + pass + + # + def get_rod(self, temp_func): + pass + + def get_v_line_with_x_tracker(self, graph, line_creator=DashedLine): + axes = self.axes + x_min = axes.x_axis.p2n(graph.get_start()) + x_max = axes.x_axis.p2n(graph.get_end()) + x_tracker = ValueTracker(x_min) + get_x = x_tracker.get_value + v_line = always_redraw(lambda: line_creator( + axes.c2p(get_x(), 0, 0), + graph.point_from_proportion( + inverse_interpolate( + x_min, x_max, get_x() + ) + ), + )) + return v_line, x_tracker + + def get_curve_segment_with_x_tracker(self, graph, delta_x=0.5): + axes = self.axes + x_min = axes.x_axis.p2n(graph.get_start()) + x_max = axes.x_axis.p2n(graph.get_end()) + x_tracker = ValueTracker(x_min) + get_x = x_tracker.get_value + + def x2a(x): + return inverse_interpolate(x_min, x_max, x) + + curve = VMobject( + stroke_color=WHITE, + stroke_width=5 + ) + curve.add_updater(lambda m: m.pointwise_become_partial( + graph, + max(x2a(get_x() - delta_x), 0), + min(x2a(get_x() + delta_x), 1), + )) + return curve, x_tracker + + def get_lil_vector(self, graph, x): + x_axis = self.axes.x_axis + point = graph.point_from_proportion(x / TAU) + x_axis_point = x_axis.n2p(x_axis.p2n(point)) + return Arrow( + point, + interpolate( + point, x_axis_point, 0.5, + ), + buff=0, + color=RED + ) + + def get_many_lil_vectors(self, graph, n=13): + return VGroup(*[ + self.get_lil_vector(graph, x) + for x in np.linspace(0, TAU, n) + ]) diff --git a/active_projects/ode/part3/wordy_scenes.py b/active_projects/ode/part3/wordy_scenes.py new file mode 100644 index 00000000..125fa214 --- /dev/null +++ b/active_projects/ode/part3/wordy_scenes.py @@ -0,0 +1,153 @@ +from manimlib.imports import * +from active_projects.ode.part2.wordy_scenes import * + + +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 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 ThreeConstraints(WriteHeatEquationTemplate): + def construct(self): + self.cross_out_solving() + self.show_three_conditions() + + def cross_out_solving(self): + equation = self.get_d1_equation() + words = TextMobject("Solve this equation") + words.to_edge(UP) + equation.next_to(words, DOWN) + cross = Cross(words) + + self.add(words, equation) + self.wait() + self.play(ShowCreation(cross)) + self.wait() + + self.equation = equation + self.to_remove = VGroup(words, cross) + + def show_three_conditions(self): + equation = self.equation + to_remove = self.to_remove + + title = TexMobject( + "\\text{Constraints }" + "T({x}, {t})" + "\\text{ must satisfy:}", + **self.tex_mobject_config + ) + title.to_edge(UP) + + items = VGroup( + TextMobject("1)", "The PDE"), + TextMobject("2)", "Boundary condition"), + TextMobject("3)", "Initial condition"), + ) + items.scale(0.7) + items.arrange(RIGHT, buff=LARGE_BUFF) + items.set_width(FRAME_WIDTH - 2) + items.next_to(title, DOWN, LARGE_BUFF) + items[1].set_color(MAROON_B) + items[2].set_color(RED) + + bc_paren = TextMobject("(Explained soon)") + bc_paren.scale(0.7) + bc_paren.next_to(items[1], DOWN) + + self.play( + FadeInFromDown(title), + FadeOutAndShift(to_remove, UP), + equation.scale, 0.6, + equation.next_to, items[0], DOWN, + equation.shift_onto_screen, + LaggedStartMap(FadeIn, [ + items[0], + items[1][0], + items[2][0], + ]) + ) + self.wait() + self.play(Write(items[1][1])) + bc_paren.match_y(equation) + self.play(FadeInFrom(bc_paren, UP)) + self.wait(2) + self.play(Write(items[2][1])) + self.wait(2) From 43b82f2c53dfaeee84a5ac8413fdea354da38649 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 3 Jun 2019 11:33:39 -0700 Subject: [PATCH 4/9] Scenes up to AnalyzeSineCurve for diffyq part 3 --- active_projects/ode/all_part3_scenes.py | 1 + .../ode/part3/pi_creature_scenes.py | 18 +++++++++++++++++- .../ode/part3/temperature_graphs.py | 11 ++++------- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/active_projects/ode/all_part3_scenes.py b/active_projects/ode/all_part3_scenes.py index 58be7b50..b7c9fc01 100644 --- a/active_projects/ode/all_part3_scenes.py +++ b/active_projects/ode/all_part3_scenes.py @@ -20,4 +20,5 @@ SCENES_IN_ORDER = [ OceanOfPossibilities, InFouriersShoes, AnalyzeSineCurve, + SineCurveIsUnrealistic, ] diff --git a/active_projects/ode/part3/pi_creature_scenes.py b/active_projects/ode/part3/pi_creature_scenes.py index 01a269a2..1767debc 100644 --- a/active_projects/ode/part3/pi_creature_scenes.py +++ b/active_projects/ode/part3/pi_creature_scenes.py @@ -97,4 +97,20 @@ class InFouriersShoes(PiCreatureScene, WriteHeatEquationTemplate): class SineCurveIsUnrealistic(TeacherStudentsScene): def construct(self): - pass + self.student_says( + "But that would\\\\never happen!", + student_index=1, + bubble_kwargs={ + "direction": RIGHT, + "height": 3, + "width": 4, + }, + target_mode="angry" + ) + self.change_student_modes( + "guilty", "angry", "hesitant", + added_anims=[ + self.teacher.change, "tease" + ] + ) + self.wait(2) diff --git a/active_projects/ode/part3/temperature_graphs.py b/active_projects/ode/part3/temperature_graphs.py index daa7f574..99d10633 100644 --- a/active_projects/ode/part3/temperature_graphs.py +++ b/active_projects/ode/part3/temperature_graphs.py @@ -628,16 +628,14 @@ class OceanOfPossibilities(TemperatureGraphScene): phi=80 * DEGREES, theta=-80 * DEGREES, ) - self.camera.frame_center.move_to( - 3 * RIGHT - ) self.begin_ambient_camera_rotation(rate=0.01) def setup_axes(self): axes = self.get_three_d_axes(include_numbers=True) axes.add(axes.input_plane) - # axes.scale(1.25) - axes.shift(1.5 * IN) + axes.scale(0.9) + axes.center() + axes.shift(OUT + RIGHT) self.add(axes) self.axes = axes @@ -770,7 +768,6 @@ class OceanOfPossibilities(TemperatureGraphScene): self.stop_ambient_camera_rotation() self.move_camera( theta=-45 * DEGREES, - frame_center=ORIGIN, added_anims=[ LaggedStartMap(ShowCreation, lines), LaggedStartMap( @@ -790,7 +787,6 @@ class OceanOfPossibilities(TemperatureGraphScene): self.wait() self.move_camera( theta=-70 * DEGREES, - frame_center=3 * RIGHT, ) self.surface_boundary_lines = surface_boundary_lines @@ -833,6 +829,7 @@ class OceanOfPossibilities(TemperatureGraphScene): self.surface_boundary_lines.resume_updating() self.surface.resume_updating() self.graph.resume_updating() + self.begin_ambient_camera_rotation(rate=0.01) self.wait(30) From 3b42f1f709b6da09b6f7a0fbba8a04230dc60343 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:25:14 -0700 Subject: [PATCH 5/9] Added int_func to SHowIncreasingSubsets --- manimlib/animation/creation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index b3e5325c..c88a7a4e 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -6,6 +6,8 @@ from manimlib.utils.rate_functions import linear from manimlib.utils.rate_functions import double_smooth from manimlib.utils.rate_functions import smooth +import numpy as np + class ShowPartial(Animation): """ @@ -120,6 +122,7 @@ class Write(DrawBorderThenFill): class ShowIncreasingSubsets(Animation): CONFIG = { "suspend_mobject_updating": False, + "int_func": np.floor, } def __init__(self, group, **kwargs): @@ -128,5 +131,5 @@ class ShowIncreasingSubsets(Animation): def interpolate_mobject(self, alpha): n_submobs = len(self.all_submobs) - index = int(alpha * n_submobs) + index = int(self.int_func(alpha * n_submobs)) self.mobject.submobjects = self.all_submobs[:index] From 6214ea7a010f9acaddbb37a1f93507c1317094c7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:26:03 -0700 Subject: [PATCH 6/9] Added TracedPath --- manimlib/mobject/changing.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 1827f1ef..0a24a06e 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -1,6 +1,8 @@ from manimlib.constants import * +from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.rate_functions import smooth +from manimlib.utils.space_ops import get_norm class AnimatedBoundary(VGroup): @@ -66,3 +68,31 @@ class AnimatedBoundary(VGroup): for sm1, sm2 in zip(family1, family2): sm1.pointwise_become_partial(sm2, a, b) return self + + +class TracedPath(VMobject): + CONFIG = { + "stroke_width": 2, + "stroke_color": WHITE, + "min_distance_to_new_point": 0.1, + } + + def __init__(self, traced_point_func, **kwargs): + super().__init__(**kwargs) + self.traced_point_func = traced_point_func + self.add_updater(lambda m: m.update_path()) + + def update_path(self): + new_point = self.traced_point_func() + if self.has_no_points(): + self.start_new_path(new_point) + self.add_line_to(new_point) + else: + # Set the end to be the new point + self.points[-1] = new_point + + # Second to last point + nppcc = self.n_points_per_cubic_curve + dist = get_norm(new_point - self.points[-nppcc]) + if dist >= self.min_distance_to_new_point: + self.add_line_to(new_point) From 542ddb9afd955cd0cb6e736bbedaf81c11c5f429 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:26:34 -0700 Subject: [PATCH 7/9] Make sure set_opacity changes background stroke as well --- manimlib/mobject/types/vectorized_mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 640a40d1..29b91e54 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -242,6 +242,7 @@ class VMobject(Mobject): def set_opacity(self, opacity, family=True): self.set_fill(opacity=opacity, family=family) self.set_stroke(opacity=opacity, family=family) + self.set_stroke(opacity=opacity, family=family, background=True) return self def fade(self, darkness=0.5, family=True): From 503b2bc8968d6c0603baa87256935dac0f1e58b1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:27:19 -0700 Subject: [PATCH 8/9] For some reason stage_scenes still has a hardcoded input directory... --- stage_scenes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stage_scenes.py b/stage_scenes.py index 8ef370d2..e35a7220 100644 --- a/stage_scenes.py +++ b/stage_scenes.py @@ -4,7 +4,6 @@ import os import sys import importlib -from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG from manimlib.constants import VIDEO_DIR from manimlib.config import get_module from manimlib.extract_scene import is_child_scene @@ -43,7 +42,7 @@ def stage_scenes(module_name): # } # TODO, fix this animation_dir = os.path.join( - VIDEO_DIR, "ode", "part2", "1440p60" + VIDEO_DIR, "ode", "part3", "1440p60" ) # files = os.listdir(animation_dir) From caa4efdfa505d98b9b2fb75c5d86c0e543797c5f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:27:35 -0700 Subject: [PATCH 9/9] Latest scenes for diffyq part 3 --- active_projects/ode/all_part3_scenes.py | 43 +- active_projects/ode/part2/heat_equation.py | 41 +- .../ode/part3/pi_creature_scenes.py | 26 +- active_projects/ode/part3/staging.py | 592 +++++++++++++++++- .../ode/part3/temperature_graphs.py | 429 +++++++++++-- active_projects/ode/part3/wordy_scenes.py | 312 +++++++++ 6 files changed, 1369 insertions(+), 74 deletions(-) diff --git a/active_projects/ode/all_part3_scenes.py b/active_projects/ode/all_part3_scenes.py index b7c9fc01..97951066 100644 --- a/active_projects/ode/all_part3_scenes.py +++ b/active_projects/ode/all_part3_scenes.py @@ -6,19 +6,38 @@ from active_projects.ode.part3.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part3" SCENES_IN_ORDER = [ + LastChapterWrapper, + ThreeConstraints, + OceanOfPossibilities, + # TODO + ThreeMainObservations, + BreakDownAFunction, + SineCurveIsUnrealistic, + AnalyzeSineCurve, + EquationAboveSineAnalysis, + ExponentialDecay, + InvestmentGrowth, + GrowingPileOfMoney, + CarbonDecayCurve, + CarbonDecayingInMammoth, + SineWaveScaledByExp, + ShowSinExpDerivatives, + IfOnly, + BoundaryConditionInterlude, + BoundaryConditionReference, + GiantCross, + SimulateRealSineCurve, + SimulateLinearGraph, + + # SimpleCosExpGraph, + # AddMultipleSolutions, + # IveHeardOfThis, + # FourierSeriesOfLineIllustration, + # InFouriersShoes, +] + +PART_4_SCENES = [ FourierSeriesIllustraiton, FourierNameIntro, CircleAnimationOfF, - LastChapterWrapper, - ThreeMainObservations, - SimpleCosExpGraph, - AddMultipleSolutions, - IveHeardOfThis, - FourierSeriesOfLineIllustration, - BreakDownAFunction, - ThreeConstraints, - OceanOfPossibilities, - InFouriersShoes, - AnalyzeSineCurve, - SineCurveIsUnrealistic, ] diff --git a/active_projects/ode/part2/heat_equation.py b/active_projects/ode/part2/heat_equation.py index f9278d9d..4d8294bd 100644 --- a/active_projects/ode/part2/heat_equation.py +++ b/active_projects/ode/part2/heat_equation.py @@ -349,16 +349,16 @@ class BringTwoRodsTogether(Scene): if (0 < i < len(points) - 1): second_deriv = d2y / (dx**2) else: - second_deriv = 0.5 * d2y / dx - second_deriv = 0 + second_deriv = d2y / dx + # second_deriv = 0 y_change[i] = alpha * second_deriv * dt / n_mini_steps # y_change[0] = y_change[1] # y_change[-1] = y_change[-2] - y_change[0] = 0 - y_change[-1] = 0 - y_change -= np.mean(y_change) + # y_change[0] = 0 + # y_change[-1] = 0 + # y_change -= np.mean(y_change) points[:, 1] += y_change graph.set_points_smoothly(points) return graph @@ -374,8 +374,17 @@ class BringTwoRodsTogether(Scene): )[1] for alt_x in (x - dx, x, x + dx) ] - d2y = ry - 2 * y + ly - return d2y / (dx**2) + + # At the boundary, don't return the second deriv, + # but instead something matching the Neumann + # boundary condition. + if x == x_max: + return (ly - y) / dx + elif x == x_min: + return (ry - y) / dx + else: + d2y = ry - 2 * y + ly + return d2y / (dx**2) def get_rod(self, x_min, x_max, n_pieces=None): if n_pieces is None: @@ -407,7 +416,7 @@ class BringTwoRodsTogether(Scene): self.rod_point_to_color(piece.get_right()), ]) - def rod_point_to_color(self, point): + def rod_point_to_graph_y(self, point): axes = self.axes x = axes.x_axis.p2n(point) @@ -417,11 +426,16 @@ class BringTwoRodsTogether(Scene): self.graph_x_max, x, ) - y = axes.y_axis.p2n( + return axes.y_axis.p2n( graph.point_from_proportion(alpha) ) - return temperature_to_color( - (y - 45) / 45 + + def y_to_color(self, y): + return temperature_to_color((y - 45) / 45) + + def rod_point_to_color(self, point): + return self.y_to_color( + self.rod_point_to_graph_y(point) ) @@ -467,7 +481,10 @@ class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether): self.time_label.next_to(self.clock, DOWN) def add_rod(self): - rod = self.rod = self.get_rod(0, 10) + rod = self.rod = self.get_rod( + self.graph_x_min, + self.graph_x_max, + ) self.add(rod) def add_arrows(self): diff --git a/active_projects/ode/part3/pi_creature_scenes.py b/active_projects/ode/part3/pi_creature_scenes.py index 1767debc..0d2e6a28 100644 --- a/active_projects/ode/part3/pi_creature_scenes.py +++ b/active_projects/ode/part3/pi_creature_scenes.py @@ -113,4 +113,28 @@ class SineCurveIsUnrealistic(TeacherStudentsScene): self.teacher.change, "tease" ] ) - self.wait(2) + self.wait(3) + self.play( + RemovePiCreatureBubble(self.students[1]), + self.teacher.change, "raise_right_hand" + ) + self.change_all_student_modes( + "pondering", + look_at_arg=3 * UP, + ) + self.wait(5) + + + +class IfOnly(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "If only!", + target_mode="angry" + ) + self.change_all_student_modes( + "confused", + look_at_arg=self.screen + ) + self.wait(3) + diff --git a/active_projects/ode/part3/staging.py b/active_projects/ode/part3/staging.py index 279d08b7..9b6a31c8 100644 --- a/active_projects/ode/part3/staging.py +++ b/active_projects/ode/part3/staging.py @@ -200,6 +200,36 @@ class FourierNameIntro(Scene): self.wait(3) +class ManyCousinsOfFourierThings(Scene): + def construct(self): + series_variants = VGroup( + TextMobject("Complex", "Fourier Series"), + TextMobject("Discrete", "Fourier Series"), + ) + transform_variants = VGroup( + TextMobject("Discrete", "Fourier Transform"), + TextMobject("Fast", "Fourier Transform"), + TextMobject("Quantum", "Fourier Transform"), + ) + groups = VGroup(series_variants, transform_variants) + for group, vect in zip(groups, [LEFT, RIGHT]): + group.scale(0.7) + group.arrange(DOWN, aligned_edge=LEFT) + group.move_to( + vect * FRAME_WIDTH / 4 + ) + group.set_color(YELLOW) + + self.play(*[ + LaggedStartMap(FadeIn, group) + for group in groups + ]) + self.play(*[ + LaggedStartMap(FadeOut, group) + for group in groups + ]) + + class FourierSeriesIllustraiton(Scene): CONFIG = { "n_range": range(1, 31, 2), @@ -417,12 +447,568 @@ class CircleAnimationOfF(FourierOfTrebleClef): def get_shape(self): path = VMobject() - shape = TexMobject("F") + shape = TextMobject("F") for sp in shape.family_members_with_points(): path.append_points(sp.points) return path -class NewSceneName(Scene): +class ExponentialDecay(PiCreatureScene): def construct(self): - pass + k = 0.2 + mk_tex = "-0.2" + mk_tex_color = GREEN + t2c = {mk_tex: mk_tex_color} + + # Pi creature + randy = self.pi_creature + randy.flip() + randy.set_height(2.5) + randy.move_to(3 * RIGHT) + randy.to_edge(DOWN) + bubble = ThoughtBubble( + direction=LEFT, + height=3.5, + width=3, + ) + bubble.pin_to(randy) + bubble.set_fill(DARKER_GREY) + exp = TexMobject( + "Ce^{", mk_tex, "t}", + tex_to_color_map=t2c, + ) + exp.move_to(bubble.get_bubble_center()) + + # Setup axes + axes = Axes( + x_min=0, + x_max=13, + y_min=-4, + y_max=4, + ) + axes.set_stroke(width=2) + axes.set_color(LIGHT_GREY) + axes.scale(0.9) + axes.to_edge(LEFT, buff=LARGE_BUFF) + axes.x_axis.add_numbers() + axes.y_axis.add_numbers() + axes.y_axis.add_numbers(0) + axes.x_axis.add( + TextMobject("Time").next_to( + axes.x_axis.get_end(), DR, + ) + ) + axes.y_axis.add( + TexMobject("f").next_to( + axes.y_axis.get_corner(UR), RIGHT, + ).set_color(YELLOW) + ) + axes.x_axis.set_opacity(0) + + # Value trackers + y_tracker = ValueTracker(3) + x_tracker = ValueTracker(0) + dydt_tracker = ValueTracker() + dxdt_tracker = ValueTracker(0) + self.add( + y_tracker, x_tracker, + dydt_tracker, dxdt_tracker, + ) + + get_y = y_tracker.get_value + get_x = x_tracker.get_value + get_dydt = dydt_tracker.get_value + get_dxdt = dxdt_tracker.get_value + + dydt_tracker.add_updater(lambda m: m.set_value( + - k * get_y() + )) + y_tracker.add_updater(lambda m, dt: m.increment_value( + dt * get_dydt() + )) + x_tracker.add_updater(lambda m, dt: m.increment_value( + dt * get_dxdt() + )) + + # Tip/decimal + tip = ArrowTip(color=YELLOW) + tip.set_width(0.25) + tip.add_updater(lambda m: m.move_to( + axes.c2p(get_x(), get_y()), LEFT + )) + decimal = DecimalNumber() + decimal.add_updater(lambda d: d.set_value(get_y())) + decimal.add_updater(lambda d: d.next_to( + tip, RIGHT, + SMALL_BUFF, + )) + + # Rate of change arrow + arrow = Vector( + DOWN, color=RED, + max_stroke_width_to_length_ratio=50, + max_tip_length_to_length_ratio=0.2, + ) + arrow.set_stroke(width=4) + arrow.add_updater(lambda m: m.scale( + 2.5 * abs(get_dydt()) / m.get_length() + )) + arrow.add_updater(lambda m: m.move_to( + tip.get_left(), UP + )) + + # Graph + graph = TracedPath(tip.get_left) + + # Equation + ode = TexMobject( + "{d{f} \\over dt}(t)", + "=", mk_tex, "\\cdot {f}(t)", + tex_to_color_map={ + "{f}": YELLOW, + "=": WHITE, + mk_tex: mk_tex_color + } + ) + ode.to_edge(UP) + dfdt = ode[:3] + ft = ode[-2:] + + self.add(axes) + self.add(tip) + self.add(decimal) + self.add(arrow) + self.add(randy) + self.add(ode) + + # Show rate of change dependent on itself + rect = SurroundingRectangle(dfdt) + self.play(ShowCreation(rect)) + self.wait() + self.play( + Transform( + rect, + SurroundingRectangle(ft) + ) + ) + self.wait(3) + + # Show graph over time + self.play( + DrawBorderThenFill(bubble), + Write(exp), + FadeOut(rect), + randy.change, "thinking", + ) + axes.x_axis.set_opacity(1) + self.play( + y_tracker.set_value, 3, + ShowCreation(axes.x_axis), + ) + dxdt_tracker.set_value(1) + self.add(graph) + randy.add_updater(lambda r: r.look_at(tip)) + self.wait(4) + + # Show derivative of exponential + eq = TexMobject("=") + eq.next_to(ode.get_part_by_tex("="), DOWN, LARGE_BUFF) + exp.generate_target() + exp.target.next_to(eq, LEFT) + d_dt = TexMobject("{d \\over dt}") + d_dt.next_to(exp.target, LEFT) + const = TexMobject(mk_tex) + const.set_color(mk_tex_color) + dot = TexMobject("\\cdot") + const.next_to(eq, RIGHT) + dot.next_to(const, RIGHT, 2 * SMALL_BUFF) + exp_copy = exp.copy() + exp_copy.next_to(dot, RIGHT, 2 * SMALL_BUFF) + VGroup(const, dot, eq).align_to(exp_copy, DOWN) + + self.play( + MoveToTarget(exp), + FadeOut(bubble), + FadeIn(d_dt), + FadeIn(eq), + ) + self.wait(2) + self.play( + ApplyMethod( + exp[1].copy().replace, + const[0], + ) + ) + self.wait() + rect = SurroundingRectangle(exp) + rect.set_stroke(BLUE, 2) + self.play(FadeIn(rect)) + self.play( + Write(dot), + TransformFromCopy(exp, exp_copy), + rect.move_to, exp_copy + ) + self.play(FadeOut(rect)) + self.wait(5) + + +class InvestmentGrowth(Scene): + CONFIG = { + "output_tex": "{M}", + "output_color": GREEN, + "initial_value": 1, + "initial_value_tex": "{M_0}", + "k": 0.05, + "k_tex": "0.05", + "total_time": 43, + "time_rate": 4, + } + + def construct(self): + # Axes + axes = Axes( + x_min=0, + x_max=self.total_time, + y_min=0, + y_max=6, + x_axis_config={ + "unit_size": 0.3, + "tick_size": 0.05, + "numbers_with_elongated_ticks": range( + 0, self.total_time, 5 + ) + } + ) + axes.to_corner(DL, buff=LARGE_BUFF) + + time_label = TextMobject("Time") + time_label.next_to( + axes.x_axis.get_right(), + UP, MED_LARGE_BUFF + ) + time_label.shift_onto_screen() + axes.x_axis.add(time_label) + money_label = TexMobject(self.output_tex) + money_label.set_color(self.output_color) + money_label.next_to( + axes.y_axis.get_top(), + UP, + ) + axes.y_axis.add(money_label) + + # Graph + graph = axes.get_graph( + lambda x: self.initial_value * np.exp(self.k * x) + ) + graph.set_color(self.output_color) + full_graph = graph.copy() + time_tracker = self.get_time_tracker() + graph.add_updater(lambda m: m.pointwise_become_partial( + full_graph, 0, + np.clip( + time_tracker.get_value() / self.total_time, + 0, 1, + ) + )) + + # Equation + tex_kwargs = { + "tex_to_color_map": { + self.output_tex: self.output_color, + self.initial_value_tex: BLUE, + } + } + ode = TexMobject( + "{d", + "\\over dt}", + self.output_tex, + "(t)", + "=", self.k_tex, + "\\cdot", self.output_tex, "(t)", + **tex_kwargs + ) + ode.to_edge(UP) + exp = TexMobject( + self.output_tex, + "(t) =", self.initial_value_tex, + "e^{", self.k_tex, "t}", + **tex_kwargs + ) + exp.next_to(ode, DOWN, LARGE_BUFF) + + M0_part = exp.get_part_by_tex(self.initial_value_tex) + exp_part = exp[-3:] + M0_label = M0_part.copy() + M0_label.next_to( + axes.c2p(0, self.initial_value), + LEFT + ) + M0_part.set_opacity(0) + exp_part.save_state() + exp_part.align_to(M0_part, LEFT) + + self.add(axes) + self.add(graph) + self.add(time_tracker) + + self.play(FadeInFromDown(ode)) + self.wait(2) + self.play(FadeInFrom(exp, UP)) + self.wait(2) + self.play( + Restore(exp_part), + M0_part.set_opacity, 1, + ) + self.play(TransformFromCopy( + M0_part, M0_label + )) + self.wait(5) + + def get_time_tracker(self): + time_tracker = ValueTracker(0) + time_tracker.add_updater( + lambda m, dt: m.increment_value( + self.time_rate * dt + ) + ) + return time_tracker + + +class GrowingPileOfMoney(InvestmentGrowth): + CONFIG = { + "total_time": 60 + } + + def construct(self): + initial_count = 5 + k = self.k + total_time = self.total_time + + time_tracker = self.get_time_tracker() + + final_count = initial_count * np.exp(k * total_time) + dollar_signs = VGroup(*[ + TexMobject("\\$") + for x in range(int(final_count)) + ]) + dollar_signs.set_color(GREEN) + for ds in dollar_signs: + ds.shift( + 3 * np.random.random(3) + ) + dollar_signs.center() + dollar_signs.sort(get_norm) + dollar_signs.set_stroke(BLACK, 3, background=True) + + def update_dollar_signs(group): + t = time_tracker.get_value() + count = initial_count * np.exp(k * t) + alpha = count / final_count + n, sa = integer_interpolate(0, len(dollar_signs), alpha) + group.set_opacity(1) + group[n:].set_opacity(0) + group[n].set_opacity(sa) + + dollar_signs.add_updater(update_dollar_signs) + + self.add(time_tracker) + self.add(dollar_signs) + self.wait(20) + + +class CarbonDecayCurve(InvestmentGrowth): + CONFIG = { + "output_tex": "{^{14}C}", + "output_color": GOLD, + "initial_value": 4, + "initial_value_tex": "{^{14}C_0}", + "k": -0.1, + "k_tex": "-k", + "time_rate": 6, + } + + +class CarbonDecayingInMammoth(Scene): + def construct(self): + mammoth = SVGMobject("Mammoth") + mammoth.set_color( + interpolate_color(GREY_BROWN, WHITE, 0.25) + ) + mammoth.set_height(4) + body = mammoth[9] + + atoms = VGroup(*[ + self.get_atom(body) + for n in range(600) + ]) + + p_decay = 0.2 + + def update_atoms(group, dt): + for atom in group: + if np.random.random() < dt * p_decay: + group.remove(atom) + return group + atoms.add_updater(update_atoms) + + self.add(mammoth) + self.add(atoms) + self.wait(20) + + def get_atom(self, body): + atom = Dot(color=GOLD) + atom.set_height(0.05) + + dl = body.get_corner(DL) + ur = body.get_corner(UR) + + wn = 0 + while wn == 0: + point = np.array([ + interpolate(dl[0], ur[0], np.random.random()), + interpolate(dl[1], ur[1], np.random.random()), + 0 + ]) + wn = get_winding_number([ + body.point_from_proportion(a) - point + for a in np.linspace(0, 1, 300) + ]) + wn = int(np.round(wn)) + + atom.move_to(point) + return atom + + +class BoundaryConditionInterlude(Scene): + def construct(self): + background = FullScreenFadeRectangle( + fill_color=DARK_GREY + ) + storyline = self.get_main_storyline() + storyline.generate_target() + v_shift = 2 * DOWN + storyline.target.shift(v_shift) + im_to_im = storyline[1].get_center() - storyline[0].get_center() + + bc_image = self.get_labeled_image( + "Boundary\\\\conditions", + "boundary_condition_thumbnail" + ) + bc_image.next_to(storyline[1], UP) + new_arrow0 = Arrow( + storyline.arrows[0].get_start() + v_shift, + bc_image.get_left() + SMALL_BUFF * LEFT, + path_arc=-90 * DEGREES, + buff=0, + ) + new_mid_arrow = Arrow( + bc_image.get_bottom(), + storyline[1].get_top() + v_shift, + buff=SMALL_BUFF, + path_arc=60 * DEGREES, + ) + + self.add(background) + self.add(storyline[0]) + for im1, im2, arrow in zip(storyline, storyline[1:], storyline.arrows): + self.add(im2, im1) + self.play( + FadeInFrom(im2, -im_to_im), + ShowCreation(arrow), + ) + self.wait() + self.play( + GrowFromCenter(bc_image), + MoveToTarget(storyline), + Transform( + storyline.arrows[0], + new_arrow0, + ), + MaintainPositionRelativeTo( + storyline.arrows[1], + storyline, + ), + ) + self.play(ShowCreation(new_mid_arrow)) + self.wait() + + def get_main_storyline(self): + images = Group( + self.get_sine_curve_image(), + self.get_linearity_image(), + self.get_fourier_series_image(), + ) + for image in images: + image.set_height(3) + images.arrange(RIGHT, buff=1) + images.set_width(FRAME_WIDTH - 1) + + arrows = VGroup() + for im1, im2 in zip(images, images[1:]): + arrow = Arrow( + im1.get_top(), + im2.get_top(), + color=WHITE, + buff=MED_SMALL_BUFF, + path_arc=-120 * DEGREES, + rectangular_stem_width=0.025, + ) + arrow.scale(0.7, about_edge=DOWN) + arrows.add(arrow) + images.arrows = arrows + + return images + + def get_sine_curve_image(self): + return self.get_labeled_image( + "Sine curves", + "sine_curve_temp_graph", + ) + + def get_linearity_image(self): + return self.get_labeled_image( + "Linearity", + "linearity_thumbnail", + ) + + def get_fourier_series_image(self): + return self.get_labeled_image( + "Fourier series", + "fourier_series_thumbnail", + ) + + def get_labeled_image(self, text, image_file): + rect = ScreenRectangle(height=2) + border = rect.copy() + rect.set_fill(BLACK, 1) + rect.set_stroke(WHITE, 0) + border.set_stroke(WHITE, 2) + + text_mob = TextMobject(text) + text_mob.set_stroke(BLACK, 5, background=True) + text_mob.next_to(rect.get_top(), DOWN, SMALL_BUFF) + + image = ImageMobject(image_file) + image.replace(rect, dim_to_match=1) + image.scale(0.8, about_edge=DOWN) + + return Group(rect, image, border, text_mob) + + +class GiantCross(Scene): + def construct(self): + rect = FullScreenFadeRectangle() + cross = Cross(rect) + cross.set_stroke(RED, 25) + + words = TextMobject("This wouldn't\\\\happen!") + words.scale(2) + words.set_color(RED) + words.to_edge(UP) + + self.play( + FadeInFromDown(words), + ShowCreation(cross), + ) + self.wait() + diff --git a/active_projects/ode/part3/temperature_graphs.py b/active_projects/ode/part3/temperature_graphs.py index 99d10633..0ccb811c 100644 --- a/active_projects/ode/part3/temperature_graphs.py +++ b/active_projects/ode/part3/temperature_graphs.py @@ -1,6 +1,7 @@ from scipy import integrate from manimlib.imports import * +from active_projects.ode.part2.heat_equation import * class TemperatureGraphScene(SpecialThreeDScene): @@ -24,11 +25,6 @@ class TemperatureGraphScene(SpecialThreeDScene): "background_image_file": "VerticalTempGradient", }, "default_surface_config": { - "u_min": 0, - "u_max": TAU, - "v_min": 0, - "v_max": 10, - "resolution": (16, 10), "fill_opacity": 0.1, "checkerboard_colors": [LIGHT_GREY], "stroke_width": 0.5, @@ -90,6 +86,7 @@ class TemperatureGraphScene(SpecialThreeDScene): label.next_to(x_axis.n2p(val), DOWN) x_labels.add(label) x_axis.add(x_labels) + x_axis.numbers = x_labels y_axis.add_numbers() for number in y_axis.numbers: @@ -117,26 +114,39 @@ class TemperatureGraphScene(SpecialThreeDScene): def get_time_slice_graph(self, axes, func, t, **kwargs): config = dict() config.update(self.default_graph_style) + config.update({ + "t_min": axes.x_min, + "t_max": axes.x_max, + }) config.update(kwargs) return ParametricFunction( lambda x: axes.c2p( x, t, func(x, t) ), - 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 + axes, + lambda x, t: func(x), + t=0, + **kwargs ) def get_surface(self, axes, func, **kwargs): - config = merge_dicts_recursively( - self.default_surface_config, - kwargs - ) + config = { + "u_min": axes.x_min, + "u_max": axes.x_max, + "v_min": axes.y_min, + "v_max": axes.y_max, + "resolution": ( + (axes.x_max - axes.x_min) // axes.x_axis.tick_frequency, + (axes.y_max - axes.y_min) // axes.y_axis.tick_frequency, + ), + } + config.update(self.default_surface_config) + config.update(kwargs) return ParametricSurface( lambda x, t: axes.c2p( x, t, func(x, t) @@ -151,6 +161,9 @@ class TemperatureGraphScene(SpecialThreeDScene): mobject.rotate(phi, LEFT) return mobject + def get_rod_length(self): + return self.axes_config["x_max"] + class SimpleCosExpGraph(TemperatureGraphScene): def construct(self): @@ -336,9 +349,11 @@ class BreakDownAFunction(SimpleCosExpGraph): "unit_size": 0.75, "include_tip": False, }, - "z_min": 0, + "z_min": -2, + "y_max": 20, }, "n_low_axes": 4, + "k": 0.2, } def construct(self): @@ -407,12 +422,12 @@ class BreakDownAFunction(SimpleCosExpGraph): lambda x: A * np.cos(n * x / 2) ) for n, axes, A in zip( - it.count(0, 2), + it.count(), low_axes_group, - fourier_terms[::2], + fourier_terms ) ]) - k = 0.1 + k = self.k low_surfaces = VGroup(*[ self.get_surface( axes, @@ -423,9 +438,9 @@ class BreakDownAFunction(SimpleCosExpGraph): ]) ) for n, axes, A in zip( - it.count(0, 2), + it.count(), low_axes_group, - fourier_terms[::2], + fourier_terms ) ]) top_surface = self.get_surface( @@ -437,8 +452,8 @@ class BreakDownAFunction(SimpleCosExpGraph): np.exp(-k * (n / 2)**2 * t) ]) for n, A in zip( - it.count(0, 2), - fourier_terms[::2] + it.count(), + fourier_terms ) ]) ) @@ -463,10 +478,11 @@ class BreakDownAFunction(SimpleCosExpGraph): ]) 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_graph.get_end() + 1.4 * DOWN + 1.7 * RIGHT, + path_arc=90 * DEGREES, ) top_words = TextMobject("Arbitrary\\\\function") @@ -474,7 +490,7 @@ class BreakDownAFunction(SimpleCosExpGraph): top_words.set_color(YELLOW) top_arrow = Arrow( top_words.get_right(), - top_graph.get_center() + LEFT, + top_graph.point_from_proportion(0.3) ) low_words = TextMobject("Sine curves") @@ -491,14 +507,11 @@ class BreakDownAFunction(SimpleCosExpGraph): self.play( LaggedStartMap(FadeIn, low_axes_group), FadeInFrom(low_words, UP), + LaggedStartMap(FadeInFromDown, [*plusses, dots]), *[ TransformFromCopy(top_graph, low_graph) for low_graph in low_graphs - ] - ) - self.wait() - self.play( - LaggedStartMap(FadeInFromDown, [*plusses, dots]), + ], ) self.play(ShowCreation(arrow)) self.wait() @@ -515,7 +528,11 @@ class BreakDownAFunction(SimpleCosExpGraph): surface.sort(lambda p: -p[2]) anims1 = [] - anims2 = [] + anims2 = [ + ApplyMethod( + top_axes.y_axis.set_opacity, 1, + ), + ] 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) @@ -558,38 +575,35 @@ class BreakDownAFunction(SimpleCosExpGraph): # def initial_func(self, x): - return 3 * np.exp(-(x - PI)**2) + # 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 + x1 = TAU / 4 - 1 + x2 = TAU / 4 + 1 + x3 = 3 * TAU / 4 - 1.6 + x4 = 3 * TAU / 4 + 0.3 T0 = -2 T1 = 2 + T2 = 1 if x < x1: return T0 elif x < x2: - return interpolate( - T0, T1, - inverse_interpolate(x1, x2, x) - ) + alpha = inverse_interpolate(x1, x2, x) + return bezier([T0, T0, T1, T1])(alpha) elif x < x3: return T1 elif x < x4: - return interpolate( - T1, T0, - inverse_interpolate(x3, x4, x) - ) + alpha = inverse_interpolate(x3, x4, x) + return bezier([T1, T1, T2, T2])(alpha) else: - return T0 + return T2 def get_initial_func_discontinuities(self): # return [TAU / 4, 3 * TAU / 4] return [] - def get_fourier_cosine_terms(self, func, n_terms=20): + def get_fourier_cosine_terms(self, func, n_terms=40): result = [ integrate.quad( lambda x: (1 / PI) * func(x) * np.cos(n * x / 2), @@ -914,7 +928,7 @@ class AnalyzeSineCurve(TemperatureGraphScene): def show_sine_wave_on_axes(self): axes = self.axes graph = self.get_initial_state_graph( - axes, lambda x, t: np.sin(x) + axes, lambda x: np.sin(x) ) graph.set_stroke(width=4) graph_label = TexMobject( @@ -1020,7 +1034,7 @@ class AnalyzeSineCurve(TemperatureGraphScene): curve_x_tracker = self.curve_x_tracker d2_graph = self.get_initial_state_graph( - axes, lambda x, t: -np.sin(x), + axes, lambda x: -np.sin(x), ) dashed_d2_graph = DashedVMobject(d2_graph, num_dashes=50) dashed_d2_graph.color_using_background_image(None) @@ -1266,3 +1280,326 @@ class AnalyzeSineCurve(TemperatureGraphScene): self.get_lil_vector(graph, x) for x in np.linspace(0, TAU, n) ]) + + +class SineWaveScaledByExp(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_min": -1.5, + "z_max": 1.5, + "z_axis_config": { + "unit_size": 2, + "tick_frequency": 0.5, + "label_direction": LEFT, + }, + "y_axis_config": { + "label_direction": RIGHT, + }, + }, + "k": 0.3, + } + + def construct(self): + self.setup_axes() + self.setup_camera() + self.show_sine_wave() + self.show_decay_surface() + self.linger_at_end() + + def setup_axes(self): + axes = self.get_three_d_axes() + + # Add number labels + self.add_axes_numbers(axes) + for axis in [axes.x_axis, axes.y_axis]: + axis.numbers.rotate( + 90 * DEGREES, + axis=axis.get_vector(), + about_point=axis.point_from_proportion(0.5) + ) + axis.numbers.set_shade_in_3d(True) + axes.z_axis.add_numbers(*range(-1, 2)) + for number in axes.z_axis.numbers: + number.rotate(90 * DEGREES, RIGHT) + + axes.z_axis.label.next_to( + axes.z_axis.get_end(), OUT, + ) + + # Input plane + axes.input_plane.set_opacity(0.25) + self.add(axes.input_plane) + + # Shift into place + # axes.shift(5 * LEFT) + self.axes = axes + self.add(axes) + + def setup_camera(self): + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + distance=50, + ) + self.camera.set_frame_center( + 2 * RIGHT, + ) + + def show_sine_wave(self): + time_tracker = ValueTracker(0) + graph = always_redraw( + lambda: self.get_time_slice_graph( + self.axes, + self.sin_exp, + t=time_tracker.get_value(), + ) + ) + graph.suspend_updating() + + graph_label = TexMobject("\\sin(x)") + graph_label.set_color(BLUE) + graph_label.rotate(90 * DEGREES, RIGHT) + graph_label.next_to( + graph.point_from_proportion(0.25), + OUT, + SMALL_BUFF, + ) + + self.play( + ShowCreation(graph), + FadeInFrom(graph_label, IN) + ) + self.wait() + graph.resume_updating() + + self.time_tracker = time_tracker + self.graph = graph + + def show_decay_surface(self): + time_tracker = self.time_tracker + axes = self.axes + + plane = Rectangle() + plane.rotate(90 * DEGREES, RIGHT) + plane.set_stroke(width=0) + plane.set_fill(WHITE, 0.2) + plane.match_depth(axes.z_axis) + plane.match_width(axes.x_axis, stretch=True) + plane.add_updater( + lambda p: p.move_to(axes.c2p( + 0, + time_tracker.get_value(), + 0, + ), LEFT) + ) + + time_slices = VGroup(*[ + self.get_time_slice_graph( + self.axes, + self.sin_exp, + t=t, + ) + for t in range(0, 10) + ]) + surface_t_tracker = ValueTracker(0) + surface = always_redraw( + lambda: self.get_surface( + self.axes, + self.sin_exp, + v_max=surface_t_tracker.get_value(), + ).set_stroke(opacity=0) + ) + + exp_graph = ParametricFunction( + lambda t: axes.c2p( + PI / 2, + t, + self.sin_exp(PI / 2, t) + ), + t_min=axes.y_min, + t_max=axes.y_max, + ) + exp_graph.set_stroke(RED, 3) + exp_graph.set_shade_in_3d(True) + + exp_label = TexMobject("e^{-\\alpha t}") + exp_label.scale(1.5) + exp_label.set_color(RED) + exp_label.rotate(90 * DEGREES, RIGHT) + exp_label.rotate(90 * DEGREES, OUT) + exp_label.next_to( + exp_graph.point_from_proportion(0.3), + OUT + UP, + ) + + self.move_camera( + theta=-25 * DEGREES, + ) + self.add(surface) + self.add(plane) + self.play( + surface_t_tracker.set_value, axes.y_max, + time_tracker.set_value, axes.y_max, + ShowIncreasingSubsets( + time_slices, + int_func=np.ceil, + ), + run_time=5, + rate_func=linear, + ) + surface.clear_updaters() + + self.play( + ShowCreation(exp_graph), + FadeOut(plane), + FadeInFrom(exp_label, IN), + time_slices.set_stroke, {"width": 1}, + ) + + def linger_at_end(self): + self.wait() + self.begin_ambient_camera_rotation(rate=-0.02) + self.wait(20) + + # + def sin_exp(self, x, t): + return np.sin(x) * np.exp(-self.k * t) + + +class BoundaryConditionReference(ShowEvolvingTempGraphWithArrows): + def construct(self): + self.setup_axes() + self.setup_graph() + + rod = self.get_rod(0, 10) + self.color_rod_by_graph(rod) + + boundary_points = [ + rod.get_right(), + rod.get_left(), + ] + boundary_dots = VGroup(*[ + Dot(point, radius=0.2) + for point in boundary_points + ]) + boundary_arrows = VGroup(*[ + Vector(2 * DOWN).next_to(dot, UP) + for dot in boundary_dots + ]) + boundary_arrows.set_stroke(YELLOW, 10) + + words = TextMobject( + "Different rules\\\\", + "at the boundary", + ) + words.scale(1.5) + words.to_edge(UP) + + # self.add(self.axes) + # self.add(self.graph) + self.add(rod) + self.play(FadeInFromDown(words)) + self.play( + LaggedStartMap(GrowArrow, boundary_arrows), + LaggedStartMap(GrowFromCenter, boundary_dots), + lag_ratio=0.3, + run_time=1, + ) + self.wait() + + +class SimulateRealSineCurve(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "axes_config": { + "x_min": 0, + "x_max": TAU, + "x_axis_config": { + "unit_size": 1.5, + "include_tip": False, + "tick_frequency": PI / 4, + }, + "y_min": -1.5, + "y_max": 1.5, + "y_axis_config": { + "tick_frequency": 0.5, + "unit_size": 2, + }, + }, + "graph_x_min": 0, + "graph_x_max": TAU, + "arrow_xs": np.linspace(0, TAU, 13), + "wait_time": 30, + "alpha": 0.5, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + self.add_arrows() + self.let_play() + + def add_labels_to_axes(self): + x_axis = self.axes.x_axis + x_axis.add(*[ + TexMobject(tex).scale(0.5).next_to( + x_axis.n2p(n), + DOWN, + buff=MED_SMALL_BUFF + ) + for tex, n in [ + ("\\tau \\over 4", TAU / 4), + ("\\tau \\over 2", TAU / 2), + ("3 \\tau \\over 4", 3 * TAU / 4), + ("\\tau", TAU), + ] + ]) + + def add_axes(self): + super().add_axes() + self.add_labels_to_axes() + + def add_rod(self): + super().add_rod() + self.rod.set_opacity(0.5) + self.rod.set_stroke(width=0) + + def initial_function(self, x): + return np.sin(x) + + def y_to_color(self, y): + return temperature_to_color(0.8 * y) + + +class SimulateLinearGraph(SimulateRealSineCurve): + CONFIG = { + "axes_config": { + "y_min": 0, + "y_max": 3, + "y_axis_config": { + "tick_frequency": 0.5, + "unit_size": 2, + }, + }, + "arrow_scale_factor": 2, + "alpha": 1, + "wait_time": 40, + "step_size": 0.02, + } + + # def let_play(self): + # pass + + def add_labels_to_axes(self): + pass + + def y_to_color(self, y): + return temperature_to_color(0.8 * (y - 1.5)) + + def initial_function(self, x): + axes = self.axes + y_max = axes.y_max + x_max = axes.x_max + slope = y_max/ x_max + return slope * x diff --git a/active_projects/ode/part3/wordy_scenes.py b/active_projects/ode/part3/wordy_scenes.py index 125fa214..2aac7093 100644 --- a/active_projects/ode/part3/wordy_scenes.py +++ b/active_projects/ode/part3/wordy_scenes.py @@ -151,3 +151,315 @@ class ThreeConstraints(WriteHeatEquationTemplate): self.wait(2) self.play(Write(items[2][1])) self.wait(2) + + +class EquationAboveSineAnalysis(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation() + equation.to_edge(UP) + equation.shift(2 * LEFT) + eq_index = equation.index_of_part_by_tex("=") + lhs = equation[:eq_index] + eq = equation[eq_index] + rhs = equation[eq_index + 1:] + t_terms = equation.get_parts_by_tex("{t}")[1:] + t_terms.save_state() + zeros = VGroup(*[ + TexMobject("0").replace(t, dim_to_match=1) + for t in t_terms + ]) + zeros.align_to(t_terms, DOWN) + new_rhs = TexMobject( + "=", "-\\alpha \\cdot {T}", "({x}, 0)", + **self.tex_mobject_config + ) + # new_rhs.move_to(equation.get_right()) + # new_rhs.next_to(equation, DOWN, MED_LARGE_BUFF) + # new_rhs.align_to(eq, LEFT) + new_rhs.next_to(equation, RIGHT) + new_rhs.shift(SMALL_BUFF * DOWN) + + self.add(equation) + self.play(ShowCreationThenFadeAround(rhs)) + self.wait() + self.play( + FadeOutAndShift(t_terms, UP), + FadeInFrom(zeros, DOWN), + ) + t_terms.fade(1) + self.wait() + self.play( + # VGroup(equation, zeros).next_to, + # new_rhs, LEFT, + FadeIn(new_rhs), + ) + self.wait() + self.play( + VGroup( + lhs[6:], + eq, + rhs, + new_rhs[0], + new_rhs[-3:], + zeros, + ).fade, 0.5, + ) + self.play(ShowCreationThenFadeAround(lhs[:6])) + self.play(ShowCreationThenFadeAround(new_rhs[1:-3])) + self.wait() + + +class ShowSinExpDerivatives(WriteHeatEquationTemplate): + CONFIG = { + "tex_mobject_config": { + "tex_to_color_map": { + "{0}": WHITE, + "\\partial": WHITE, + "=": WHITE, + } + } + } + + def construct(self): + pde = self.get_d1_equation_without_inputs() + pde.to_edge(UP) + pde.generate_target() + + new_rhs = TexMobject( + "=- \\alpha \\cdot T", + **self.tex_mobject_config, + ) + new_rhs.next_to(pde, RIGHT) + new_rhs.align_to(pde.get_part_by_tex("alpha"), DOWN) + + equation1 = TexMobject( + "T({x}, {0}) = \\sin\\left({x}\\right)", + **self.tex_mobject_config + ) + equation2 = TexMobject( + "T({x}, {t}) = \\sin\\left({x}\\right)", + "e^{-\\alpha{t}}", + **self.tex_mobject_config + ) + for eq in equation1, equation2: + eq.next_to(pde, DOWN, MED_LARGE_BUFF) + + eq2_part1 = equation2[:len(equation1)] + eq2_part2 = equation2[len(equation1):] + + # Rectangles + exp_rect = SurroundingRectangle(eq2_part2) + exp_rect.set_stroke(RED, 3) + sin_rect = SurroundingRectangle( + eq2_part1[-3:] + ) + sin_rect.set_color(BLUE) + + VGroup(pde.target, new_rhs).center().to_edge(UP) + + # Show proposed solution + self.add(pde) + self.add(equation1) + self.wait() + self.play( + MoveToTarget(pde), + FadeInFrom(new_rhs, LEFT) + ) + self.wait() + self.play( + ReplacementTransform(equation1, eq2_part1), + FadeIn(eq2_part2), + ) + self.play(ShowCreation(exp_rect)) + self.wait() + self.play(FadeOut(exp_rect)) + + # Take partial derivatives wrt x + q_mark = TexMobject("?") + q_mark.next_to(pde.get_part_by_tex("="), UP) + q_mark.set_color(RED) + + arrow1 = Vector(3 * DOWN + 1 * RIGHT, color=WHITE) + arrow1.scale(1.2 / arrow1.get_length()) + arrow1.next_to( + eq2_part2.get_corner(DL), + DOWN, MED_LARGE_BUFF + ) + ddx_label1 = TexMobject( + "\\partial \\over \\partial {x}", + **self.tex_mobject_config, + ) + ddx_label1.scale(0.7) + ddx_label1.next_to( + arrow1.point_from_proportion(0.8), + UR, SMALL_BUFF + ) + + pde_ddx = VGroup( + *pde.get_parts_by_tex("\\partial")[2:], + pde.get_parts_by_tex("\\over")[1], + pde.get_part_by_tex("{x}"), + ) + pde_ddx_rect = SurroundingRectangle(pde_ddx) + pde_ddx_rect.set_color(GREEN) + + eq2_part2_rect = SurroundingRectangle(eq2_part2) + + dx = TexMobject( + "\\cos\\left({x}\\right)", "e^{-\\alpha {t}}", + **self.tex_mobject_config + ) + ddx = TexMobject( + "-\\sin\\left({x}\\right)", "e^{-\\alpha {t}}", + **self.tex_mobject_config + ) + dx.next_to(arrow1, DOWN) + dx.align_to(eq2_part2, RIGHT) + x_shift = arrow1.get_end()[0] - arrow1.get_start()[0] + x_shift *= 2 + dx.shift(x_shift * RIGHT) + arrow2 = arrow1.copy() + arrow2.next_to(dx, DOWN) + arrow2.shift(MED_SMALL_BUFF * RIGHT) + dx_arrows = VGroup(arrow1, arrow2) + + ddx_label2 = ddx_label1.copy() + ddx_label2.shift( + arrow2.get_center() - arrow1.get_center() + ) + ddx.next_to(arrow2, DOWN) + ddx.align_to(eq2_part2, RIGHT) + ddx.shift(2 * x_shift * RIGHT) + + rhs = equation2[-6:] + + self.play( + FadeInFromDown(q_mark) + ) + self.play( + ShowCreation(pde_ddx_rect) + ) + self.wait() + self.play( + LaggedStart( + GrowArrow(arrow1), + GrowArrow(arrow2), + ), + TransformFromCopy( + pde_ddx[0], ddx_label1 + ), + TransformFromCopy( + pde_ddx[0], ddx_label2 + ), + ) + self.wait() + self.play( + TransformFromCopy(rhs, dx) + ) + self.wait() + self.play( + FadeIn(eq2_part2_rect) + ) + self.play( + Transform( + eq2_part2_rect, + SurroundingRectangle(dx[-3:]) + ) + ) + self.play( + FadeOut(eq2_part2_rect) + ) + self.wait() + self.play( + TransformFromCopy(dx, ddx) + ) + self.play( + FadeIn( + SurroundingRectangle(ddx).match_style( + pde_ddx_rect + ) + ) + ) + self.wait() + + # Take partial derivative wrt t + pde_ddt = pde[:pde.index_of_part_by_tex("=") - 1] + pde_ddt_rect = SurroundingRectangle(pde_ddt) + + dt_arrow = Arrow( + arrow1.get_start(), + arrow2.get_end() + RIGHT, + buff=0 + ) + dt_arrow.flip(UP) + dt_arrow.next_to(dx_arrows, LEFT, MED_LARGE_BUFF) + + dt_label = TexMobject( + "\\partial \\over \\partial {t}", + **self.tex_mobject_config, + ) + dt_label.scale(1) + dt_label.next_to( + dt_arrow.get_center(), UL, + SMALL_BUFF, + ) + + rhs_copy = rhs.copy() + rhs_copy.next_to(dt_arrow.get_end(), DOWN) + rhs_copy.shift(MED_LARGE_BUFF * LEFT) + rhs_copy.match_y(ddx) + + minus_alpha_in_exp = rhs_copy[-3][1:].copy() + minus_alpha_in_exp.set_color(RED) + minus_alpha = TexMobject("-\\alpha") + minus_alpha.next_to(rhs_copy, LEFT) + minus_alpha.align_to(rhs_copy[0][0], DOWN) + dot = TexMobject("\\cdot") + dot.move_to(midpoint( + minus_alpha.get_right(), + rhs_copy.get_left(), + )) + + self.play( + TransformFromCopy( + pde_ddx_rect, + pde_ddt_rect, + ) + ) + self.play( + GrowArrow(dt_arrow), + TransformFromCopy( + pde_ddt, + dt_label, + ) + ) + self.wait() + self.play(TransformFromCopy(rhs, rhs_copy)) + self.play(FadeIn(minus_alpha_in_exp)) + self.play( + ApplyMethod( + minus_alpha_in_exp.replace, minus_alpha, + path_arc=TAU / 4 + ), + FadeIn(dot), + ) + self.play( + FadeIn(minus_alpha), + FadeOut(minus_alpha_in_exp), + ) + self.wait() + rhs_copy.add(minus_alpha, dot) + self.play( + FadeIn(SurroundingRectangle(rhs_copy)) + ) + self.wait() + + # + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.move_to(q_mark, DOWN) + self.play( + FadeInFromDown(checkmark), + FadeOutAndShift(q_mark, UP) + ) + self.wait()