From caa4efdfa505d98b9b2fb75c5d86c0e543797c5f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 13 Jun 2019 09:27:35 -0700 Subject: [PATCH] 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()