From 7ae67fad09da61742cd435b585a2ffe89d76036d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Mar 2019 16:18:35 -0700 Subject: [PATCH 01/18] Quick fix for LaggedStart kwargs confusion, but this needs a better long-term solution --- manimlib/animation/composition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 3b9a6197..03704df6 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -151,8 +151,10 @@ class LaggedStartMap(LaggedStart): args_list.append(arg_creator(submob)) else: args_list.append((submob,)) + anim_kwargs = dict(kwargs) + anim_kwargs.pop("lag_ratio") animations = [ - AnimationClass(*args, **kwargs) + AnimationClass(*args, **anim_kwargs) for args in args_list ] super().__init__(*animations, **kwargs) From 5337dc5ee4e1165f9783d75d2ddd577ea040840f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Mar 2019 16:19:25 -0700 Subject: [PATCH 02/18] Fixed name conflict for AnimatedBoundary --- manimlib/mobject/changing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 9387ebd1..0b58c280 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -22,9 +22,11 @@ class AnimatedBoundary(VGroup): ] self.add(*self.boundary_copies) self.total_time = 0 - self.add_updater(lambda m, dt: self.update(dt)) + self.add_updater( + lambda m, dt: self.update_boundary_copies(dt) + ) - def update(self, dt): + def update_boundary_copies(self, dt): # Not actual time, but something which passes at # an altered rate to make the implementation below # cleaner From ea4df1f41e12897f0ae6b62cd10b5d691877c01e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Mar 2019 16:19:46 -0700 Subject: [PATCH 03/18] Editing some div_curl scenes I want to carry over --- old_projects/div_curl.py | 70 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/old_projects/div_curl.py b/old_projects/div_curl.py index 8e8114a7..698507f8 100644 --- a/old_projects/div_curl.py +++ b/old_projects/div_curl.py @@ -2817,18 +2817,16 @@ class ShowTwoPopulations(Scene): num_foxes = Integer(10) num_foxes.scale(self.count_word_scale_val) num_foxes.next_to(labels[0], RIGHT) - num_foxes.align_to(labels[0][1], DOWN) + num_foxes.align_to(labels[0][0][1], DOWN) num_rabbits = Integer(10) num_rabbits.scale(self.count_word_scale_val) num_rabbits.next_to(labels[1], RIGHT) - num_rabbits.align_to(labels[1][1], DOWN) + num_rabbits.align_to(labels[1][0][1], DOWN) - self.add(ContinualChangingDecimal( - num_foxes, lambda a: get_num_foxes() - )) - self.add(ContinualChangingDecimal( - num_rabbits, lambda a: get_num_rabbits() - )) + num_foxes.add_updater(lambda d: d.set_value(get_num_foxes())) + num_rabbits.add_updater(lambda d: d.set_value(get_num_rabbits())) + + self.add(num_foxes, num_rabbits) for count in num_foxes, num_rabbits: self.add(Mobject.add_updater( @@ -2853,13 +2851,13 @@ class ShowTwoPopulations(Scene): height=self.animal_height, fill_color=color, ) - for submob in result.family_members_with_points(): - if submob.is_subpath: - submob.is_subpath = False - submob.set_fill( - interpolate_color(color, BLACK, 0.8), - opacity=1 - ) + # for submob in result.family_members_with_points(): + # if submob.is_subpath: + # submob.is_subpath = False + # submob.set_fill( + # interpolate_color(color, BLACK, 0.8), + # opacity=1 + # ) x_shift, y_shift = [ (2 * random.random() - 1) * max_val for max_val in [ @@ -3021,25 +3019,7 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam self.pop_sizes_updates = pop_sizes_updates def write_differential_equations(self): - variables = ["X", "YY"] - equations = TexMobject( - """ - {dX \\over dt} = - X \\cdot (\\alpha - \\beta YY \\,) \\\\ - \\quad \\\\ - {dYY \\over dt} = - YY \\cdot (\\delta X - \\gamma) - """, - substrings_to_isolate=variables - ) - animals = [self.get_rabbit(), self.get_fox().flip()] - for char, animal in zip(variables, animals): - for part in equations.get_parts_by_tex(char): - animal_copy = animal.copy() - animal_copy.set_height(0.5) - animal_copy.move_to(part, DL) - Transform(part, animal_copy).update(1) - + equations = self.get_equations() equations.shift(2 * DOWN) rect = SurroundingRectangle(equations, color=YELLOW) rect.set_fill(BLACK, 0.8) @@ -3183,6 +3163,28 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam ) self.wait(self.flow_time) + # + def get_equations(self): + variables = ["X", "YY"] + equations = TexMobject( + """ + {dX \\over dt} = + X \\cdot (\\alpha - \\beta YY \\,) \\\\ + \\quad \\\\ + {dYY \\over dt} = + YY \\cdot (\\delta X - \\gamma) + """, + substrings_to_isolate=variables + ) + animals = [self.get_rabbit(), self.get_fox().flip()] + for char, animal in zip(variables, animals): + for part in equations.get_parts_by_tex(char): + animal_copy = animal.copy() + animal_copy.set_height(0.5) + animal_copy.move_to(part, DL) + part.become(animal_copy) + return equations + class PhaseFlowWords(Scene): def construct(self): From 34851761e294cbb9150f4575b1604e791e5f8c8a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Mar 2019 16:20:09 -0700 Subject: [PATCH 04/18] Many more ode1 scenes and fixes as the video gets edited --- active_projects/ode/all_part1_scenes.py | 22 +- active_projects/ode/part1/pendulum.py | 252 ++++++-- active_projects/ode/part1/phase_space.py | 37 +- active_projects/ode/part1/pi_scenes.py | 60 +- .../ode/part1/shared_constructs.py | 10 + active_projects/ode/part1/staging.py | 588 +++++++++++++++++- active_projects/ode/part1/wordy_scenes.py | 368 ++++++++++- .../ode/solve_pendulum_ode_sample_code.py | 63 ++ stage_scenes.py | 2 +- 9 files changed, 1267 insertions(+), 135 deletions(-) create mode 100644 active_projects/ode/solve_pendulum_ode_sample_code.py diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 8065580c..83e220c9 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -6,8 +6,10 @@ from active_projects.ode.part1.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part1" ALL_SCENE_CLASSES = [ + VectorFieldTest, IntroducePendulum, MultiplePendulumsOverlayed, + PeriodFormula, FormulasAreLies, MediumAnglePendulum, MediumHighAnglePendulum, @@ -18,21 +20,31 @@ ALL_SCENE_CLASSES = [ VeryLowAnglePendulum, FormulasAreLies, TourOfDifferentialEquations, + WherePendulumLeads, + LongDoublePendulum, # FollowThisThread, StrogatzQuote, + ShowHorizontalDashedLine, + RabbitFoxPopulations, + RabbitFoxEquation, # Something... ShowGravityAcceleration, AnalyzePendulumForce, + ShowSineValues, BuildUpEquation, ShowDerivativeVideo, SubtleAirCurrents, + SimpleDampenedPendulum, DefineODE, + SecondOrderEquationExample, ODEvsPDEinFrames, ProveTeacherWrong, SetAsideSeekingSolution, - ReferencePiCollisionStateSpaces, + # VisualizeHeightSlopeCurvature, VisualizeStates, + ReferencePiCollisionStateSpaces, IntroduceVectorField, + XComponentArrows, BreakingSecondOrderIntoTwoFirstOrder, ShowPendulumPhaseFlow, ShowHighVelocityCase, @@ -42,10 +54,18 @@ ALL_SCENE_CLASSES = [ LorenzVectorField, ThreeBodiesInSpace, AltThreeBodiesInSpace, + ThreeBodyTitle, ThreeBodySymbols, AskAboutActuallySolving, WriteODESolvingCode, TakeManyTinySteps, InaccurateComputation, HungerForExactness, + ShowRect, + JumpToThisPoint, + ThreeBodyEquation, + ItGetsWorse, + ChaosTitle, + RevisitQuote, + EndScreen, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 5feba2d6..4d8c28b7 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -287,6 +287,9 @@ class ThetaVsTAxes(Axes): y_axis.label = theta_label y_axis.add(theta_label) + self.y_axis_label = theta_label + self.x_axis_label = t_label + x_axis.add_numbers() y_axis.add(self.get_y_axis_coordinates(y_axis)) @@ -357,6 +360,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): "length": 3, "top_point": 4 * RIGHT, "weight_diameter": 0.35, + "gravity": 20, }, "theta_vs_t_axes_config": { "y_max": PI / 4, @@ -366,6 +370,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): "unit_size": 2, "tip_length": 0.3, }, + "x_max": 12, "number_line_config": { "stroke_width": 2, } @@ -378,12 +383,13 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): def construct(self): self.add_pendulum() - self.label_pi_creatures() + # self.label_pi_creatures() self.label_pendulum() self.add_graph() + self.label_function() self.show_graph_period() self.show_length_and_gravity() - self.tweak_length_and_gravity() + # self.tweak_length_and_gravity() def create_pi_creatures(self): randy = Randolph(color=BLUE_C) @@ -437,7 +443,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): GrowArrow(morty_label.arrow), morty.change, "raise_right_hand", ) - self.wait() + self.wait(2) def label_pendulum(self): pendulum = self.pendulum @@ -446,18 +452,18 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): rect = SurroundingRectangle(label, buff=0.5 * SMALL_BUFF) rect.add_updater(lambda r: r.move_to(label)) - self.add(rect) + for pi in randy, morty: + pi.add_updater( + lambda m: m.look_at(pendulum.weight) + ) + + self.play(randy.change, "pondering") + self.play(morty.change, "pondering") + self.wait(3) + randy.clear_updaters() + morty.clear_updaters() self.play( ShowCreationThenFadeOut(rect), - ShowCreationThenDestruction( - label.copy().set_style( - fill_opacity=0, - stroke_color=PINK, - stroke_width=2, - ) - ), - randy.change, "pondering", - morty.change, "pondering", ) self.wait() @@ -467,7 +473,10 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): axes.to_corner(UL) self.play( - Restore(self.camera_frame), + Restore( + self.camera_frame, + rate_func=squish_rate_func(smooth, 0, 0.9), + ), DrawBorderThenFill( axes, rate_func=squish_rate_func(smooth, 0.5, 1), @@ -481,11 +490,30 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): ), run_time=3, ) - self.wait(2) - self.graph = axes.get_live_drawn_graph(self.pendulum) + self.wait(1.5) + self.graph = axes.get_live_drawn_graph(self.pendulum) self.add(self.graph) - self.wait(4) + + def label_function(self): + hm_word = TextMobject("Simple harmonic motion") + hm_word.scale(1.25) + hm_word.to_edge(UP) + + formula = TexMobject( + "=\\theta_0 \\cos(\\sqrt{g / L} t)" + ) + formula.next_to( + self.axes.y_axis_label, RIGHT, SMALL_BUFF + ) + formula.set_stroke(width=0, background=True) + + self.play(FadeInFrom(hm_word, DOWN)) + self.wait() + self.play( + Write(formula), + hm_word.to_corner, UR + ) def show_graph_period(self): pendulum = self.pendulum @@ -503,13 +531,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): line.shift(SMALL_BUFF * RIGHT) brace = Brace(line, UP, buff=SMALL_BUFF) brace.add_to_back(brace.copy().set_style(BLACK, 10)) - formula = TexMobject( - "\\sqrt{\\,", "2\\pi", "L", "/", "g", "}", - tex_to_color_map={ - "L": BLUE, - "g": YELLOW, - } - ) + formula = get_period_formula() formula.next_to(brace, UP, SMALL_BUFF) self.period_formula = formula @@ -537,29 +559,22 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): self.pendulum, length_multiple=0.5 / 9.8, ) + down_vectors = self.get_down_vectors() + down_vectors.set_color(YELLOW) + down_vectors.set_opacity(0.5) - self.play(ShowCreationThenDestructionAround(L)) - dot = Dot(fill_opacity=0.25) - dot.move_to(L) self.play( + ShowCreationThenDestructionAround(L), ShowCreation(new_rod), - dot.move_to, new_rod, - dot.fade, 1, ) - self.remove(dot) self.play(FadeOut(new_rod)) - self.wait() - self.play(ShowCreationThenDestructionAround(g)) - dot.move_to(g) - dot.set_fill(opacity=0.5) self.play( + ShowCreationThenDestructionAround(g), GrowArrow(g_vect), - dot.move_to, g_vect, - dot.fade, 1, ) - self.remove(dot) - self.wait(2) + self.play(self.get_down_vectors_animation(down_vectors)) + self.wait(6) self.gravity_vector = g_vect @@ -593,15 +608,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): # new_pendulum_config["initial_theta"] = pendulum.get_theta() new_pendulum = Pendulum(**new_pendulum_config) - down_vectors = VGroup(*[ - Vector(0.5 * DOWN) - for x in range(10 * 150) - ]) - down_vectors.arrange_in_grid(10, 150, buff=MED_SMALL_BUFF) - down_vectors.set_color_by_gradient(BLUE, RED) - # for vect in down_vectors: - # vect.shift(0.1 * np.random.random(3)) - down_vectors.to_edge(RIGHT) + down_vectors = self.get_down_vectors() self.play(randy.change, "happy") self.play( @@ -624,10 +631,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): g_vect.scale(2) self.play( FadeOut(graph2), - LaggedStart(*[ - GrowArrow(v, rate_func=there_and_back) - for v in down_vectors - ], lag_ratio=0.0005, run_time=2, remover=True) + self.get_down_vectors_animation(down_vectors) ) self.play( FadeIn(graph3), @@ -635,6 +639,30 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): ) self.wait(6) + # + def get_down_vectors(self): + down_vectors = VGroup(*[ + Vector(0.5 * DOWN) + for x in range(10 * 150) + ]) + down_vectors.arrange_in_grid(10, 150, buff=MED_SMALL_BUFF) + down_vectors.set_color_by_gradient(BLUE, RED) + # for vect in down_vectors: + # vect.shift(0.1 * np.random.random(3)) + down_vectors.to_edge(RIGHT) + return down_vectors + + def get_down_vectors_animation(self, down_vectors): + return LaggedStart( + *[ + GrowArrow(v, rate_func=there_and_back) + for v in down_vectors + ], + lag_ratio=0.0005, + run_time=2, + remover=True + ) + class MultiplePendulumsOverlayed(Scene): CONFIG = { @@ -868,6 +896,95 @@ class VeryLowAnglePendulum(LowAnglePendulum): } +class WherePendulumLeads(PiCreatureScene): + def construct(self): + pendulum = Pendulum( + top_point=UP, + length=3, + gravity=20, + ) + pendulum.start_swinging() + + l_title = TextMobject("Linearization") + l_title.scale(1.5) + l_title.to_corner(UL) + c_title = TextMobject("Chaos") + c_title.scale(1.5) + c_title.move_to(l_title) + c_title.move_to( + c_title.get_center() * np.array([-1, 1, 1]) + ) + + get_theta = pendulum.get_theta + spring = always_redraw( + lambda: ParametricFunction( + lambda t: np.array([ + np.cos(TAU * t) + (1.4 + get_theta()) * t, + np.sin(TAU * t) - 0.5, + 0, + ]), + t_min=-0.5, + t_max=7, + color=GREY, + sheen_factor=1, + sheen_direction=UL, + ).scale(0.2).to_edge(LEFT, buff=0) + ) + spring_rect = SurroundingRectangle( + spring, buff=MED_LARGE_BUFF, + stroke_width=0, + fill_color=BLACK, + fill_opacity=0, + ) + + weight = Dot(radius=0.25) + weight.add_updater(lambda m: m.move_to( + spring.points[-1] + )) + weight.set_color(BLUE) + weight.set_sheen(1, UL) + spring_system = VGroup(spring, weight) + + linear_formula = TexMobject( + "\\frac{d \\vec{\\textbf{x}}}{dt}=" + "A\\vec{\\textbf{x}}" + ) + linear_formula.next_to(spring, UP, LARGE_BUFF) + linear_formula.match_x(l_title) + + randy = self.pi_creature + randy.set_height(2) + randy.center() + randy.to_edge(DOWN) + randy.shift(3 * LEFT) + q_marks = TexMobject("???") + q_marks.next_to(randy, UP) + + self.add(pendulum, randy) + self.play( + randy.change, "pondering", pendulum, + FadeInFromDown(q_marks, lag_ratio=0.3) + ) + self.play(randy.look_at, pendulum) + self.wait(5) + self.play( + Animation(VectorizedPoint(pendulum.get_top())), + FadeOutAndShift(q_marks, UP, lag_ratio=0.3), + ) + self.add(spring_system) + self.play( + FadeOut(spring_rect), + FadeInFrom(linear_formula, UP), + FadeInFromDown(l_title), + ) + self.play(FadeInFromDown(c_title)) + self.wait(8) + + +class LongDoublePendulum(ExternallyAnimatedScene): + pass + + class AnalyzePendulumForce(MovingCameraScene): CONFIG = { "pendulum_config": { @@ -1215,13 +1332,22 @@ class AnalyzePendulumForce(MovingCameraScene): self.play(FocusOn(pendulum.theta_label)) self.play(Indicate(pendulum.theta_label)) - old_updaters = pendulum.get_updaters() - pendulum.clear_updaters(recursive=False) - pendulum.start_swinging() + + pendulum_copy = pendulum.deepcopy() + pendulum_copy.clear_updaters() + pendulum_copy.fade(1) + pendulum_copy.start_swinging() + + def new_updater(p): + p.set_theta(pendulum_copy.get_theta()) + pendulum.add_updater(new_updater) + + self.add(pendulum_copy) self.wait(5) - pendulum.clear_updaters() - for updater in old_updaters: - pendulum.add_updater(updater) + pendulum_copy.end_swinging() + self.remove(pendulum_copy) + pendulum.remove_updater(new_updater) + self.update_mobjects(0) def show_arc_length(self): pendulum = self.pendulum @@ -1664,6 +1790,18 @@ class BuildUpEquation(Scene): self.wait() +class SimpleDampenedPendulum(Scene): + def construct(self): + pendulum = Pendulum( + top_point=ORIGIN, + initial_theta=150 * DEGREES, + mu=0.5, + ) + self.add(pendulum) + pendulum.start_swinging() + self.wait(20) + + class NewSceneName(Scene): def construct(self): pass diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index baf80d8d..63992f6c 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -951,13 +951,13 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): self.initialize_vector_field() self.add(self.vector_field) self.add_flexible_state() + self.show_high_vector() self.show_trajectory() - def show_trajectory(self): + def add_flexible_state(self): + super().add_flexible_state() state = self.state plane = self.plane - field = self.vector_field - frame = self.camera_frame state.to_edge(DOWN, buff=SMALL_BUFF), start_point = plane.coords_to_point(0, 4) @@ -965,6 +965,33 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): dot.move_to(start_point) state.update() + self.dot = dot + self.start_point = start_point + + def show_high_vector(self): + field = self.vector_field + top_vectors = VGroup(*filter( + lambda a: np.all(a.get_center() > [-10, 1.5, -10]), + field + )).copy() + top_vectors.set_stroke(PINK, 3) + top_vectors.sort(lambda p: p[0]) + + self.play( + ShowCreationThenFadeOut( + top_vectors, + run_time=2, + lag_ratio=0.01, + ) + ) + + def show_trajectory(self): + state = self.state + field = self.vector_field + frame = self.camera_frame + dot = self.dot + start_point = self.start_point + traj = VMobject() traj.start_new_path(start_point) dt = 0.01 @@ -998,6 +1025,10 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): class TweakMuInFormula(Scene): def construct(self): + self.add(FullScreenFadeRectangle( + opacity=0.75, + )) + ode = get_ode() ode.to_edge(DOWN, buff=LARGE_BUFF) mu = ode.get_part_by_tex("\\mu") diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index f3d304a3..4c753585 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -111,7 +111,11 @@ class FormulasAreLies(PiCreatureScene): class ProveTeacherWrong(TeacherStudentsScene): def construct(self): tex_config = { - "tex_to_color_map": {"{\\theta}": BLUE} + "tex_to_color_map": { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, + } } func = TexMobject( "{\\theta}(t)", "=", @@ -119,23 +123,24 @@ class ProveTeacherWrong(TeacherStudentsScene): **tex_config, ) d_func = TexMobject( - "\\dot {\\theta}(t)", "=", + "{\\dot\\theta}(t)", "=", "-\\left(\\sqrt{g / L}\\right)", "\\theta_0", "\\sin(\\sqrt{g / L} \\cdot t)", **tex_config, ) dd_func = TexMobject( - "\\ddot {\\theta}(t)", "=", + "{\\ddot\\theta}(t)", "=", "-\\left(g / L\\right)", "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)", **tex_config, ) - ode = TexMobject( - "\\ddot {\\theta}({t})", "=", - "-\\mu \\dot {\\theta}({t})", - "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", - **tex_config, - ) + # ode = TexMobject( + # "\\ddot {\\theta}({t})", "=", + # "-\\mu \\dot {\\theta}({t})", + # "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", + # **tex_config, + # ) + ode = get_ode() arrows = [TexMobject("\\Downarrow") for x in range(2)] VGroup(func, d_func, dd_func, ode, *arrows).scale(0.7) @@ -376,9 +381,44 @@ class HungerForExactness(TeacherStudentsScene): ) self.wait() self.wait(3) + self.change_student_modes("tired", "sad", "concerned_musician") + self.wait(4) + self.look_at(solution) + self.wait(5) self.play( FadeOutAndShift(solution, 2 * LEFT), Restore(ode), - self.get_student_changes(*3 * ["sick"]) + self.get_student_changes( + "sick", "angry", "tired", + ) ) self.wait(3) + + mystery = TexMobject( + "\\theta(t) = ???", + tex_to_color_map={"\\theta": BLUE}, + ) + mystery.scale(2) + mystery.to_edge(UP) + mystery.set_stroke(width=0, background=True) + + self.play( + FadeInFromDown(mystery), + self.teacher.change, "pondering" + ) + self.add( + AnimatedBoundary(mystery, stroke_width=1), + mystery, + ) + self.change_all_student_modes("sad") + self.look_at(mystery) + self.wait(5) + + +class ItGetsWorse(TeacherStudentsScene): + def construct(self): + self.teacher_says("It gets\\\\worse") + self.change_student_modes( + "hesitant", "pleading", "erm" + ) + self.wait(2) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 265a4585..e803f694 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -35,6 +35,16 @@ def get_ode(): return ode +def get_period_formula(): + return TexMobject( + "\\sqrt{\\,", "2\\pi", "L", "/", "g", "}", + tex_to_color_map={ + "L": BLUE, + "g": YELLOW, + } + ) + + def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): theta, omega = point[:2] return np.array([ diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 8fb884da..7cda089e 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -3,6 +3,8 @@ from active_projects.ode.part1.shared_constructs import * from active_projects.ode.part1.pendulum import Pendulum from active_projects.ode.part1.pendulum import ThetaVsTAxes from active_projects.ode.part1.phase_space import IntroduceVectorField +from old_projects.div_curl import PhaseSpaceOfPopulationModel +from old_projects.div_curl import ShowTwoPopulations # Scenes @@ -15,16 +17,21 @@ class VectorFieldTest(Scene): ) mu_tracker = ValueTracker(1) field = VectorField( - lambda p: pendulum_vector_field( + lambda p: pendulum_vector_field_func( plane.point_to_coords(p), mu=mu_tracker.get_value() ), delta_x=0.5, delta_y=0.5, - max_magnitude=4, + max_magnitude=6, opacity=0.5, # length_func=lambda norm: norm, ) + field.set_opacity(1) + + self.add(plane, field) + return + stream_lines = StreamLines( field.func, delta_x=0.5, @@ -39,6 +46,26 @@ class VectorFieldTest(Scene): self.wait(10) +class ShowRect(Scene): + def construct(self): + rect = Rectangle() + rect.set_stroke(YELLOW) + rect.set_height(1) + rect.set_width(3, stretch=True) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + + +class PeriodFormula(Scene): + def construct(self): + formula = get_period_formula() + formula.scale(2) + q_mark = TexMobject("?") + q_mark.scale(3) + q_mark.next_to(formula, RIGHT) + self.add(formula, q_mark) + + class TourOfDifferentialEquations(MovingCameraScene): CONFIG = { "screen_rect_style": { @@ -214,13 +241,32 @@ class HeatEquationPreview(ExternallyAnimatedScene): pass +class ShowHorizontalDashedLine(Scene): + def construct(self): + line = DashedLine(LEFT_SIDE, RIGHT_SIDE) + self.play(ShowCreation(line)) + self.wait() + + +class RabbitFoxPopulations(ShowTwoPopulations): + pass + + +class RabbitFoxEquation(PhaseSpaceOfPopulationModel): + def construct(self): + equations = self.get_equations() + self.add(equations) + + class ShowGravityAcceleration(Scene): def construct(self): self.add_gravity_field() self.add_title() self.pulse_gravity_down() + self.show_g_value() self.show_trajectory() self.combine_v_vects() + self.show_g_symbol() def add_gravity_field(self): gravity_field = self.gravity_field = VectorField( @@ -238,17 +284,11 @@ class ShowGravityAcceleration(Scene): title = self.title = TextMobject("Gravitational acceleration") title.scale(1.5) title.to_edge(UP) - g_eq = self.g_eq = TexMobject( - "{g}", "=", "-9.8", "\\frac{\\text{m/s}}{\\text{s}}", - **Lg_formula_config + title.add_background_rectangle( + buff=0.05, + opacity=1, ) - g_eq.next_to(title, DOWN) - for mob in title, g_eq: - mob.add_background_rectangle_to_submobjects( - buff=0.05, - opacity=1, - ) - self.add(title, g_eq) + self.play(FadeInFromDown(title)) def pulse_gravity_down(self): field = self.gravity_field @@ -260,7 +300,36 @@ class ShowGravityAcceleration(Scene): ) for vector in field ]), run_time=2, lag_ratio=0.001) - self.add(self.title, self.g_eq) + self.add(self.title) + + def show_g_value(self): + title = self.title + g_eq = self.g_eq = TexMobject( + "-9.8", "{\\text{m/s}", "\\over", "\\text{s}}", + **Lg_formula_config + ) + g_eq.add_background_rectangle_to_submobjects() + g_eq.scale(2) + g_eq.center() + num, ms, per, s = g_eq + + self.add(num) + self.wait(0.75) + self.play( + FadeInFrom(ms, 0.25 * DOWN, run_time=0.5) + ) + self.wait(0.25) + self.play(LaggedStart( + GrowFromPoint(per, per.get_left()), + FadeInFrom(s, 0.5 * UP), + lag_ratio=0.7, + run_time=0.75 + )) + self.wait() + self.play( + g_eq.scale, 0.5, + g_eq.next_to, title, DOWN, + ) def show_trajectory(self): ball = Circle( @@ -329,6 +398,18 @@ class ShowGravityAcceleration(Scene): rate_func=squish_rate_func(smooth, 0, 0.1) ) + time_label = TextMobject("Time = ") + time_label.shift(MED_SMALL_BUFF * LEFT) + time_tracker = ValueTracker(0) + time = DecimalNumber(0) + time.next_to(time_label, RIGHT) + time.add_updater(lambda d, dt: d.set_value( + time_tracker.get_value() + )) + time_group = VGroup(time_label, time) + time_group.center().to_edge(DOWN) + self.add(time_group) + ball_copies = VGroup() v_vect_copies = VGroup() self.add(dashed_graph, ball) @@ -353,11 +434,16 @@ class ShowGravityAcceleration(Scene): ShowCreation(dashed_graph, **kw), MoveAlongPath(ball, graph, **kw), MoveAlongPath(v_point, velocity_graph, **kw), + ApplyMethod( + time_tracker.increment_value, 1, + rate_func=linear + ), flash, run_time=1, ) dashed_graph.restore() randy.clear_updaters() + self.play(FadeOut(time_group)) self.wait() self.v_vects = v_vect_copies @@ -406,16 +492,30 @@ class ShowGravityAcceleration(Scene): ) self.wait() + def show_g_symbol(self): + g = TexMobject("g") + brace = Brace(self.g_eq[0][2:], UP, buff=SMALL_BUFF) + g.scale(1.5) + g.next_to(brace, UP) + g.set_color(YELLOW) + self.play( + FadeOut(self.title), + GrowFromCenter(brace), + FadeInFrom(g, UP), + ) + self.wait() + class ShowDerivativeVideo(Scene): - CONFIG = { - "camera_config": {"background_color": DARKER_GREY} - } - def construct(self): - title = TextMobject("Essence of Calculus") - title.scale(1.25) + title = TextMobject("Essence of", "Calculus") + title.scale(1.5) title.to_edge(UP) + + title2 = TextMobject("Essence of", "Linear Algebra") + title2.scale(1.5) + title2.move_to(title, DOWN) + rect = ScreenRectangle(height=6) rect = rect.copy() rect.set_style( @@ -428,6 +528,8 @@ class ShowDerivativeVideo(Scene): self.add(title, rect) self.add(animated_rect) + self.wait(5) + self.play(ReplacementTransform(title, title2)) self.wait(10) @@ -483,7 +585,7 @@ class DefineODE(Scene): def write_differential_equation(self): de_word = TextMobject("Differential", "Equation") - de_word.to_edge(UP) + de_word.to_edge(UP, buff=MED_SMALL_BUFF) equation = get_ode() equation.next_to(de_word, DOWN) @@ -777,11 +879,7 @@ class DefineODE(Scene): self.second_order_word = so def show_higher_order_examples(self): - main_example = VGroup( - self.second_order_word, - self.ode_initials, - self.equation - ) + main_example = self.get_main_example() tex_config = {"tex_to_color_map": {"{x}": BLUE}} example3 = VGroup( TextMobject("Third order ODE"), @@ -821,6 +919,13 @@ class DefineODE(Scene): ) self.wait(2) + def get_main_example(self): + return VGroup( + self.second_order_word, + self.ode_initials, + self.equation + ) + def show_changing_curvature_group(self): t_tracker = self.t_tracker curvature_group = self.curvature_group @@ -828,7 +933,8 @@ class DefineODE(Scene): graph = VMobject() graph.pointwise_become_partial( self.fake_graph, - 0.25, 1, + t_tracker.get_value() / self.axes.x_max, + 1, ) dashed_graph = DashedVMobject(graph, num_dashes=100) dashed_graph.set_stroke(GREEN, 1) @@ -844,14 +950,397 @@ class DefineODE(Scene): self.wait() -class ODEvsPDEinFrames(Scene): +# Largely a copy of DefineODE, which is not great. +# But what can you do? +class SecondOrderEquationExample(DefineODE): def construct(self): - pass + self.add_graph() + self.write_differential_equation() + self.show_value_slope_curvature() + self.show_higher_order_examples() + self.show_changing_curvature_group() + + def add_graph(self): + axes = self.axes = Axes( + x_min=0, + x_max=10.5, + y_min=-2.5, + y_max=2.5, + ) + axes.center() + axes.to_edge(DOWN) + x_t = TexMobject("x", "(t)") + x_t.set_color_by_tex("x", BLUE) + t = TexMobject("t") + t.next_to(axes.x_axis.get_right(), UP) + x_t.next_to(axes.y_axis.get_top(), UP) + + axes.add(t, x_t) + axes.add_coordinates() + + self.add(axes) + + def write_differential_equation(self): + de_word = TextMobject("Differential", "Equation") + de_word.scale(1.25) + de_word.to_edge(UP, buff=MED_SMALL_BUFF) + so_word = TextMobject("Second Order") + so_word.scale(1.25) + de_word.generate_target() + group = VGroup(so_word, de_word.target) + group.arrange(RIGHT) + group.to_edge(UP, buff=MED_SMALL_BUFF) + so_word.align_to(de_word.target[0], DOWN) + so_line = Line(LEFT, RIGHT, color=YELLOW) + so_line.match_width(so_word) + so_line.next_to(so_word, DOWN, buff=SMALL_BUFF) + + equation = TexMobject( + "{\\ddot x}(t)", "=", + "-\\mu", "{\\dot x}(t)", + "-", "\\omega", "{x}(t)", + tex_to_color_map={ + "{x}": BLUE, + "{\\dot x}": YELLOW, + "{\\ddot x}": RED, + } + ) + equation.next_to(de_word, DOWN) + + dd_x_part = equation[:2] + dd_x_rect = SurroundingRectangle(dd_x_part) + + self.add(de_word, equation) + self.play( + MoveToTarget(de_word), + FadeInFrom(so_word, RIGHT), + GrowFromCenter(so_line), + ) + self.play(ReplacementTransform(so_line, dd_x_rect)) + self.play(FadeOut(dd_x_rect)) + + self.equation = equation + self.title = VGroup(*so_word, *de_word) + + def show_value_slope_curvature(self): + axes = self.axes + graph = axes.get_graph( + lambda t: -2.5 * np.cos(2 * t) * np.exp(-0.2 * t) + ) + + tex_config = { + "tex_to_color_map": { + "{x}": BLUE, + "{\\dot x}": YELLOW, + "{\\ddot x}": RED, + }, + "height": 0.5, + } + x, d_x, dd_x = [ + TexMobject( + "{" + s + "x}(t)", + **tex_config + ) + for s in ("", "\\dot ", "\\ddot ") + ] + + t_tracker = ValueTracker(1.25) + get_t = t_tracker.get_value + + def get_point(t): + return graph.point_from_proportion(t / axes.x_max) + + def get_dot(): + return Dot(get_point(get_t())).scale(0.5) + + def get_v_line(): + point = get_point(get_t()) + x_point = axes.x_axis.number_to_point( + axes.x_axis.point_to_number(point) + ) + return DashedLine( + x_point, point, + dash_length=0.025, + stroke_color=BLUE, + stroke_width=2, + ) + + def get_tangent_line(curve, alpha): + line = Line( + ORIGIN, 1.5 * RIGHT, + color=YELLOW, + stroke_width=1.5, + ) + da = 0.0001 + p0 = curve.point_from_proportion(alpha) + p1 = curve.point_from_proportion(alpha - da) + p2 = curve.point_from_proportion(alpha + da) + angle = angle_of_vector(p2 - p1) + line.rotate(angle) + line.move_to(p0) + return line + + def get_slope_line(): + return get_tangent_line( + graph, get_t() / axes.x_max + ) + + def get_curve(): + curve = VMobject() + t = get_t() + curve.set_points_smoothly([ + get_point(t + a) + for a in np.linspace(-0.5, 0.5, 11) + ]) + curve.set_stroke(RED, 1) + return curve + + v_line = always_redraw(get_v_line) + dot = always_redraw(get_dot) + slope_line = always_redraw(get_slope_line) + curve = always_redraw(get_curve) + + x.next_to(v_line, RIGHT, SMALL_BUFF) + d_x.next_to(slope_line.get_end(), UP, SMALL_BUFF) + dd_x.next_to(curve.get_end(), RIGHT, SMALL_BUFF) + xs = VGroup(x, d_x, dd_x) + + words = VGroup( + TextMobject("= Height").set_color(BLUE), + TextMobject("= Slope").set_color(YELLOW), + TextMobject("= ``Curvature''").set_color(RED), + ) + words.scale(0.75) + for word, sym in zip(words, xs): + word.next_to(sym, RIGHT, buff=2 * SMALL_BUFF) + sym.word = word + + self.play( + ShowCreation(v_line), + FadeInFromPoint(dot, v_line.get_start()), + FadeInFrom(x, DOWN), + FadeInFrom(x.word, DOWN), + ) + self.add(slope_line, dot) + self.play( + ShowCreation(slope_line), + FadeInFrom(d_x, LEFT), + FadeInFrom(d_x.word, LEFT), + ) + + a_tracker = ValueTracker(0) + curve_copy = curve.copy() + changing_slope = always_redraw( + lambda: get_tangent_line( + curve_copy, + a_tracker.get_value(), + ).set_stroke( + opacity=there_and_back(a_tracker.get_value()) + ) + ) + self.add(curve, dot) + self.play( + ShowCreation(curve), + FadeInFrom(dd_x, LEFT), + FadeInFrom(dd_x.word, LEFT), + ) + self.add(changing_slope) + self.play( + a_tracker.set_value, 1, + run_time=3, + ) + self.remove(changing_slope, a_tracker) + + self.t_tracker = t_tracker + self.curvature_group = VGroup( + v_line, slope_line, curve, dot + ) + self.curvature_group_labels = VGroup(xs, words) + self.fake_graph = graph + + def get_main_example(self): + return VGroup( + self.equation, + self.title, + ) + +# class VisualizeHeightSlopeCurvature(DefineODE): +# CONFIG = { +# "pendulum_config": { +# "length": 2, +# "top_point": 5 * RIGHT + 2 * UP, +# "initial_theta": 175 * DEGREES, +# "mu": 0.3, +# }, +# } + +# def construct(self): +# self.add_graph() +# self.show_value_slope_curvature() +# self.show_changing_curvature_group() + + +class ODEvsPDEinFrames(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + frames.shift(0.5 * DOWN) + + animated_frames = VGroup(*[ + AnimatedBoundary( + frame, + cycle_rate=0.2, + max_stroke_width=1, + ) + for frame in frames + ]) + + titles = VGroup( + # TextMobject("ODEs"), + # TextMobject("PDEs"), + TextMobject("Ordinary", "Differential", "Equations"), + TextMobject("Partial", "Differential", "Equations"), + ) + for title, frame in zip(titles, frames): + title.arrange( + DOWN, + buff=MED_SMALL_BUFF, + aligned_edge=LEFT + ) + title.next_to(frame, UP, MED_LARGE_BUFF) + title.initials = VGroup(*[ + part[0] for part in title + ]) + titles[0][1].shift(0.05 * UP) + + # ODE content + ode = get_ode() + ode.set_width(frames[0].get_width() - MED_LARGE_BUFF) + ode.next_to(frames[0].get_top(), DOWN) + ts = ode.get_parts_by_tex("{t}") + one_input = TextMobject("One input") + one_input.next_to(frames[0].get_bottom(), UP) + o_arrows = VGroup(*[ + Arrow( + one_input.get_top(), + t.get_bottom(), + buff=0.2, + color=WHITE, + max_tip_length_to_length_ratio=0.075, + path_arc=pa + ) + for t, pa in zip(ts, [-0.7, 0, 0.7]) + ]) + o_arrows.set_stroke(width=3) + frames[0].add(ode, one_input, o_arrows) + + # PDE content + pde = TexMobject( + """ + \\frac{\\partial T}{\\partial t} + {(x, y, t)} = + \\frac{\\partial^2 T}{\\partial x^2} + {(x, y, t)} + + \\frac{\\partial^2 T}{\\partial y^2} + {(x, y, t)} + """, + tex_to_color_map={"{(x, y, t)}": WHITE} + ) + pde.set_width(frames[1].get_width() - MED_LARGE_BUFF) + pde.next_to(frames[1].get_top(), DOWN) + inputs = pde.get_parts_by_tex("{(x, y, t)}") + multi_input = TextMobject("Multiple inputs") + multi_input.next_to(frames[1].get_bottom(), UP) + p_arrows = VGroup(*[ + Arrow( + multi_input.get_top(), + mob.get_bottom(), + buff=0.2, + color=WHITE, + max_tip_length_to_length_ratio=0.075, + path_arc=pa + ) + for mob, pa in zip(inputs, [-0.7, 0, 0.7]) + ]) + p_arrows.set_stroke(width=3) + frames[1].add(pde, multi_input, p_arrows) + + self.add( + frames[0], + animated_frames[0], + titles[0] + ) + self.play( + Write(one_input), + ShowCreation(o_arrows, lag_ratio=0.5) + ) + self.wait(2) + self.play(titles[0].initials.set_color, BLUE) + self.wait(7) + + # Transition + self.play( + TransformFromCopy(*titles), + TransformFromCopy(*frames), + ) + self.play(VFadeIn(animated_frames[1])) + self.wait() + self.play(titles[1].initials.set_color, YELLOW) + self.wait(30) class ReferencePiCollisionStateSpaces(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + def construct(self): - pass + title = TextMobject("The block collision puzzle") + title.scale(1.5) + title.to_edge(UP) + self.add(title) + + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + boundary = AnimatedBoundary(frames) + self.add(frames, boundary) + self.wait(15) + + +class XComponentArrows(Scene): + def construct(self): + field = VectorField( + lambda p: np.array([p[1], 0, 0]) + ) + field.set_opacity(0.75) + for u in (1, -1): + field.sort(lambda p: u * p[0]) + self.play(LaggedStartMap( + GrowArrow, field, + lag_ratio=0.1, + run_time=1 + )) + self.play(FadeOut(field)) class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): @@ -879,6 +1368,14 @@ class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): system1.next_to(sys_word, DOWN) system2.move_to(system1) + theta_dots = VGroup(*filter( + lambda m: ( + isinstance(m, SingleStringTexMobject) and + "{\\dot\\theta}" == m.get_tex_string() + ), + system1.get_family(), + )) + self.add(ode) self.play(FadeInFrom(so_word, 0.5 * DOWN)) self.wait() @@ -905,6 +1402,13 @@ class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): FadeInFromDown(sys_word) ) self.wait() + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + theta_dots, + surrounding_rectangle_config={ + "color": PINK, + } + )) self.play(ReplacementTransform(system1, system2)) self.wait() @@ -993,6 +1497,13 @@ class ThreeBodiesInSpace(SpecialThreeDScene): axes.set_stroke(width=0.5) self.add(axes) + # Orient + self.set_camera_orientation( + phi=70 * DEGREES, + theta=-110 * DEGREES, + ) + self.begin_ambient_camera_rotation() + def add_bodies(self): masses = self.masses colors = self.colors @@ -1071,11 +1582,6 @@ class ThreeBodiesInSpace(SpecialThreeDScene): self.add(traj, body) def let_play(self): - self.set_camera_orientation( - phi=70 * DEGREES, - theta=-110 * DEGREES, - ) - self.begin_ambient_camera_rotation() # Break it up to see partial files as # it's rendered for x in range(int(self.play_time)): @@ -1127,6 +1633,20 @@ class AltThreeBodiesInSpace(ThreeBodiesInSpace): } +class TwoBodiesInSpace(ThreeBodiesInSpace): + CONFIG = { + "colors": [GREY, BLUE], + "masses": [1, 6], + "play_time": 5, + } + + def construct(self): + self.add_axes() + self.add_bodies() + self.add_trajectories() + self.let_play() + + class DefineODECopy(DefineODE): pass diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py index 6a966789..8ada4c57 100644 --- a/active_projects/ode/part1/wordy_scenes.py +++ b/active_projects/ode/part1/wordy_scenes.py @@ -37,6 +37,40 @@ class SmallAngleApproximationTex(Scene): class StrogatzQuote(Scene): def construct(self): + quote = self.get_quote() + movers = VGroup(*quote[:-1].family_members_with_points()) + for mover in movers: + mover.save_state() + disc = Circle(radius=0.05) + disc.set_stroke(width=0) + disc.set_fill(BLACK, 0) + disc.move_to(mover) + mover.become(disc) + self.play( + FadeInFrom(quote.author_part, LEFT), + LaggedStartMap( + # FadeInFromLarge, + # quote[:-1].family_members_with_points(), + Restore, movers, + lag_ratio=0.005, + run_time=2, + ) + # FadeInFromDown(quote[:-1]), + # lag_ratio=0.01, + ) + self.wait() + self.play( + Write(quote.law_part.copy().set_color(YELLOW)), + run_time=1, + ) + self.wait() + self.play( + Write(quote.language_part.copy().set_color(BLUE)), + run_time=1.5, + ) + self.wait(2) + + def get_quote(self): law_words = "laws of physics" language_words = "language of differential equations" author = "-Steven Strogatz" @@ -51,46 +85,58 @@ class StrogatzQuote(Scene): arg_separator=" ", substrings_to_isolate=[law_words, language_words, author] ) - law_part = quote.get_part_by_tex(law_words) - language_part = quote.get_part_by_tex(language_words) - author_part = quote.get_part_by_tex(author) + quote.law_part = quote.get_part_by_tex(law_words) + quote.language_part = quote.get_part_by_tex(language_words) + quote.author_part = quote.get_part_by_tex(author) quote.set_width(12) quote.to_edge(UP) quote[-2].shift(SMALL_BUFF * LEFT) - author_part.shift(RIGHT + 0.5 * DOWN) - author_part.scale(1.2, about_edge=UL) + quote.author_part.shift(RIGHT + 0.5 * DOWN) + quote.author_part.scale(1.2, about_edge=UL) + + return quote + + +class ShowSineValues(Scene): + def construct(self): + angle_tracker = ValueTracker(60 * DEGREES) + get_angle = angle_tracker.get_value + formula = always_redraw( + lambda: self.get_sine_formula(get_angle()) + ) + self.add(formula) - movers = VGroup(*quote[:-1].family_members_with_points()) - for mover in movers: - mover.save_state() - disc = Circle(radius=0.05) - disc.set_stroke(width=0) - disc.set_fill(BLACK, 0) - disc.move_to(mover) - mover.become(disc) self.play( - FadeInFrom(author_part, LEFT), - LaggedStartMap( - # FadeInFromLarge, - # quote[:-1].family_members_with_points(), - Restore, movers, - lag_ratio=0.005, - run_time=2, - ) - # FadeInFromDown(quote[:-1]), - # lag_ratio=0.01, + angle_tracker.set_value, 0, + run_time=3, ) self.wait() self.play( - Write(law_part.copy().set_color(YELLOW)), - run_time=1, + angle_tracker.set_value, 90 * DEGREES, + run_time=3, ) self.wait() - self.play( - Write(language_part.copy().set_color(BLUE)), - run_time=1.5, + + def get_sine_formula(self, angle): + sin, lp, rp = TexMobject( + "\\sin", "(", ") = " ) - self.wait(2) + input_part = Integer( + angle / DEGREES, + unit="^\\circ", + ) + input_part.set_color(YELLOW) + output_part = DecimalNumber( + np.sin(input_part.get_value() * DEGREES), + num_decimal_places=3, + ) + result = VGroup( + sin, lp, input_part, rp, output_part + ) + result.arrange(RIGHT, buff=SMALL_BUFF) + sin.scale(1.1, about_edge=DOWN) + lp.align_to(rp, UP) + return result class SetAsideSeekingSolution(Scene): @@ -141,6 +187,14 @@ class SetAsideSeekingSolution(Scene): ) +class ThreeBodyTitle(Scene): + def construct(self): + title = TextMobject("Three body problem") + title.scale(1.5) + title.to_edge(UP) + self.add(title) + + class ThreeBodySymbols(Scene): def construct(self): self.init_coord_groups() @@ -260,3 +314,259 @@ class ThreeBodySymbols(Scene): rate_func=linear, ) self.play(FadeOut(coord_copies)) + + +class ThreeBodyEquation(Scene): + def construct(self): + x1 = "\\vec{\\textbf{x}}_1" + x2 = "\\vec{\\textbf{x}}_2" + x3 = "\\vec{\\textbf{x}}_3" + kw = { + "tex_to_color_map": { + x1: RED, + x2: GREEN, + x3: BLUE, + } + } + equations = VGroup(*[ + TexMobject( + "{d^2", t1, "\\over dt^2}", "=", + "G", "\\left(" + "{" + m2, "(", t2, "-", t1, ")" + "\\over" + "||", t2, "-", t1, "||^3}", + "+", + "{" + m3, "(", t3, "-", t1, ")" + "\\over" + "||", t3, "-", t1, "||^3}", + "\\right)", + **kw + ) + for t1, t2, t3, m1, m2, m3 in [ + (x1, x2, x3, "m_1", "m_2", "m_3"), + (x2, x3, x1, "m_2", "m_3", "m_1"), + (x3, x1, x2, "m_3", "m_1", "m_2"), + ] + ]) + equations.arrange(DOWN, buff=LARGE_BUFF) + + self.play(LaggedStartMap( + FadeInFrom, equations, + lambda m: (m, UP), + lag_ratio=0.2, + )) + self.wait() + + +class JumpToThisPoint(Scene): + def construct(self): + dot = Dot(color=YELLOW) + dot.scale(0.5) + + arrow = Vector(DR, color=WHITE) + arrow.next_to(dot, UL, SMALL_BUFF) + words = TextMobject( + "Jump directly to\\\\", + "this point?", + ) + words.add_background_rectangle_to_submobjects() + words.next_to(arrow.get_start(), UP, SMALL_BUFF) + + self.play( + FadeInFromLarge(dot, 20), + rate_func=rush_into, + ) + self.play( + GrowArrow(arrow), + FadeInFromDown(words), + ) + + +class ChaosTitle(Scene): + def construct(self): + title = TextMobject("Chaos theorey") + title.scale(1.5) + title.to_edge(UP) + line = Line(LEFT, RIGHT) + line.set_width(FRAME_WIDTH - 1) + line.next_to(title, DOWN) + + self.play( + Write(title), + ShowCreation(line), + ) + self.wait() + + +class RevisitQuote(StrogatzQuote, PiCreatureScene): + def construct(self): + quote = self.get_quote() + quote.law_part.set_color(YELLOW) + quote.language_part.set_color(BLUE) + quote.set_stroke(BLACK, 6, background=True) + quote.scale(0.8, about_edge=UL) + + new_langauge_part = TextMobject( + "\\Large Language of differential equations" + ) + new_langauge_part.to_edge(UP) + new_langauge_part.match_style(quote.language_part) + + randy = self.pi_creature + + self.play( + FadeInFrom(quote[:-1], DOWN), + FadeInFrom(quote[-1:], LEFT), + randy.change, "raise_right_hand", + ) + self.play(Blink(randy)) + self.play(randy.change, "angry") + self.play( + Blink(randy), + VFadeOut(randy, run_time=3) + ) + mover = VGroup(quote.language_part) + self.add(quote, mover) + self.play( + ReplacementTransform( + mover, new_langauge_part, + ), + *[ + FadeOut(part) + for part in quote + if part is not quote.language_part + ], + run_time=2, + ) + self.wait() + + +class EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Juan Benet", + "Vassili Philippov", + "Burt Humburg", + "Carlos Vergara Del Rio", + "Matt Russell", + "Scott Gray", + "soekul", + "Tihan Seale", + "Ali Yahya", + "dave nicponski", + "Evan Phillips", + "Graham", + "Joseph Kelly", + "Kaustuv DeBiswas", + "LambdaLabs", + "Lukas Biewald", + "Mike Coleman", + "Peter Mcinerney", + "Quantopian", + "Roy Larson", + "Scott Walter, Ph.D.", + "Yana Chernobilsky", + "Yu Jun", + "Jordan Scales", + "Lukas -krtek.net- Novy", + "John Shaughnessy", + "Britt Selvitelle", + "David Gow", + "J", + "Jonathan Wilson", + "Joseph John Cox", + "Magnus Dahlström", + "Randy C. Will", + "Ryan Atallah", + "Luc Ritchie", + "1stViewMaths", + "Adrian Robinson", + "Alexis Olson", + "Andreas Benjamin Brössel", + "Andrew Busey", + "Ankalagon", + "Antonio Juarez", + "Arjun Chakroborty", + "Art Ianuzzi", + "Awoo", + "Bernd Sing", + "Boris Veselinovich", + "Brian Staroselsky", + "Chad Hurst", + "Charles Southerland", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Cooper Jones", + "Danger Dai", + "Dave B", + "Dave Kester", + "David Clark", + "DeathByShrimp", + "Delton Ding", + "eaglle", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Giovanni Filippi", + "Hal Hildebrand", + "Herman Dieset", + "Hitoshi Yamauchi", + "Isaac Jeffrey Lee", + "j eduardo perez", + "Jacob Magnuson", + "Jameel Syed", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Kai-Siang Ang", + "Kanan Gill", + "L0j1k", + "Lee Redden", + "Linh Tran", + "Ludwig Schubert", + "Magister Mugit", + "Mark B Bahu", + "Mark Heising", + "Martin Price", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Cocke", + "Michael Faust", + "Michael Hardel", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nikita Lesnikov", + "Omar Zrien", + "Owen Campbell-Moore", + "Peter Ehrnstrom", + "RedAgent14", + "rehmi post", + "Richard Burgmann", + "Richard Comish", + "Ripta Pasay", + "Rish Kundalia", + "Robert Teed", + "Roobie", + "Ryan Williams", + "Samuel D. Judge", + "Solara570", + "Stevie Metke", + "Tal Einav", + "Ted Suzman", + "Valeriy Skobelev", + "Xavier Bernard", + "Yavor Ivanov", + "Yaw Etse", + "YinYangBalance.Asia", + "Zach Cardwell", + ] + } diff --git a/active_projects/ode/solve_pendulum_ode_sample_code.py b/active_projects/ode/solve_pendulum_ode_sample_code.py new file mode 100644 index 00000000..e7076c3c --- /dev/null +++ b/active_projects/ode/solve_pendulum_ode_sample_code.py @@ -0,0 +1,63 @@ +import numpy as np + +# Physical constants +g = 9.8 +L = 2 +mu = 0.1 + +THETA_0 = np.pi / 3 # 60 degrees +THETA_DOT_0 = 0 # No initial angular velocity + +# Definition of ODE +def get_theta_double_dot(theta, theta_dot): + return -mu * theta_dot - (g / L) * np.sin(theta) + + +# Solution to the differential equation +def theta(t): + # Initialize changing values + theta = THETA_0 + theta_dot = THETA_DOT_0 + delta_t = 0.01 # Some time step + for time in np.arange(0, t, delta_t): + # Take many little time steps of size delta_t + # until the total time is the function's input + theta_double_dot = get_theta_double_dot( + theta, theta_dot + ) + theta += theta_dot * delta_t + theta_dot += theta_double_dot * delta_t + return theta + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stage_scenes.py b/stage_scenes.py index f993081e..ab84fe49 100644 --- a/stage_scenes.py +++ b/stage_scenes.py @@ -43,7 +43,7 @@ def stage_scenes(module_name): # } # TODO, fix this animation_dir = os.path.join( - VIDEO_DIR, "clacks_solution2", "1440p60" + VIDEO_DIR, "ode", "part1", "1440p60" ) # files = os.listdir(animation_dir) From e8fe67650aabe0864868eb6432072e5b94b43899 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 29 Mar 2019 15:12:31 -0700 Subject: [PATCH 05/18] Fixed problem with LaggedStartMap --- manimlib/animation/composition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 03704df6..043765bf 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -152,7 +152,8 @@ class LaggedStartMap(LaggedStart): else: args_list.append((submob,)) anim_kwargs = dict(kwargs) - anim_kwargs.pop("lag_ratio") + if "lag_ratio" in anim_kwargs: + anim_kwargs.pop("lag_ratio") animations = [ AnimationClass(*args, **anim_kwargs) for args in args_list From 9d566de8d1f2a436f7608403498d52ab1cda00e3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 29 Mar 2019 15:12:38 -0700 Subject: [PATCH 06/18] Pile of ODE scenes --- active_projects/ode/all_part1_scenes.py | 19 + active_projects/ode/part1/pendulum.py | 488 ++++++++++-------- active_projects/ode/part1/phase_space.py | 438 ++++++++++++++-- active_projects/ode/part1/pi_scenes.py | 53 +- .../ode/part1/shared_constructs.py | 6 +- active_projects/ode/part1/staging.py | 195 +++++-- active_projects/ode/part1/wordy_scenes.py | 283 ++++++++++ 7 files changed, 1178 insertions(+), 304 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 83e220c9..a8a13ebd 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -40,6 +40,18 @@ ALL_SCENE_CLASSES = [ ODEvsPDEinFrames, ProveTeacherWrong, SetAsideSeekingSolution, + # + WriteInRadians, + XEqLThetaToCorner, + ComingUp, + InputLabel, + SoWhatIsThetaThen, + ReallyHardToSolve, + ReasonForSolution, + GleickQuote, + SpectrumOfStartingStates, + WritePhaseFlow, + AskAboutStability, # VisualizeHeightSlopeCurvature, VisualizeStates, ReferencePiCollisionStateSpaces, @@ -54,11 +66,18 @@ ALL_SCENE_CLASSES = [ LorenzVectorField, ThreeBodiesInSpace, AltThreeBodiesInSpace, + TwoBodiesInSpace, + TwoBodiesWithZPart, ThreeBodyTitle, ThreeBodySymbols, + # + HighAmplitudePendulum, + WritePhaseSpace, + # AskAboutActuallySolving, WriteODESolvingCode, TakeManyTinySteps, + ManyStepsFromDifferentStartingPoints, InaccurateComputation, HungerForExactness, ShowRect, diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 4d8c28b7..08aa6ce2 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -514,6 +514,7 @@ class IntroducePendulum(PiCreatureScene, MovingCameraScene): Write(formula), hm_word.to_corner, UR ) + self.wait(4) def show_graph_period(self): pendulum = self.pendulum @@ -1002,19 +1003,20 @@ class AnalyzePendulumForce(MovingCameraScene): def construct(self): self.add_pendulum() + self.show_arc_length() self.add_g_vect() self.show_constraint() self.break_g_vect_into_components() - self.show_gsin_formula() - self.show_acceleration_at_different_angles() self.show_angle_geometry() - self.ask_about_what_to_do() + self.show_gsin_formula() + self.show_sign() + self.show_acceleration_formula() + # self.ask_about_what_to_do() - self.emphasize_theta() - self.show_arc_length() - self.show_angular_velocity() - self.show_angular_acceleration() - self.circle_g_sin_formula() + # self.emphasize_theta() + # self.show_angular_velocity() + # self.show_angular_acceleration() + # self.circle_g_sin_formula() def add_pendulum(self): pendulum = Pendulum(**self.pendulum_config) @@ -1027,6 +1029,97 @@ class AnalyzePendulumForce(MovingCameraScene): self.pendulum = pendulum self.theta_tracker = theta_tracker + def show_arc_length(self): + pendulum = self.pendulum + angle = pendulum.get_theta() + height = pendulum.length + top = pendulum.get_fixed_point() + + line = Line(UP, DOWN) + line.set_height(height) + line.move_to(top, UP) + arc = always_redraw(lambda: Arc( + start_angle=-90 * DEGREES, + angle=pendulum.get_theta(), + arc_center=pendulum.get_fixed_point(), + radius=pendulum.length, + stroke_color=GREEN, + )) + + brace = Brace(Line(ORIGIN, 5 * UP), RIGHT) + brace.point = VectorizedPoint(brace.get_right()) + brace.add(brace.point) + brace.set_height(angle) + brace.move_to(ORIGIN, DL) + brace.apply_complex_function(np.exp) + brace.scale(height) + brace.rotate(-90 * DEGREES) + brace.move_to(arc) + brace.shift(MED_SMALL_BUFF * normalize( + arc.point_from_proportion(0.5) - top + )) + x_sym = TexMobject("x") + x_sym.set_color(GREEN) + x_sym.next_to(brace.point, DR, buff=SMALL_BUFF) + + rhs = TexMobject("=", "L", "\\theta") + rhs.set_color_by_tex("\\theta", BLUE) + rhs.next_to(x_sym, RIGHT) + rhs.shift(0.7 * SMALL_BUFF * UP) + line_L = TexMobject("L") + line_L.next_to( + pendulum.rod.get_center(), UR, SMALL_BUFF, + ) + + self.play( + ShowCreation(arc), + Rotate(line, angle, about_point=top), + UpdateFromAlphaFunc( + line, lambda m, a: m.set_stroke( + width=2 * there_and_back(a) + ) + ), + GrowFromPoint( + brace, line.get_bottom(), + path_arc=angle + ), + ) + self.play(FadeInFrom(x_sym, UP)) + self.wait() + + # Show equation + line.set_stroke(BLUE, 5) + self.play( + ShowCreationThenFadeOut(line), + FadeInFromDown(line_L) + ) + self.play( + TransformFromCopy( + line_L, rhs.get_part_by_tex("L") + ), + Write(rhs.get_part_by_tex("=")) + ) + self.play( + TransformFromCopy( + pendulum.theta_label, + rhs.get_parts_by_tex("\\theta"), + ) + ) + self.add(rhs) + + x_eq = VGroup(x_sym, rhs) + + self.play( + FadeOut(brace), + x_eq.rotate, angle / 2, + x_eq.next_to, arc.point_from_proportion(0.5), + UL, {"buff": -MED_SMALL_BUFF} + ) + + self.x_eq = x_eq + self.arc = arc + self.line_L = line_L + def add_g_vect(self): pendulum = self.pendulum @@ -1044,15 +1137,10 @@ class AnalyzePendulumForce(MovingCameraScene): GrowArrow(g_vect), FadeInFrom(g_word, UP, lag_ratio=0.1), ) + self.wait() def show_constraint(self): pendulum = self.pendulum - weight = pendulum.weight - p = weight.get_center() - path = CubicBezier([p, p + 3 * DOWN, p + 3 * UP, p]) - - self.play(MoveAlongPath(weight, path, run_time=2)) - self.wait() arcs = VGroup() for u in [-1, 2, -1]: @@ -1063,7 +1151,7 @@ class AnalyzePendulumForce(MovingCameraScene): radius=pendulum.length, arc_center=pendulum.get_fixed_point(), stroke_width=2, - stroke_color=GREEN, + stroke_color=YELLOW, stroke_opacity=0.5, ) self.play( @@ -1071,9 +1159,7 @@ class AnalyzePendulumForce(MovingCameraScene): ShowCreation(arc) ) arcs.add(arc) - self.wait() - - self.traced_arcs = arcs + self.play(FadeOut(arcs)) def break_g_vect_into_components(self): g_vect = self.g_vect @@ -1100,6 +1186,52 @@ class AnalyzePendulumForce(MovingCameraScene): self.play(GrowArrow(g_vect.perp)) self.wait() + def show_angle_geometry(self): + g_vect = self.g_vect + + arc = Arc( + start_angle=90 * DEGREES, + angle=self.pendulum.get_theta(), + radius=0.5, + arc_center=g_vect.get_end(), + ) + q_mark = TexMobject("?") + q_mark.next_to(arc.get_center(), UL, SMALL_BUFF) + theta_label = TexMobject("\\theta") + theta_label.move_to(q_mark) + + self.add(g_vect) + self.play( + ShowCreation(arc), + Write(q_mark) + ) + self.play(ShowCreationThenFadeAround(q_mark)) + self.wait() + self.play(ShowCreationThenFadeAround( + self.pendulum.theta_label + )) + self.play( + TransformFromCopy( + self.pendulum.theta_label, + theta_label, + ), + FadeOut(q_mark) + ) + self.wait() + self.play(WiggleOutThenIn(g_vect.tangent)) + self.play(WiggleOutThenIn( + Line( + *g_vect.get_start_and_end(), + buff=0, + ).add_tip().match_style(g_vect), + remover=True + )) + self.wait() + self.play( + FadeOut(arc), + FadeOut(theta_label), + ) + def show_gsin_formula(self): g_vect = self.g_vect g_word = self.g_word @@ -1154,27 +1286,7 @@ class AnalyzePendulumForce(MovingCameraScene): self.g_sin_label = g_sin_label self.g_cos_label = g_cos_label - def show_acceleration_at_different_angles(self): - to_fade = VGroup( - self.g_cos_label, - self.g_vect.perp, - ) - new_comp_line_sytle = { - "stroke_width": 0.5, - "stroke_opacity": 0.25, - } - - self.play( - to_fade.set_opacity, 0.25, - self.g_vect.component_lines.set_style, - new_comp_line_sytle - ) - self.g_vect.component_lines.add_updater( - lambda m: m.set_style(**new_comp_line_sytle) - ) - for mob in to_fade: - mob.add_updater(lambda m: m.set_opacity(0.25)) - + def show_sign(self): get_theta = self.pendulum.get_theta theta_decimal = DecimalNumber(include_sign=True) theta_decimal.add_updater(lambda d: d.set_value( @@ -1187,84 +1299,148 @@ class AnalyzePendulumForce(MovingCameraScene): GREEN if get_theta() > 0 else RED )) + self.play( + FadeInFrom(theta_decimal, UP), + FadeOut(self.x_eq), + FadeOut(self.line_L), + ) + self.set_theta(-60 * DEGREES, run_time=4) + self.set_theta(60 * DEGREES, run_time=4) + self.play( + FadeOut(theta_decimal), + FadeIn(self.x_eq), + ) + + def show_acceleration_formula(self): + x_eq = self.x_eq + g_sin_theta = self.g_sin_label + + equation = TexMobject( + "a", "=", + "\\ddot", "x", + "=", + "-", "g", "\\sin\\big(", "\\theta", "\\big)", + ) + equation.to_edge(LEFT) + + second_deriv = equation[2:4] + x_part = equation.get_part_by_tex("x") + x_part.set_color(GREEN) + a_eq = equation[:2] + eq2 = equation.get_parts_by_tex("=")[1] + rhs = equation[5:] + + second_deriv_L_form = TexMobject( + "L", "\\ddot", "\\theta" + ) + second_deriv_L_form.move_to(second_deriv, DOWN) + eq3 = TexMobject("=") + eq3.rotate(90 * DEGREES) + eq3.next_to(second_deriv_L_form, UP) + + g_L_frac = TexMobject( + "-", "{g", "\\over", "L}" + ) + g_L_frac.move_to(rhs[:2], LEFT) + g_L_frac.shift(SMALL_BUFF * UP / 2) + + mu_term = TexMobject( + "-\\mu", "\\dot", "\\theta", + ) + mu_term.next_to(g_L_frac, LEFT) + mu_term.shift(SMALL_BUFF * UP / 2) + + mu_brace = Brace(mu_term, UP) + mu_word = mu_brace.get_text("Air resistance") + + for mob in equation, second_deriv_L_form, mu_term: + mob.set_color_by_tex("\\theta", BLUE) + + self.play( + TransformFromCopy(x_eq[0], x_part), + Write(equation[:3]), + ) + self.wait() + self.play( + Write(eq2), + TransformFromCopy(g_sin_theta, rhs) + ) + self.wait() + # + self.show_acceleration_at_different_angles() + # + self.play( + FadeInFromDown(second_deriv_L_form), + Write(eq3), + second_deriv.next_to, eq3, UP, + a_eq.shift, SMALL_BUFF * LEFT, + eq2.shift, SMALL_BUFF * RIGHT, + rhs.shift, SMALL_BUFF * RIGHT, + ) + self.wait() + self.wait() + self.play( + FadeOut(a_eq), + FadeOut(second_deriv), + FadeOut(eq3), + ReplacementTransform( + second_deriv_L_form.get_part_by_tex("L"), + g_L_frac.get_part_by_tex("L"), + ), + ReplacementTransform( + equation.get_part_by_tex("-"), + g_L_frac.get_part_by_tex("-"), + ), + ReplacementTransform( + equation.get_part_by_tex("g"), + g_L_frac.get_part_by_tex("g"), + ), + Write(g_L_frac.get_part_by_tex("\\over")), + rhs[2:].next_to, g_L_frac, RIGHT, {"buff": SMALL_BUFF}, + ) + self.wait() + self.play( + GrowFromCenter(mu_term), + VGroup(eq2, second_deriv_L_form[1:]).next_to, + mu_term, LEFT, + ) + self.play( + GrowFromCenter(mu_brace), + FadeInFromDown(mu_word), + ) + + def show_acceleration_at_different_angles(self): + to_fade = VGroup( + self.g_cos_label, + self.g_vect.perp, + ) + new_comp_line_sytle = { + "stroke_width": 0.5, + "stroke_opacity": 0.25, + } + + self.play( + FadeOut(self.x_eq), + to_fade.set_opacity, 0.25, + self.g_vect.component_lines.set_style, + new_comp_line_sytle + ) + self.g_vect.component_lines.add_updater( + lambda m: m.set_style(**new_comp_line_sytle) + ) + for mob in to_fade: + mob.add_updater(lambda m: m.set_opacity(0.25)) + self.set_theta(0) self.wait(2) self.set_theta(89.9 * DEGREES, run_time=3) self.wait(2) - self.set_theta(60 * DEGREES, run_time=2) - self.wait() - - self.play(FadeInFrom(theta_decimal, UP)) - self.set_theta(-60 * DEGREES, run_time=4) - self.set_theta(60 * DEGREES, run_time=4) - self.play(FadeOut(theta_decimal)) - - def show_angle_geometry(self): - g_vect = self.g_vect - vectors = VGroup( - g_vect, g_vect.tangent, g_vect.perp, - ) - g_line = Line( - g_vect.get_start(), - g_vect.get_end(), - stroke_width=1, - stroke_color=WHITE, - ) - - arc = Arc( - start_angle=90 * DEGREES, - angle=self.pendulum.get_theta(), - radius=0.5, - arc_center=g_vect.get_end(), - ) - q_mark = TexMobject("?") - q_mark.next_to(arc.get_center(), UL, SMALL_BUFF) - theta_label = TexMobject("\\theta") - theta_label.move_to(q_mark) - - opposite = Line( - *g_vect.component_lines[0].get_start_and_end() - ) - adjascent = g_line.copy() - opposite.set_stroke(BLUE, 5, opacity=1) - adjascent.set_stroke(YELLOW, 5) - - vectors.save_state() - vectors.suspend_updating() - self.add(g_line, g_vect) - self.play(vectors.set_opacity, 0.3) - self.play( - ShowCreation(arc), - Write(q_mark) - ) - self.play(ShowCreationThenFadeAround(q_mark)) - self.wait() - self.play(ShowCreationThenFadeAround( - self.pendulum.theta_label - )) - self.play( - TransformFromCopy( - self.pendulum.theta_label, - theta_label, - ), - FadeOut(q_mark) + self.set_theta( + 60 * DEGREES, + FadeIn(self.x_eq), + run_time=2, ) self.wait() - self.play(ShowCreation(opposite)) - self.play(ShowCreation(adjascent)) - self.wait() - self.play( - FadeOut(opposite), - FadeOut(adjascent), - ) - self.play( - Restore(vectors), - FadeOut(g_line), - FadeOut(arc), - FadeOut(theta_label), - ) - - vectors.resume_updating() def ask_about_what_to_do(self): g_vect = self.g_vect @@ -1349,96 +1525,6 @@ class AnalyzePendulumForce(MovingCameraScene): pendulum.remove_updater(new_updater) self.update_mobjects(0) - def show_arc_length(self): - pendulum = self.pendulum - to_fade = VGroup( - self.g_vect, - self.g_term, - self.g_vect.component_lines, - self.g_vect.tangent, - self.g_vect.perp, - self.g_sin_label, - self.g_cos_label, - ) - angle = pendulum.get_theta() - height = pendulum.length - top = pendulum.get_fixed_point() - - line = Line(UP, DOWN) - line.set_height(height) - line.move_to(top, UP) - arc = Arc( - start_angle=-90 * DEGREES, - angle=angle, - arc_center=top, - radius=height, - stroke_color=GREEN, - ) - - brace = Brace(Line(ORIGIN, 5 * UP), RIGHT) - brace.point = VectorizedPoint(brace.get_right()) - brace.add(brace.point) - brace.set_height(angle) - brace.move_to(ORIGIN, DL) - brace.apply_complex_function(np.exp) - brace.scale(height) - brace.rotate(-90 * DEGREES) - brace.move_to(arc) - brace.shift(MED_SMALL_BUFF * normalize( - arc.point_from_proportion(0.5) - top - )) - x_sym = TexMobject("x") - x_sym.set_color(GREEN) - x_sym.next_to(brace.point, DR, buff=SMALL_BUFF) - - rhs = TexMobject("=", "L", "\\theta") - rhs.set_color_by_tex("\\theta", BLUE) - rhs.next_to(x_sym, RIGHT) - rhs.shift(0.7 * SMALL_BUFF * UP) - line_L = TexMobject("L") - line_L.next_to( - pendulum.rod.get_center(), UR, SMALL_BUFF, - ) - - if hasattr(self, "traced_arcs"): - self.play(FadeOut(self.traced_arcs)) - self.play(FadeOut(to_fade)) - self.play( - ShowCreation(arc), - Rotate(line, angle, about_point=top), - UpdateFromAlphaFunc( - line, lambda m, a: m.set_stroke( - width=2 * there_and_back(a) - ) - ), - GrowFromPoint( - brace, line.get_bottom(), - path_arc=angle - ), - ) - self.play(FadeInFrom(x_sym, UP)) - self.wait() - - # Show equation - line.set_stroke(BLUE, 5) - self.play( - ShowCreationThenFadeOut(line), - FadeInFromDown(line_L) - ) - self.play( - TransformFromCopy( - line_L, rhs.get_part_by_tex("L") - ), - Write(rhs.get_part_by_tex("=")) - ) - self.play( - TransformFromCopy( - pendulum.theta_label, - rhs.get_parts_by_tex("\\theta"), - ) - ) - self.add(rhs) - def show_angular_velocity(self): pass diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 63992f6c..40d69d30 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -940,19 +940,26 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): "big_pendulum_config": { "max_velocity_vector_length_to_length_ratio": 1, }, + "run_time": 25, + "initial_theta": 0, + "initial_theta_dot": 4, + "frame_shift_vect": TAU * RIGHT, } def setup(self): MovingCameraScene.setup(self) def construct(self): + self.initialize_plane_and_field() + self.add_flexible_state() + self.show_high_vector() + self.show_trajectory() + + def initialize_plane_and_field(self): self.initialize_plane() self.add(self.plane) self.initialize_vector_field() self.add(self.vector_field) - self.add_flexible_state() - self.show_high_vector() - self.show_trajectory() def add_flexible_state(self): super().add_flexible_state() @@ -960,7 +967,10 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): plane = self.plane state.to_edge(DOWN, buff=SMALL_BUFF), - start_point = plane.coords_to_point(0, 4) + start_point = plane.coords_to_point( + self.initial_theta, + self.initial_theta_dot, + ) dot = self.get_state_controlling_dot(state) dot.move_to(start_point) state.update() @@ -987,24 +997,14 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): def show_trajectory(self): state = self.state - field = self.vector_field frame = self.camera_frame dot = self.dot start_point = self.start_point - traj = VMobject() - traj.start_new_path(start_point) - dt = 0.01 - total_time = 25 - for x in range(int(total_time / dt)): - end = traj.points[-1] - dp_dt = field.func(end) - traj.add_smooth_curve_to(end + dp_dt * dt) - - traj.set_stroke(WHITE, 2) + traj = self.get_trajectory(start_point, self.run_time) self.add(traj, dot) - self.play( + anims = [ ShowCreation( traj, rate_func=linear, @@ -1012,15 +1012,32 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): UpdateFromFunc( dot, lambda d: d.move_to(traj.points[-1]) ), - ApplyMethod( - frame.shift, TAU * RIGHT, - rate_func=squish_rate_func( - smooth, 0, 0.3, - ) - ), - MaintainPositionRelativeTo(state.rect, frame), - run_time=total_time, - ) + ] + if get_norm(self.frame_shift_vect) > 0: + anims += [ + ApplyMethod( + frame.shift, self.frame_shift_vect, + rate_func=squish_rate_func( + smooth, 0, 0.3, + ) + ), + MaintainPositionRelativeTo(state.rect, frame), + ] + self.play(*anims, run_time=total_time) + + def get_trajectory(self, start_point, time, dt=0.1, added_steps=1000): + field = self.vector_field + traj = VMobject() + traj.start_new_path(start_point) + for x in range(int(time / dt)): + last_point = traj.points[-1] + for y in range(added_steps): + dp_dt = field.func(last_point) + last_point += dp_dt * dt / added_steps + traj.add_smooth_curve_to(last_point) + traj.make_smooth() + traj.set_stroke(WHITE, 2) + return traj class TweakMuInFormula(Scene): @@ -1146,6 +1163,216 @@ class TweakMuInVectorField(ShowPendulumPhaseFlow): self.wait(self.flow_time) +class HighAmplitudePendulum(ShowHighVelocityCase): + CONFIG = { + "big_pendulum_config": { + "damping": 0.02, + }, + "initial_theta": 175 * DEGREES, + "initial_theta_dot": 0, + "frame_shift_vect": 0 * RIGHT, + } + + def construct(self): + self.initialize_plane_and_field() + self.add_flexible_state() + self.show_trajectory() + + +class SpectrumOfStartingStates(ShowHighVelocityCase): + CONFIG = { + "run_time": 15, + } + + def construct(self): + self.initialize_plane_and_field() + self.vector_field.set_opacity(0.5) + self.show_many_trajectories() + + def show_many_trajectories(self): + plane = self.plane + + delta_x = 0.5 + delta_y = 0.5 + n = 20 + + start_points = [ + plane.coords_to_point(x, y) + for x in np.linspace(PI - delta_x, PI + delta_x, n) + for y in np.linspace(-delta_y, delta_y, n) + ] + start_points.sort( + key=lambda p: np.dot(p, UL) + ) + time = self.run_time + + # Count points + dots = VGroup(*[ + Dot(sp, radius=0.025) + for sp in start_points + ]) + dots.set_color_by_gradient(PINK, BLUE, YELLOW) + words = TextMobject( + "Spectrum of\\\\", "initial conditions" + ) + words.set_stroke(BLACK, 5, background=True) + words.next_to(dots, UP) + + self.play( + # ShowIncreasingSubsets(dots, run_time=2), + LaggedStartMap( + FadeInFromLarge, dots, + lambda m: (m, 10), + run_time=2 + ), + FadeInFromDown(words), + ) + self.wait() + + trajs = VGroup() + for sp in start_points: + trajs.add( + self.get_trajectory( + sp, time, + added_steps=10, + ) + ) + for traj, dot in zip(trajs, dots): + traj.set_stroke(dot.get_color(), 1) + + def update_dots(ds): + for d, t in zip(ds, trajs): + d.move_to(t.points[-1]) + return ds + dots.add_updater(update_dots) + + self.add(dots, trajs, words) + self.play( + ShowCreation( + trajs, + lag_ratio=0, + ), + rate_func=linear, + run_time=time, + ) + self.wait() + + +class AskAboutStability(ShowHighVelocityCase): + CONFIG = { + "initial_theta": 60 * DEGREES, + "initial_theta_dot": 1, + } + + def construct(self): + self.initialize_plane_and_field() + self.add_flexible_state() + self.show_fixed_points() + self.label_fixed_points() + self.ask_about_stability() + self.show_nudges() + + def show_fixed_points(self): + state1 = self.state + plane = self.plane + dot1 = self.dot + + state2 = self.get_flexible_state_picture() + state2.to_corner(DR, buff=SMALL_BUFF) + dot2 = self.get_state_controlling_dot(state2) + dot2.set_color(BLUE) + + fp1 = plane.coords_to_point(0, 0) + fp2 = plane.coords_to_point(PI, 0) + + self.play( + dot1.move_to, fp1, + run_time=3, + ) + self.wait() + self.play(FadeIn(state2)) + self.play( + dot2.move_to, fp2, + path_arc=-30 * DEGREES, + run_time=2, + ) + self.wait() + + self.state1 = state1 + self.state2 = state2 + self.dot1 = dot1 + self.dot2 = dot2 + + def label_fixed_points(self): + dots = VGroup(self.dot1, self.dot2) + + label = TextMobject("Fixed points") + label.scale(1.5) + label.set_stroke(BLACK, 5, background=True) + label.next_to(dots, UP, buff=2) + label.shift(SMALL_BUFF * DOWN) + + arrows = VGroup(*[ + Arrow( + label.get_bottom(), dot.get_center(), + color=dot.get_color(), + ) + for dot in dots + ]) + + self.play( + self.vector_field.set_opacity, 0.5, + FadeInFromDown(label) + ) + self.play(ShowCreation(arrows)) + self.wait(2) + + self.to_fade = VGroup(label, arrows) + + def ask_about_stability(self): + question = TextMobject("Stable?") + question.scale(2) + question.shift(FRAME_WIDTH * RIGHT / 4) + question.to_edge(UP) + question.set_stroke(BLACK, 5, background=True) + + self.play(Write(question)) + self.play(FadeOut(self.to_fade)) + + def show_nudges(self): + dots = VGroup(self.dot1, self.dot2) + time = 20 + + self.play(*[ + ApplyMethod( + dot.shift, 0.1 * UL, + rate_func=rush_from, + ) + for dot in dots + ]) + + trajs = VGroup() + for dot in dots: + traj = self.get_trajectory( + dot.get_center(), + time, + ) + traj.set_stroke(dot.get_color(), 2) + trajs.add(traj) + + def update_dots(ds): + for t, d in zip(trajs, ds): + d.move_to(t.points[-1]) + dots.add_updater(update_dots) + self.add(trajs, dots) + self.play( + ShowCreation(trajs, lag_ratio=0), + rate_func=linear, + run_time=time + ) + self.wait() + + class TakeManyTinySteps(IntroduceVectorField): CONFIG = { "initial_theta": 60 * DEGREES, @@ -1153,14 +1380,16 @@ class TakeManyTinySteps(IntroduceVectorField): } def construct(self): + self.initialize_plane_and_field() + self.take_many_time_steps() + + def initialize_plane_and_field(self): self.initialize_plane() self.initialize_vector_field() field = self.vector_field field.set_opacity(0.35) self.add(self.plane, field) - self.take_many_time_steps() - def take_many_time_steps(self): delta_t_tracker = ValueTracker(0.5) get_delta_t = delta_t_tracker.get_value @@ -1170,12 +1399,18 @@ class TakeManyTinySteps(IntroduceVectorField): traj = always_redraw( lambda: self.get_time_step_trajectory( - get_delta_t(), get_t() + get_delta_t(), + get_t(), + self.initial_theta, + self.initial_theta_dot, ) ) vectors = always_redraw( lambda: self.get_path_vectors( - get_delta_t(), get_t() + get_delta_t(), + get_t(), + self.initial_theta, + self.initial_theta_dot, ) ) @@ -1214,28 +1449,99 @@ class TakeManyTinySteps(IntroduceVectorField): ) ) + theta_t_label = TexMobject("\\theta(t)...\\text{ish}") + theta_t_label.scale(0.75) + theta_t_label.add_updater(lambda m: m.next_to( + vectors[-1].get_end(), + vectors[-1].get_vector(), + SMALL_BUFF, + )) + self.add(traj, vectors, init_labels, labels) time_tracker.set_value(0) + target_time = 10 self.play( - time_tracker.set_value, 10, - run_time=5, - rate_func=linear, + VFadeIn(theta_t_label), + ApplyMethod( + time_tracker.set_value, target_time, + run_time=5, + rate_func=linear, + ) ) self.wait() t_label[-1].clear_updaters() + self.remove(theta_t_label) + target_delta_t = 0.01 self.play( - delta_t_tracker.set_value, 0.01, + delta_t_tracker.set_value, target_delta_t, run_time=7, ) self.wait() + traj.clear_updaters() + vectors.clear_updaters() + + # Show steps + count_tracker = ValueTracker(0) + count = Integer() + count.scale(1.5) + count.to_edge(LEFT) + count.shift(UP) + count.add_updater(lambda c: c.set_value( + count_tracker.get_value() + )) + count_label = TextMobject("steps") + count_label.scale(1.5) + count_label.add_updater( + lambda m: m.next_to(count, RIGHT).shift(SMALL_BUFF * DOWN) + ) + + scaled_vectors = vectors.copy() + scaled_vectors.clear_updaters() + for vector in scaled_vectors: + vector.scale( + 1 / vector.get_length(), + about_point=vector.get_start() + ) + vector.set_color(YELLOW) + + def update_scaled_vectors(group): + group.set_opacity(0) + group[min( + int(count.get_value()), + len(group) - 1, + )].set_opacity(1) + + scaled_vectors.add_updater(update_scaled_vectors) + + self.add(count, count_label, scaled_vectors) + self.play( + # LaggedStartMap( + # ApplyFunction, vectors, + # lambda vector: ( + # lambda v: v.scale( + # 1 / v.get_length() + # ), + # vector + # ), + # rate_func=there_and_back, + # ), + ApplyMethod( + count_tracker.set_value, + int(target_time / target_delta_t), + rate_func=linear, + ), + run_time=5, + ) + self.play(FadeOut(scaled_vectors)) + self.wait() # - def get_time_step_points(self, delta_t, total_time): + def get_time_step_points(self, delta_t, total_time, theta_0, theta_dot_0): plane = self.plane field = self.vector_field curr_point = plane.coords_to_point( - self.initial_theta, - self.initial_theta_dot, + theta_0, + theta_dot_0, ) points = [curr_point] t = 0 @@ -1246,17 +1552,21 @@ class TakeManyTinySteps(IntroduceVectorField): t += delta_t return points - def get_time_step_trajectory(self, delta_t, total_time): + def get_time_step_trajectory(self, delta_t, total_time, theta_0, theta_dot_0): traj = VMobject() traj.set_points_as_corners( - self.get_time_step_points(delta_t, total_time) + self.get_time_step_points( + delta_t, total_time, + theta_0, theta_dot_0, + ) ) traj.set_stroke(WHITE, 2) return traj - def get_path_vectors(self, delta_t, total_time): + def get_path_vectors(self, delta_t, total_time, theta_0, theta_dot_0): corners = self.get_time_step_points( - delta_t, total_time + delta_t, total_time, + theta_0, theta_dot_0, ) result = VGroup() for a1, a2 in zip(corners, corners[1:]): @@ -1268,3 +1578,51 @@ class TakeManyTinySteps(IntroduceVectorField): ) result.add(vector) return result + + +class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps): + CONFIG = { + "initial_thetas": np.linspace(0.1, PI - 0.1, 10), + "initial_theta_dot": 0, + } + + def construct(self): + self.initialize_plane_and_field() + self.take_many_time_steps() + + def take_many_time_steps(self): + delta_t_tracker = ValueTracker(0.2) + get_delta_t = delta_t_tracker.get_value + + time_tracker = ValueTracker(10) + get_t = time_tracker.get_value + # traj = always_redraw( + # lambda: VGroup(*[ + # self.get_time_step_trajectory( + # get_delta_t(), + # get_t(), + # theta, + # self.initial_theta_dot, + # ) + # for theta in self.initial_thetas + # ]) + # ) + vectors = always_redraw( + lambda: VGroup(*[ + self.get_path_vectors( + get_delta_t(), + get_t(), + theta, + self.initial_theta_dot, + ) + for theta in self.initial_thetas + ]) + ) + + self.add(vectors) + time_tracker.set_value(0) + self.play( + time_tracker.set_value, 5, + run_time=5, + rate_func=linear, + ) diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index 4c753585..09066905 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -108,6 +108,29 @@ class FormulasAreLies(PiCreatureScene): # pass +class SoWhatIsThetaThen(TeacherStudentsScene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + self.add(ode) + + self.student_says( + "Okay, but then\\\\" + "what \\emph{is} $\\theta(t)$?" + ) + self.wait() + self.play(self.teacher.change, "happy") + self.wait(2) + self.teacher_says( + "First, you must appreciate\\\\" + "a deep truth...", + added_anims=[self.get_student_changes( + *3 * ["confused"] + )] + ) + self.wait(4) + + class ProveTeacherWrong(TeacherStudentsScene): def construct(self): tex_config = { @@ -247,9 +270,7 @@ class HungerForExactness(TeacherStudentsScene): ode.to_corner(UL) left_part = ode[:5] friction_part = ode[5:11] - right_part = ode[11:] self.add(ode) - frictionless_group = VGroup(left_part, right_part) proposed_solution = TexMobject( "\\theta_0\\cos((\\sqrt{g/L})t)e^{-\\mu t}" @@ -401,19 +422,37 @@ class HungerForExactness(TeacherStudentsScene): mystery.scale(2) mystery.to_edge(UP) mystery.set_stroke(width=0, background=True) + mystery_boundary = AnimatedBoundary( + mystery, stroke_width=1 + ) self.play( FadeInFromDown(mystery), self.teacher.change, "pondering" ) - self.add( - AnimatedBoundary(mystery, stroke_width=1), - mystery, - ) + self.add(mystery_boundary, mystery) self.change_all_student_modes("sad") self.look_at(mystery) self.wait(5) + # Define + self.student_says( + "Let $\\text{P}(\\mu, g, L; t)$ be a\\\\" + "function satisfying this ODE.", + student_index=0, + target_mode="speaking", + added_anims=[ + FadeOut(mystery), + FadeOut(mystery_boundary), + ode.to_corner, UR + ] + ) + self.change_student_modes( + "hooray", "sassy", "sassy", + look_at_arg=students[0].eyes.get_corner(UR), + ) + self.wait(2) + class ItGetsWorse(TeacherStudentsScene): def construct(self): @@ -421,4 +460,4 @@ class ItGetsWorse(TeacherStudentsScene): self.change_student_modes( "hesitant", "pleading", "erm" ) - self.wait(2) + self.wait(5) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index e803f694..0bed3314 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -20,8 +20,8 @@ def get_ode(): tex_config = { "tex_to_color_map": { "{\\theta}": BLUE, - "{\\dot\\theta}": YELLOW, - "{\\ddot\\theta}": RED, + "{\\dot\\theta}": RED, + "{\\ddot\\theta}": YELLOW, "{t}": WHITE, "{\\mu}": WHITE, } @@ -37,7 +37,7 @@ def get_ode(): def get_period_formula(): return TexMobject( - "\\sqrt{\\,", "2\\pi", "L", "/", "g", "}", + "2\\pi", "\\sqrt{\\,", "L", "/", "g", "}", tex_to_color_map={ "L": BLUE, "g": YELLOW, diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 7cda089e..f2c48ca0 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -661,8 +661,8 @@ class DefineODE(Scene): tex_config = { "tex_to_color_map": { "{\\theta}": BLUE, - "{\\dot\\theta}": YELLOW, - "{\\ddot\\theta}": RED, + "{\\dot\\theta}": RED, + "{\\ddot\\theta}": YELLOW, }, "height": 0.5, } @@ -698,7 +698,7 @@ class DefineODE(Scene): def get_tangent_line(curve, alpha): line = Line( ORIGIN, 1.5 * RIGHT, - color=YELLOW, + color=RED, stroke_width=1.5, ) da = 0.0001 @@ -722,7 +722,7 @@ class DefineODE(Scene): get_point(t + a) for a in np.linspace(-0.5, 0.5, 11) ]) - curve.set_stroke(RED, 1) + curve.set_stroke(YELLOW, 1) return curve v_line = always_redraw(get_v_line) @@ -737,8 +737,8 @@ class DefineODE(Scene): words = VGroup( TextMobject("= Height").set_color(BLUE), - TextMobject("= Slope").set_color(YELLOW), - TextMobject("= ``Curvature''").set_color(RED), + TextMobject("= Slope").set_color(RED), + TextMobject("= ``Curvature''").set_color(YELLOW), ) words.scale(0.75) for word, sym in zip(words, thetas): @@ -872,7 +872,7 @@ class DefineODE(Scene): self.wait() self.play(FocusOn(second_deriv)) self.play( - Indicate(second_deriv, color=RED), + Indicate(second_deriv, color=YELLOW), ) self.wait() @@ -1001,8 +1001,8 @@ class SecondOrderEquationExample(DefineODE): "-", "\\omega", "{x}(t)", tex_to_color_map={ "{x}": BLUE, - "{\\dot x}": YELLOW, - "{\\ddot x}": RED, + "{\\dot x}": RED, + "{\\ddot x}": YELLOW, } ) equation.next_to(de_word, DOWN) @@ -1031,8 +1031,8 @@ class SecondOrderEquationExample(DefineODE): tex_config = { "tex_to_color_map": { "{x}": BLUE, - "{\\dot x}": YELLOW, - "{\\ddot x}": RED, + "{\\dot x}": RED, + "{\\ddot x}": YELLOW, }, "height": 0.5, } @@ -1068,7 +1068,7 @@ class SecondOrderEquationExample(DefineODE): def get_tangent_line(curve, alpha): line = Line( ORIGIN, 1.5 * RIGHT, - color=YELLOW, + color=RED, stroke_width=1.5, ) da = 0.0001 @@ -1092,7 +1092,7 @@ class SecondOrderEquationExample(DefineODE): get_point(t + a) for a in np.linspace(-0.5, 0.5, 11) ]) - curve.set_stroke(RED, 1) + curve.set_stroke(YELLOW, 1) return curve v_line = always_redraw(get_v_line) @@ -1107,8 +1107,8 @@ class SecondOrderEquationExample(DefineODE): words = VGroup( TextMobject("= Height").set_color(BLUE), - TextMobject("= Slope").set_color(YELLOW), - TextMobject("= ``Curvature''").set_color(RED), + TextMobject("= Slope").set_color(RED), + TextMobject("= ``Curvature''").set_color(YELLOW), ) words.scale(0.75) for word, sym in zip(words, xs): @@ -1511,13 +1511,7 @@ class ThreeBodiesInSpace(SpecialThreeDScene): bodies = self.bodies = VGroup() velocity_vectors = VGroup() - centers = [ - np.dot( - 4 * (np.random.random(3) - 0.5), - [RIGHT, UP, OUT] - ) - for x in range(len(masses)) - ] + centers = self.get_initial_positions() for mass, color, center in zip(masses, colors, centers): body = self.get_sphere( @@ -1529,31 +1523,21 @@ class ThreeBodiesInSpace(SpecialThreeDScene): ) body.set_opacity(0.75) body.mass = mass - body.set_width(0.15 * np.sqrt(mass)) + body.radius = 0.08 * np.sqrt(mass) + body.set_width(2 * body.radius) body.point = center body.move_to(center) - to_others = [ - center - center2 - for center2 in centers - ] - velocity = 0.2 * mass * normalize(np.cross(*filter( - lambda diff: get_norm(diff) > 0, - to_others - ))) - - body.velocity = velocity - body.add_updater(self.update_body) + body.velocity = self.get_initial_velocity( + center, centers, mass + ) vect = self.get_velocity_vector_mob(body) bodies.add(body) velocity_vectors.add(vect) - self.add(body) - # self.add(vect) - total_mass = np.sum([body.mass for body in bodies]) center_of_mass = reduce(op.add, [ body.mass * body.get_center() / total_mass @@ -1567,6 +1551,26 @@ class ThreeBodiesInSpace(SpecialThreeDScene): body.shift(-center_of_mass) body.velocity -= average_momentum + def get_initial_positions(self): + return [ + np.dot( + 4 * (np.random.random(3) - 0.5), + [RIGHT, UP, OUT] + ) + for x in range(len(self.masses)) + ] + + def get_initial_velocity(self, center, centers, mass): + to_others = [ + center - center2 + for center2 in centers + ] + velocity = 0.2 * mass * normalize(np.cross(*filter( + lambda diff: get_norm(diff) > 0, + to_others + ))) + return velocity + def add_trajectories(self): def update_trajectory(traj, dt): new_point = traj.body.point @@ -1582,8 +1586,11 @@ class ThreeBodiesInSpace(SpecialThreeDScene): self.add(traj, body) def let_play(self): + bodies = self.bodies + bodies.add_updater(self.update_bodies) # Break it up to see partial files as # it's rendered + self.add(bodies) for x in range(int(self.play_time)): self.wait() @@ -1607,23 +1614,25 @@ class ThreeBodiesInSpace(SpecialThreeDScene): # ) return always_redraw(draw_vector) - def update_body(self, body, dt): + def update_bodies(self, bodies, dt): G = self.G - acceleration = np.zeros(3) - for body2 in self.bodies: - if body2 is body: - continue - diff = body2.point - body.point - m2 = body2.mass - R = get_norm(diff) - acceleration += G * m2 * diff / (R**3) - num_mid_steps = 100 + num_mid_steps = 1000 for x in range(num_mid_steps): - body.point += body.velocity * dt / num_mid_steps - body.velocity += acceleration * dt / num_mid_steps - body.move_to(body.point) - return body + for body in bodies: + acceleration = np.zeros(3) + for body2 in bodies: + if body2 is body: + continue + diff = body2.point - body.point + m2 = body2.mass + R = get_norm(diff) + acceleration += G * m2 * diff / (R**3) + body.point += body.velocity * dt / num_mid_steps + body.velocity += acceleration * dt / num_mid_steps + for body in bodies: + body.move_to(body.point) + return bodies class AltThreeBodiesInSpace(ThreeBodiesInSpace): @@ -1636,16 +1645,96 @@ class AltThreeBodiesInSpace(ThreeBodiesInSpace): class TwoBodiesInSpace(ThreeBodiesInSpace): CONFIG = { "colors": [GREY, BLUE], - "masses": [1, 6], - "play_time": 5, + "masses": [6, 36], + "play_time": 60, } def construct(self): self.add_axes() self.add_bodies() self.add_trajectories() + self.add_velocity_vectors() + self.add_force_vectors() self.let_play() + def add_bodies(self): + super().add_bodies() + for body in self.bodies: + body.point = 3 * normalize(body.get_center()) + # body.point += 2 * IN + # body.velocity += (4 / 60) * OUT + body.move_to(body.point) + + def get_initial_positions(self): + return [ + np.dot( + 6 * (np.random.random(3) - 0.5), + [RIGHT, UP, ORIGIN] + ) + for x in range(len(self.masses)) + ] + + def get_initial_velocity(self, center, centers, mass): + return 0.75 * normalize(np.cross(center, OUT)) + + def add_velocity_vectors(self): + vectors = VGroup(*[ + self.get_velocity_vector(body) + for body in self.bodies + ]) + self.velocity_vectors = vectors + self.add(vectors) + + def get_velocity_vector(self, body): + def create_vector(b): + v = Vector( + b.velocity, + color=RED, + max_stroke_width_to_length_ratio=3, + ) + v.set_stroke(width=3) + v.shift( + b.point + b.radius * normalize(b.velocity) - + v.get_start(), + ) + v.set_shade_in_3d(True) + return v + return always_redraw(lambda: create_vector(body)) + + def add_force_vectors(self): + vectors = VGroup(*[ + self.get_force_vector(b1, b2) + for (b1, b2) in (self.bodies, self.bodies[::-1]) + ]) + self.force_vectors = vectors + self.add(vectors) + + def get_force_vector(self, body1, body2): + def create_vector(b1, b2): + r = b2.point - b1.point + F = r / (get_norm(r)**3) + v = Vector( + 4 * F, + color=YELLOW, + max_stroke_width_to_length_ratio=3, + ) + v.set_stroke(width=3) + v.shift( + b1.point + b1.radius * normalize(F) - + v.get_start(), + ) + v.set_shade_in_3d(True) + return v + return always_redraw(lambda: create_vector(body1, body2)) + + +class TwoBodiesWithZPart(TwoBodiesInSpace): + def add_bodies(self): + super().add_bodies() + for body in self.bodies: + body.point += 3 * IN + body.velocity += (6 / 60) * OUT + class DefineODECopy(DefineODE): pass diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py index 8ada4c57..8529bc67 100644 --- a/active_projects/ode/part1/wordy_scenes.py +++ b/active_projects/ode/part1/wordy_scenes.py @@ -97,6 +97,289 @@ class StrogatzQuote(Scene): return quote +class WriteInRadians(Scene): + def construct(self): + words = TextMobject("In radians") + words.set_color(YELLOW) + square = SurroundingRectangle(TexMobject("\\theta")) + square.next_to(words, UP) + self.play(ShowCreation(square)) + self.play(Write(words), FadeOut(square)) + self.wait() + + +class XEqLThetaToCorner(Scene): + def construct(self): + equation = TexMobject( + "x = L\\theta", + tex_to_color_map={ + "x": GREEN, + "\\theta": BLUE, + } + ) + equation.move_to(DOWN + 3 * RIGHT) + self.add(equation) + self.play(equation.to_corner, DL, {"buff": LARGE_BUFF}) + self.wait() + + +class ComingUp(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + frame = ScreenRectangle( + stroke_width=0, + fill_color=BLACK, + fill_opacity=1, + height=6 + ) + title = TextMobject("Coming up") + title.scale(1.5) + title.to_edge(UP) + frame.next_to(title, DOWN) + animated_frame = AnimatedBoundary(frame) + self.add(frame, title, animated_frame) + self.wait(10) + + +class InputLabel(Scene): + def construct(self): + label = TextMobject("Input") + label.scale(1.25) + arrow = Vector(UP) + arrow.next_to(label, UP) + self.play( + FadeInFrom(label, UP), + GrowArrow(arrow) + ) + self.wait() + + +class ReallyHardToSolve(Scene): + def construct(self): + words = TextMobject( + "They're", "really\\\\", + "freaking", "hard\\\\", + "to", "solve\\\\", + ) + words.set_height(6) + + self.wait() + for word in words: + wait_time = 0.05 * len(word) + self.add(word) + self.wait(wait_time) + self.wait() + + +class ReasonForSolution(Scene): + def construct(self): + # Words + eq_word = TextMobject("Differential\\\\Equation") + s_word = TextMobject("Solution") + u_word = TextMobject("Understanding") + c_word = TextMobject("Computation") + cu_group = VGroup(u_word, c_word) + cu_group.arrange(DOWN, buff=2) + group = VGroup(eq_word, s_word, cu_group) + group.arrange(RIGHT, buff=2) + words = VGroup(eq_word, s_word, u_word, c_word) + + # Arrows + arrows = VGroup( + Arrow(eq_word.get_right(), s_word.get_left()), + Arrow(s_word.get_right(), u_word.get_left()), + Arrow(s_word.get_right(), c_word.get_left()), + ) + arrows.set_color(LIGHT_GREY) + new_arrows = VGroup( + Arrow( + eq_word.get_corner(UR), + u_word.get_left(), + path_arc=-60 * DEGREES, + ), + Arrow( + eq_word.get_corner(DR), + c_word.get_left(), + path_arc=60 * DEGREES, + ), + ) + new_arrows.set_color(BLUE) + + # Define first examples + t2c = { + "{x}": BLUE, + "{\\dot x}": RED, + } + equation = TexMobject( + "{\\dot x}(t) = k {x}(t)", + tex_to_color_map=t2c, + ) + equation.next_to(eq_word, DOWN) + solution = TexMobject( + "{x}(t) = x_0 e^{kt}", + tex_to_color_map=t2c, + ) + solution.next_to(s_word, DOWN, MED_LARGE_BUFF) + equation.align_to(solution, DOWN) + + axes = Axes( + x_min=-1, + x_max=5.5, + y_min=-1, + y_max=4.5, + y_axis_config={"unit_size": 0.5} + ) + axes.set_stroke(width=2) + graph_line = axes.get_graph( + lambda x: np.exp(0.4 * x) + ) + graph_line.set_stroke(width=2) + graph = VGroup(axes, graph_line) + graph.scale(0.5) + graph.next_to(u_word, UP) + + computation = TexMobject( + # "\\displaystyle " + "e^x = \\sum_{n=0}^\\infty " + "\\frac{x^n}{n!}" + ) + computation.next_to(c_word, DOWN) + + first_examples = VGroup( + equation, solution, graph, computation + ) + + # Second example + ode = get_ode() + ode.scale(0.75) + second_examples = VGroup( + ode, + TexMobject("???").set_color(LIGHT_GREY), + ScreenRectangle( + height=2, + stroke_width=1, + ), + ) + for fe, se in zip(first_examples, second_examples): + se.move_to(fe, DOWN) + + ode.shift(2 * SMALL_BUFF * DOWN) + ode.add_to_back(BackgroundRectangle(ode[-4:])) + + self.add(eq_word) + self.add(equation) + self.play( + FadeInFrom(s_word, LEFT), + GrowArrow(arrows[0]), + TransformFromCopy(equation, solution) + ) + self.wait() + self.play( + FadeInFrom(c_word, UL), + GrowArrow(arrows[2]), + FadeInFrom(computation, UP) + ) + self.wait() + self.play( + FadeInFrom(u_word, DL), + GrowArrow(arrows[1]), + FadeInFromDown(graph) + ) + self.wait(2) + + self.play( + FadeOut(first_examples), + FadeIn(second_examples[:2]) + ) + self.wait() + self.play( + arrows.fade, 0.75, + s_word.fade, 0.75, + second_examples[1].fade, 0.75, + ShowCreation(new_arrows[0]), + FadeIn(second_examples[2]) + ) + self.play( + ShowCreation(new_arrows[1]), + Animation(second_examples), + ) + self.wait() + + +class WritePhaseSpace(Scene): + def construct(self): + word = TextMobject("Phase space") + word.scale(2) + word.shift(FRAME_WIDTH * LEFT / 4) + word.to_edge(UP) + word.add_background_rectangle() + + lines = VGroup(*[ + Line(v, 1.3 * v) + for v in compass_directions(50) + ]) + lines.replace(word, stretch=True) + lines.scale(1.5) + lines.set_stroke(YELLOW) + lines.shuffle() + + self.add(word) + self.play( + ShowPassingFlashWithThinningStrokeWidth( + lines, + lag_ratio=0.002, + run_time=1.5, + time_width=0.9, + n_segments=5, + ) + ) + self.wait() + + +class GleickQuote(Scene): + def construct(self): + quote = TextMobject( + "``[Phase space is] one of the most\\\\", + "powerful inventions", "of modern science.''\\\\", + ) + quote.power_part = quote.get_part_by_tex("power") + book = ImageMobject("ChaosBookCover") + book.set_height(5) + book.next_to(ORIGIN, LEFT) + book.to_edge(DOWN) + gleick = ImageMobject("JamesGleick") + gleick.set_height(5) + gleick.next_to(ORIGIN, RIGHT) + gleick.to_edge(DOWN) + quote.to_edge(UP) + + self.play( + FadeInFrom(book, RIGHT), + FadeInFrom(gleick, LEFT), + ) + self.wait() + self.play(Write(quote)) + self.play(Write( + quote.power_part.copy().set_color(BLUE), + run_time=1 + )) + self.wait() + + +class WritePhaseFlow(Scene): + def construct(self): + words = TextMobject("Phase flow") + words.scale(2) + words.shift(FRAME_WIDTH * LEFT / 4) + words.to_edge(UP) + words.add_background_rectangle() + self.play(Write(words)) + self.wait() + + class ShowSineValues(Scene): def construct(self): angle_tracker = ValueTracker(60 * DEGREES) From 46711a7e3ab1697804d8c4bcbddd97a89decac72 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:22:06 -0700 Subject: [PATCH 07/18] Fixed typo --- manimlib/mobject/coordinate_systems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index b3d2cc35..ebce02f6 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -82,7 +82,7 @@ class CoordinateSystem(): )[0], target=x, lower_bound=self.x_min, - uplper_bound=self.x_max, + upper_bound=self.x_max, ) if alpha is not None: return graph.point_from_proportion(alpha) From a03de9dde9349f9789744a5f210925073c25c0d9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:22:24 -0700 Subject: [PATCH 08/18] Fixed tyop --- manimlib/mobject/mobject_update_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject_update_utils.py b/manimlib/mobject/mobject_update_utils.py index 2d07235a..767adf20 100644 --- a/manimlib/mobject/mobject_update_utils.py +++ b/manimlib/mobject/mobject_update_utils.py @@ -22,7 +22,7 @@ def always(method, *args, **kwargs): def f_always(method, *arg_generators, **kwargs): """ - More functional version of always, where insetead + More functional version of always, where instead of taking in args, it takes in functions which ouput the relevant arguments. """ From 6b59cdcd374721634147308eef2b31574e862655 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:22:53 -0700 Subject: [PATCH 09/18] Fixed issue with numberline last tick vs. arrow conflict --- manimlib/mobject/number_line.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index df372e3e..365ceccc 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -95,9 +95,10 @@ class NumberLine(Line): ) def get_tick_numbers(self): + u = -1 if self.include_tip else 1 return np.arange( self.leftmost_tick, - self.x_max + self.tick_frequency / 2, + self.x_max + u * self.tick_frequency / 2, self.tick_frequency ) From 86a8cefc0f2122767155d632cce8288617dd2930 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:23:11 -0700 Subject: [PATCH 10/18] Fixed ClockPassesTime animation --- manimlib/mobject/svg/drawings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 7c97adb0..42c0764f 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -393,11 +393,13 @@ class ClockPassesTime(Animation): radians=hour_radians, **rot_kwargs ) + self.hour_rotation.begin() self.minute_rotation = Rotating( clock.minute_hand, radians=12 * hour_radians, **rot_kwargs ) + self.minute_rotation.begin() Animation.__init__(self, clock, **kwargs) def interpolate_mobject(self, alpha): From b9c73fb1a738a6ee21e5d623eba4cbef3216cbd7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:23:44 -0700 Subject: [PATCH 11/18] Temporary patch to a PiCreatureScene problem --- manimlib/for_3b1b_videos/pi_creature_scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index bb790691..4af617e1 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -174,6 +174,7 @@ class PiCreatureScene(Scene): continue anims_with_pi_creature = [anim for anim in animations if pi_creature in anim.mobject.get_family()] for anim in anims_with_pi_creature: + continue # TODO, this is broken if isinstance(anim, Transform): index = anim.mobject.get_family().index(pi_creature) target_family = anim.target_mobject.get_family() From e3a40388ec8f2052e95fe625dbddd4793001bf85 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 30 Mar 2019 13:23:56 -0700 Subject: [PATCH 12/18] Pile of ode scenes made while editing --- active_projects/ode/all_part1_scenes.py | 10 + active_projects/ode/part1/phase_space.py | 481 +++++++- .../ode/part1/shared_constructs.py | 52 + active_projects/ode/part1/staging.py | 1040 ++++++++++++++++- 4 files changed, 1506 insertions(+), 77 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index a8a13ebd..3ab32a2a 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -28,6 +28,8 @@ ALL_SCENE_CLASSES = [ RabbitFoxPopulations, RabbitFoxEquation, # Something... + ShowSimpleTrajectory, + SimpleProjectileEquation, ShowGravityAcceleration, AnalyzePendulumForce, ShowSineValues, @@ -52,6 +54,13 @@ ALL_SCENE_CLASSES = [ SpectrumOfStartingStates, WritePhaseFlow, AskAboutStability, + LoveExample, + PassageOfTime, + LovePhaseSpace, + ComparePhysicsToLove, + FramesComparingPhysicsToLove, + SetupToTakingManyTinySteps, + ShowClutterPrevention, # VisualizeHeightSlopeCurvature, VisualizeStates, ReferencePiCollisionStateSpaces, @@ -81,6 +90,7 @@ ALL_SCENE_CLASSES = [ InaccurateComputation, HungerForExactness, ShowRect, + ShowSquare, JumpToThisPoint, ThreeBodyEquation, ItGetsWorse, diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 40d69d30..ea4ad0c9 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -1025,7 +1025,7 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): ] self.play(*anims, run_time=total_time) - def get_trajectory(self, start_point, time, dt=0.1, added_steps=1000): + def get_trajectory(self, start_point, time, dt=0.1, added_steps=100): field = self.vector_field traj = VMobject() traj.start_new_path(start_point) @@ -1373,10 +1373,261 @@ class AskAboutStability(ShowHighVelocityCase): self.wait() +class LovePhaseSpace(ShowHighVelocityCase): + CONFIG = { + "vector_field_config": { + "max_magnitude": 4, + # "delta_x": 2, + # "delta_y": 2, + }, + "a": 0.5, + "b": 0.3, + "mu": 0.2, + } + + def construct(self): + self.setup_plane() + self.add_equations() + self.show_vector_field() + self.show_example_trajectories() + self.add_resistance_term() + self.show_new_trajectories() + + def setup_plane(self): + plane = self.plane = NumberPlane() + plane.add_coordinates() + self.add(plane) + + h1, h2 = hearts = VGroup(*[ + get_heart_var(i) + for i in (1, 2) + ]) + hearts.scale(0.5) + hearts.set_stroke(BLACK, 5, background=True) + + h1.next_to(plane.x_axis.get_right(), UL, SMALL_BUFF) + h2.next_to(plane.y_axis.get_top(), DR, SMALL_BUFF) + for h in hearts: + h.shift_onto_screen(buff=MED_SMALL_BUFF) + plane.add(hearts) + + self.axis_hearts = hearts + + def add_equations(self): + equations = VGroup( + get_love_equation1(), + get_love_equation2(), + ) + equations.scale(0.5) + equations.arrange( + DOWN, + aligned_edge=LEFT, + buff=MED_LARGE_BUFF + ) + equations.to_corner(UL) + equations.add_background_rectangle_to_submobjects() + # for eq in equations: + # eq.add_background_rectangle_to_submobjects() + + self.add(equations) + self.equations = equations + + def show_vector_field(self): + field = VectorField( + lambda p: np.array([ + self.a * p[1], -self.b * p[0], 0 + ]), + **self.vector_field_config + ) + field.sort(get_norm) + x_range = np.arange(-7, 7.5, 0.5) + y_range = np.arange(-4, 4.5, 0.5) + x_axis_arrows = VGroup(*[ + field.get_vector([x, 0, 0]) + for x in x_range + ]) + y_axis_arrows = VGroup(*[ + field.get_vector([0, y, 0]) + for y in y_range + ]) + axis_arrows = VGroup(*x_axis_arrows, *y_axis_arrows) + + axis_arrows.save_state() + for arrow in axis_arrows: + real_len = get_norm(field.func(arrow.get_start())) + arrow.scale( + 0.5 * real_len / arrow.get_length(), + about_point=arrow.get_start() + ) + + self.play( + LaggedStartMap(GrowArrow, x_axis_arrows), + ) + self.play( + LaggedStartMap(GrowArrow, y_axis_arrows), + ) + self.wait() + self.add(field, self.equations, self.axis_hearts) + self.play( + axis_arrows.restore, + # axis_arrows.fade, 1, + ShowCreation(field), + run_time=3 + ) + self.remove(axis_arrows) + self.wait() + + self.field = self.vector_field = field + + def show_example_trajectories(self): + n_points = 20 + total_time = 30 + + start_points = self.start_points = [ + 2.5 * np.random.random() * rotate_vector( + RIGHT, + TAU * np.random.random() + ) + for x in range(n_points) + ] + dots = VGroup(*[Dot(sp) for sp in start_points]) + dots.set_color_by_gradient(BLUE, WHITE) + + words = TextMobject("Possible initial\\\\", "conditions") + words.scale(1.5) + words.add_background_rectangle_to_submobjects() + words.set_stroke(BLACK, 5, background=True) + words.shift(FRAME_WIDTH * RIGHT / 4) + words.to_edge(UP) + self.possibility_words = words + + self.play( + LaggedStartMap( + FadeInFromLarge, dots, + lambda m: (m, 5) + ), + FadeInFromDown(words) + ) + + trajs = VGroup(*[ + self.get_trajectory( + sp, total_time, + added_steps=10, + ) + for sp in start_points + ]) + trajs.set_color_by_gradient(BLUE, WHITE) + + dots.trajs = trajs + + def update_dots(ds): + for d, t in zip(ds, ds.trajs): + d.move_to(t.points[-1]) + dots.add_updater(update_dots) + + self.add(trajs, dots) + self.play( + ShowCreation( + trajs, + lag_ratio=0, + run_time=10, + rate_func=linear, + ) + ) + + self.trajs = trajs + self.dots = dots + + def add_resistance_term(self): + added_term = VGroup( + TexMobject("-\\mu"), + get_heart_var(2).scale(0.5), + ) + added_term.arrange(RIGHT, buff=SMALL_BUFF) + equation2 = self.equations[1] + equation2.generate_target() + br, deriv, eq, neg_b, h1 = equation2.target + added_term.next_to(eq, RIGHT, SMALL_BUFF) + added_term.align_to(h1, DOWN) + VGroup(neg_b, h1).next_to( + added_term, RIGHT, SMALL_BUFF, + aligned_edge=DOWN, + ) + br.stretch(1.2, 0, about_edge=LEFT) + + brace = Brace(added_term, DOWN, buff=SMALL_BUFF) + words = brace.get_text( + "``Resistance'' term" + ) + words.set_stroke(BLACK, 5, background=True) + words.add_background_rectangle() + + self.add(equation2, added_term) + self.play( + MoveToTarget(equation2), + FadeInFromDown(added_term), + GrowFromCenter(brace), + Write(words), + ) + self.play(ShowCreationThenFadeAround(added_term)) + + equation2.add(added_term, brace, words) + + def show_new_trajectories(self): + dots = self.dots + trajs = self.trajs + field = self.field + + new_field = VectorField( + lambda p: np.array([ + self.a * p[1], + -self.mu * p[1] - self.b * p[0], + 0 + ]), + **self.vector_field_config + ) + new_field.sort(get_norm) + + field.generate_target() + for vect in field.target: + vect.become(new_field.get_vector(vect.get_start())) + + self.play(*map( + FadeOut, + [trajs, dots, self.possibility_words] + )) + self.play(MoveToTarget(field)) + self.vector_field = new_field + + total_time = 30 + new_trajs = VGroup(*[ + self.get_trajectory( + sp, total_time, + added_steps=10, + ) + for sp in self.start_points + ]) + new_trajs.set_color_by_gradient(BLUE, WHITE) + dots.trajs = new_trajs + + self.add(new_trajs, dots) + self.play( + ShowCreation( + new_trajs, + lag_ratio=0, + run_time=10, + rate_func=linear, + ), + ) + self.wait() + + class TakeManyTinySteps(IntroduceVectorField): CONFIG = { "initial_theta": 60 * DEGREES, "initial_theta_dot": 0, + "initial_theta_tex": "\\pi / 3", + "initial_theta_dot_tex": "0", } def construct(self): @@ -1391,10 +1642,10 @@ class TakeManyTinySteps(IntroduceVectorField): self.add(self.plane, field) def take_many_time_steps(self): - delta_t_tracker = ValueTracker(0.5) + self.setup_trackers() + delta_t_tracker = self.delta_t_tracker get_delta_t = delta_t_tracker.get_value - - time_tracker = ValueTracker(10) + time_tracker = self.time_tracker get_t = time_tracker.get_value traj = always_redraw( @@ -1414,40 +1665,9 @@ class TakeManyTinySteps(IntroduceVectorField): ) ) - t_label, dt_label = labels = VGroup(*[ - VGroup( - TexMobject("{} = ".format(s)), - DecimalNumber(0) - ).arrange(RIGHT, aligned_edge=DOWN) - for s in ("t", "{\\Delta t}") - ]) - init_labels = VGroup( - TexMobject( - "\\theta_0", "= \\pi / 3", - tex_to_color_map={"\\theta": BLUE}, - ), - TexMobject( - "{\\dot\\theta}_0 = 0", - tex_to_color_map={"{\\dot\\theta}": YELLOW}, - ), - ) - for group in labels, init_labels: - for label in group: - label.scale(1.25) - label.add_background_rectangle() - group.arrange(DOWN) - group.shift(FRAME_WIDTH * RIGHT / 4) - labels.to_edge(UP) - init_labels.shift(2 * DOWN) - - dt_label[-1].add_updater( - lambda d: d.set_value(get_delta_t()) - ) - t_label[-1].add_updater( - lambda d: d.set_value( - int(get_t() / get_delta_t()) * get_delta_t() - ) - ) + # Labels + labels, init_labels = self.get_labels(get_t, get_delta_t) + t_label, dt_label = labels theta_t_label = TexMobject("\\theta(t)...\\text{ish}") theta_t_label.scale(0.75) @@ -1485,14 +1705,18 @@ class TakeManyTinySteps(IntroduceVectorField): count = Integer() count.scale(1.5) count.to_edge(LEFT) - count.shift(UP) + count.shift(UP + MED_SMALL_BUFF * UR) count.add_updater(lambda c: c.set_value( count_tracker.get_value() )) count_label = TextMobject("steps") count_label.scale(1.5) count_label.add_updater( - lambda m: m.next_to(count, RIGHT).shift(SMALL_BUFF * DOWN) + lambda m: m.next_to( + count[-1], RIGHT, + submobject_to_align=m[0][0], + aligned_edge=DOWN + ) ) scaled_vectors = vectors.copy() @@ -1515,16 +1739,6 @@ class TakeManyTinySteps(IntroduceVectorField): self.add(count, count_label, scaled_vectors) self.play( - # LaggedStartMap( - # ApplyFunction, vectors, - # lambda vector: ( - # lambda v: v.scale( - # 1 / v.get_length() - # ), - # vector - # ), - # rate_func=there_and_back, - # ), ApplyMethod( count_tracker.set_value, int(target_time / target_delta_t), @@ -1535,6 +1749,49 @@ class TakeManyTinySteps(IntroduceVectorField): self.play(FadeOut(scaled_vectors)) self.wait() + def setup_trackers(self): + self.delta_t_tracker = ValueTracker(0.5) + self.time_tracker = ValueTracker(10) + + def get_labels(self, get_t, get_delta_t): + t_label, dt_label = labels = VGroup(*[ + VGroup( + TexMobject("{} = ".format(s)), + DecimalNumber(0) + ).arrange(RIGHT, aligned_edge=DOWN) + for s in ("t", "{\\Delta t}") + ]) + + dt_label[-1].add_updater( + lambda d: d.set_value(get_delta_t()) + ) + t_label[-1].add_updater( + lambda d: d.set_value( + int(np.ceil(get_t() / get_delta_t())) * get_delta_t() + ) + ) + + init_labels = VGroup( + TexMobject( + "\\theta_0", "=", self.initial_theta_tex, + tex_to_color_map={"\\theta": BLUE}, + ), + TexMobject( + "{\\dot\\theta}_0 =", self.initial_theta_dot_tex, + tex_to_color_map={"{\\dot\\theta}": YELLOW}, + ), + ) + for group in labels, init_labels: + for label in group: + label.scale(1.25) + label.add_background_rectangle() + group.arrange(DOWN) + group.shift(FRAME_WIDTH * RIGHT / 4) + labels.to_edge(UP) + init_labels.shift(2 * DOWN) + + return labels, init_labels + # def get_time_step_points(self, delta_t, total_time, theta_0, theta_dot_0): plane = self.plane @@ -1580,6 +1837,130 @@ class TakeManyTinySteps(IntroduceVectorField): return result +class SetupToTakingManyTinySteps(TakeManyTinySteps): + CONFIG = { + } + + def construct(self): + self.initialize_plane_and_field() + self.show_step() + + def show_step(self): + self.setup_trackers() + get_delta_t = self.delta_t_tracker.get_value + get_t = self.time_tracker.get_value + + labels, init_labels = self.get_labels(get_t, get_delta_t) + t_label, dt_label = labels + + dt_part = dt_label[1][0][:-1].copy() + + init_labels_rect = SurroundingRectangle(init_labels) + init_labels_rect.set_color(PINK) + + field = self.vector_field + point = self.plane.coords_to_point( + self.initial_theta, + self.initial_theta_dot, + ) + dot = Dot(point, color=init_labels_rect.get_color()) + + vector_value = field.func(point) + vector = field.get_vector(point) + vector.scale( + get_norm(vector_value) / vector.get_length(), + about_point=vector.get_start() + ) + scaled_vector = vector.copy() + scaled_vector.scale( + get_delta_t(), + about_point=scaled_vector.get_start() + ) + + v_label = TexMobject("\\vec{\\textbf{v}}") + v_label.set_stroke(BLACK, 5, background=True) + v_label.next_to(vector, LEFT, SMALL_BUFF) + + real_field = field.copy() + for v in real_field: + p = v.get_start() + v.scale( + get_norm(field.func(p)) / v.get_length(), + about_point=p + ) + + self.add(init_labels) + self.play(ShowCreation(init_labels_rect)) + self.play(ReplacementTransform( + init_labels_rect, + dot, + )) + self.wait() + self.add(vector, dot) + self.play( + ShowCreation(vector), + FadeInFrom(v_label, RIGHT), + ) + self.play(FadeInFromDown(dt_label)) + self.wait() + + # + v_label.generate_target() + dt_part.generate_target() + dt_part.target.next_to(scaled_vector, LEFT, SMALL_BUFF) + v_label.target.next_to(dt_part.target, LEFT, SMALL_BUFF) + rect = BackgroundRectangle( + VGroup(v_label.target, dt_part.target) + ) + + self.add(rect, v_label, dt_part) + self.play( + ReplacementTransform(vector, scaled_vector), + FadeIn(rect), + MoveToTarget(v_label), + MoveToTarget(dt_part), + ) + self.add(scaled_vector, dot) + self.wait() + + self.play( + LaggedStart(*[ + Transform( + sm1, sm2, + rate_func=there_and_back_with_pause, + ) + for sm1, sm2 in zip(field, real_field) + ], lag_ratio=0.001, run_time=3) + ) + self.wait() + + +class ShowClutterPrevention(SetupToTakingManyTinySteps): + def construct(self): + self.initialize_plane_and_field() + + # Copied from above scene + field = self.vector_field + real_field = field.copy() + for v in real_field: + p = v.get_start() + v.scale( + get_norm(field.func(p)) / v.get_length(), + about_point=p + ) + + self.play( + LaggedStart(*[ + Transform( + sm1, sm2, + rate_func=there_and_back_with_pause, + ) + for sm1, sm2 in zip(field, real_field) + ], lag_ratio=0.001, run_time=3) + ) + self.wait() + + class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps): CONFIG = { "initial_thetas": np.linspace(0.1, PI - 0.1, 10), diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 0bed3314..37aa12a0 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -64,3 +64,55 @@ def get_vector_symbol(*texs, **kwargs): config.update(kwargs) array = [[tex] for tex in texs] return Matrix(array, **config) + + +def get_heart_var(index): + heart = SuitSymbol("hearts") + if index == 1: + heart.set_color(BLUE_C) + elif index == 2: + heart.set_color(GREEN) + heart.set_height(0.7) + index = Integer(index) + index.move_to(heart.get_corner(DR)) + heart.add(index) + return heart + + +def get_heart_var_deriv(index): + heart = get_heart_var(index) + filler_tex = "T" + deriv = TexMobject("{d", filler_tex, "\\over", "dt}") + deriv.scale(2) + filler = deriv.get_part_by_tex(filler_tex) + heart.match_height(filler) + heart.move_to(filler) + heart.scale(1.5, about_edge=UL) + deriv.remove(filler) + deriv.add(heart) + deriv.heart = heart + return deriv + + +def get_love_equation1(): + equation = VGroup( + get_heart_var_deriv(1), + TexMobject("=").scale(2), + TexMobject("a").scale(2), + get_heart_var(2) + ) + equation.arrange(RIGHT) + equation[-1].shift(SMALL_BUFF * DL) + return equation + + +def get_love_equation2(): + equation = VGroup( + get_heart_var_deriv(2), + TexMobject("=").scale(2), + TexMobject("-b").scale(2), + get_heart_var(1), + ) + equation.arrange(RIGHT) + equation[-1].shift(SMALL_BUFF * DL) + return equation diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index f2c48ca0..8fcb852a 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -47,15 +47,28 @@ class VectorFieldTest(Scene): class ShowRect(Scene): + CONFIG = { + "height": 1, + "width": 3, + } + def construct(self): - rect = Rectangle() + rect = Rectangle( + height=self.height, + width=self.width, + ) rect.set_stroke(YELLOW) - rect.set_height(1) - rect.set_width(3, stretch=True) self.play(ShowCreation(rect)) self.play(FadeOut(rect)) +class ShowSquare(ShowRect): + CONFIG = { + "height": 3, + "width": 3, + } + + class PeriodFormula(Scene): def construct(self): formula = get_period_formula() @@ -259,6 +272,11 @@ class RabbitFoxEquation(PhaseSpaceOfPopulationModel): class ShowGravityAcceleration(Scene): + CONFIG = { + "flash": True, + "add_ball_copies": True, + } + def construct(self): self.add_gravity_field() self.add_title() @@ -332,24 +350,10 @@ class ShowGravityAcceleration(Scene): ) def show_trajectory(self): - ball = Circle( - stroke_width=1, - stroke_color=WHITE, - fill_color=GREY, - fill_opacity=1, - sheen_factor=1, - sheen_direction=UL, - radius=0.25, - ) - randy = Randolph(mode="pondering") - randy.eyes.set_stroke(BLACK, 0.5) - randy.match_height(ball) - randy.scale(0.75) - randy.move_to(ball) - ball.add(randy) - total_time = 6 + ball = self.get_ball() + p0 = 3 * DOWN + 5 * LEFT v0 = 2.8 * UP + 1.5 * RIGHT g = 0.9 * DOWN @@ -416,11 +420,13 @@ class ShowGravityAcceleration(Scene): for t1, t2 in zip(times, times[1:]): v_vect_copy = v_vect.copy() v_vect_copies.add(v_vect_copy) - self.add(v_vect_copy) ball_copy = ball.copy() ball_copy.clear_updaters() ball_copies.add(ball_copy) - self.add(ball_copy) + + if self.add_ball_copies: + self.add(v_vect_copy) + self.add(ball_copy, ball) dashed_graph.save_state() kw = { @@ -430,7 +436,7 @@ class ShowGravityAcceleration(Scene): alpha ) } - self.play( + anims = [ ShowCreation(dashed_graph, **kw), MoveAlongPath(ball, graph, **kw), MoveAlongPath(v_point, velocity_graph, **kw), @@ -438,9 +444,10 @@ class ShowGravityAcceleration(Scene): time_tracker.increment_value, 1, rate_func=linear ), - flash, - run_time=1, - ) + ] + if self.flash: + anims.append(flash) + self.play(*anims, run_time=1) dashed_graph.restore() randy.clear_updaters() self.play(FadeOut(time_group)) @@ -505,6 +512,517 @@ class ShowGravityAcceleration(Scene): ) self.wait() + # + def get_ball(self): + ball = Circle( + stroke_width=1, + stroke_color=WHITE, + fill_color=GREY, + fill_opacity=1, + sheen_factor=1, + sheen_direction=UL, + radius=0.25, + ) + randy = Randolph(mode="pondering") + randy.eyes.set_stroke(BLACK, 0.5) + randy.match_height(ball) + randy.scale(0.75) + randy.move_to(ball) + ball.add(randy) + return ball + + +class ShowSimpleTrajectory(ShowGravityAcceleration): + CONFIG = { + "flash": False, + } + + def construct(self): + self.show_trajectory() + + +class SimpleProjectileEquation(ShowGravityAcceleration): + CONFIG = { + "y0": 0, + "g": 9.8, + "axes_config": { + "x_min": 0, + "x_max": 6, + "x_axis_config": { + "unit_size": 1.5, + "tip_width": 0.15, + }, + "y_min": -30, + "y_max": 35, + "y_axis_config": { + "unit_size": 0.1, + "numbers_with_elongated_ticks": range( + -30, 35, 10 + ), + "tick_size": 0.05, + "numbers_to_show": range(-30, 31, 10), + "tip_width": 0.15, + }, + "center_point": 2 * LEFT, + } + } + + def construct(self): + self.add_axes() + self.setup_trajectory() + + self.show_trajectory() + self.show_equation() + self.solve_for_velocity() + self.solve_for_position() + + def add_axes(self): + axes = self.axes = Axes(**self.axes_config) + axes.set_stroke(width=2) + axes.add_coordinates() + + t_label = TexMobject("t") + t_label.next_to(axes.x_axis.get_right(), UL) + axes.add(t_label) + + self.add(axes) + + def setup_trajectory(self): + axes = self.axes + total_time = self.total_time = 5 + + ball = self.get_ball() + offset_vector = 3 * LEFT + + g = self.g + y0 = self.y0 + v0 = 0.5 * g * total_time + + t_tracker = ValueTracker(0) + get_t = t_tracker.get_value + + # Position + def y_func(t): + return -0.5 * g * t**2 + v0 * t + y0 + + graph_template = axes.get_graph(y_func, x_max=total_time) + graph_template.set_stroke(width=2) + traj_template = graph_template.copy() + traj_template.stretch(0, 0) + traj_template.move_to( + axes.coords_to_point(0, 0), DOWN + ) + traj_template.shift(offset_vector) + traj_template.set_stroke(width=0.5) + + graph = VMobject() + graph.set_stroke(BLUE, 2) + traj = VMobject() + traj.set_stroke(WHITE, 0.5) + graph.add_updater(lambda g: g.pointwise_become_partial( + graph_template, 0, get_t() / total_time + )) + traj.add_updater(lambda t: t.pointwise_become_partial( + traj_template, 0, get_t() / total_time + )) + + def get_ball_point(): + return axes.coords_to_point( + 0, y_func(get_t()) + ) + offset_vector + + f_always(ball.move_to, get_ball_point) + + h_line = always_redraw(lambda: DashedLine( + get_ball_point(), + axes.input_to_graph_point(get_t(), graph_template), + stroke_width=1, + )) + + y_label = TexMobject("y", "(t)") + y_label.set_color_by_tex("y", BLUE) + y_label.add_updater( + lambda m: m.next_to( + graph.get_last_point(), + UR, SMALL_BUFF, + ) + ) + + # Velocity + def v_func(t): + return -g * t + v0 + + def get_v_vect(): + return Vector( + axes.y_axis.unit_size * v_func(get_t()) * UP, + color=RED, + ) + v_vect = always_redraw( + lambda: get_v_vect().shift(get_ball_point()) + ) + v_brace = always_redraw(lambda: Brace(v_vect, LEFT)) + dy_dt_label = TexMobject( + "{d", "y", "\\over dt}", "(t)", + ) + dy_dt_label.scale(0.8) + dy_dt_label.set_color_by_tex("y", BLUE) + y_dot_label = TexMobject("\\dot y", "(t)") + y_dot_label.set_color_by_tex("\\dot y", RED) + for label in dy_dt_label, y_dot_label: + label.add_updater(lambda m: m.next_to( + v_brace, LEFT, SMALL_BUFF, + )) + + graphed_v_vect = always_redraw( + lambda: get_v_vect().shift( + axes.coords_to_point(get_t(), 0) + ) + ) + v_graph_template = axes.get_graph( + v_func, x_max=total_time, + ) + v_graph = VMobject() + v_graph.set_stroke(RED, 2) + v_graph.add_updater(lambda m: m.pointwise_become_partial( + v_graph_template, + 0, get_t() / total_time, + )) + + # Acceleration + def get_a_vect(): + return Vector( + axes.y_axis.unit_size * g * DOWN + ) + + a_vect = get_a_vect() + a_vect.add_updater(lambda a: a.move_to( + get_ball_point(), UP, + )) + a_brace = Brace(a_vect, RIGHT) + always(a_brace.next_to, a_vect, RIGHT, SMALL_BUFF) + d2y_dt2_label = TexMobject( + "d^2", "{y}", "\\over dt}", "(t)" + ) + d2y_dt2_label.scale(0.8) + d2y_dt2_label.set_color_by_tex( + "y", BLUE, + ) + y_ddot_label = TexMobject("\\ddot y", "(t)") + y_ddot_label.set_color_by_tex("\\ddot y", YELLOW) + for label in d2y_dt2_label, y_ddot_label: + label.add_updater(lambda m: m.next_to( + a_brace, RIGHT, SMALL_BUFF + )) + a_graph = axes.get_graph( + lambda t: -g, x_max=total_time, + ) + a_graph.set_stroke(YELLOW, 2) + + graphed_a_vect = get_a_vect() + graphed_a_vect.add_updater(lambda a: a.move_to( + axes.coords_to_point(get_t(), 0), UP, + )) + + self.set_variables_as_attrs( + t_tracker, + graph, + y_label, + traj, + h_line, + v_vect, + v_brace, + dy_dt_label, + y_dot_label, + ball, + graphed_v_vect, + v_graph, + a_vect, + a_brace, + d2y_dt2_label, + y_ddot_label, + a_graph, + graphed_a_vect, + ) + + def show_trajectory(self): + self.add( + self.h_line, + self.traj, + self.ball, + self.graph, + self.y_label, + ) + self.play_trajectory() + self.wait() + + self.add( + self.v_vect, + self.v_brace, + self.dy_dt_label, + self.ball, + self.graphed_v_vect, + self.v_graph, + ) + self.play_trajectory() + self.wait() + + self.add( + self.a_vect, + self.ball, + self.a_brace, + self.d2y_dt2_label, + self.a_graph, + self.graphed_a_vect, + ) + self.play_trajectory() + self.wait() + + self.play( + ReplacementTransform( + self.dy_dt_label, + self.y_dot_label, + ), + ShowCreationThenFadeAround( + self.y_dot_label, + ), + ) + self.play( + ReplacementTransform( + self.d2y_dt2_label, + self.y_ddot_label, + ), + ShowCreationThenFadeAround( + self.y_ddot_label, + ), + ) + + def show_equation(self): + y_ddot = self.y_ddot_label + new_y_ddot = y_ddot.deepcopy() + new_y_ddot.clear_updaters() + + equation = VGroup( + new_y_ddot, + *TexMobject( + "=", "-g", + tex_to_color_map={"-g": YELLOW}, + ), + ) + new_y_ddot.next_to(equation[1], LEFT, SMALL_BUFF) + equation.move_to(self.axes) + equation.to_edge(UP) + + self.play( + TransformFromCopy(y_ddot, new_y_ddot), + Write(equation[1:]), + FadeOut(self.graph), + FadeOut(self.y_label), + FadeOut(self.h_line), + FadeOut(self.v_graph), + FadeOut(self.graphed_v_vect), + FadeOut(self.graphed_a_vect), + ) + + self.equation = equation + + def solve_for_velocity(self): + axes = self.axes + equation = self.equation + v_graph = self.v_graph.deepcopy() + v_graph.clear_updaters() + v_start_point = v_graph.get_start() + origin = axes.coords_to_point(0, 0) + offset = v_start_point - origin + v_graph.shift(-offset) + + tex_question, answer1, answer2 = derivs = [ + TexMobject( + "{d", "(", *term, ")", "\\over", "dt}", "(t)", + "=", "-g", + tex_to_color_map={ + "-g": YELLOW, + "v_0": RED, + "?": RED, + } + ) + for term in [ + ("?", "?", "?", "?"), + ("-g", "t"), + ("-g", "t", "+", "v_0",), + ] + ] + for x in range(2): + answer1.submobjects.insert( + 4, VectorizedPoint(answer1[4].get_left()) + ) + for deriv in derivs: + deriv.next_to(equation, DOWN, MED_LARGE_BUFF) + + question = TextMobject( + "What function has slope $-g$?", + tex_to_color_map={"$-g$": YELLOW}, + ) + question.next_to(tex_question, DOWN) + question.set_stroke(BLACK, 5, background=True) + question.add_background_rectangle() + + v0_dot = Dot(v_start_point, color=PINK) + v0_label = TexMobject("v_0") + v0_label.set_color(RED) + v0_label.next_to(v0_dot, UR, buff=0) + + y_dot_equation = TexMobject( + "{\\dot y}", "(t)", "=", + "-g", "t", "+", "v_0", + tex_to_color_map={ + "{\\dot y}": RED, + "-g": YELLOW, + "v_0": RED, + } + ) + y_dot_equation.to_corner(UR) + + self.play( + FadeInFrom(tex_question, DOWN), + FadeInFrom(question, UP) + ) + self.wait() + self.add(v_graph, question) + self.play( + ReplacementTransform(tex_question, answer1), + ShowCreation(v_graph), + ) + self.wait() + self.play( + ReplacementTransform(answer1, answer2), + v_graph.shift, offset, + ) + self.play( + FadeInFromLarge(v0_dot), + FadeInFromDown(v0_label), + ) + self.wait() + self.play( + TransformFromCopy( + answer2[2:6], y_dot_equation[3:], + ), + Write(y_dot_equation[:3]), + equation.shift, LEFT, + ) + self.play( + FadeOut(question), + FadeOut(answer2), + ) + + self.remove(v_graph) + self.add(self.v_graph) + self.y_dot_equation = y_dot_equation + + def solve_for_position(self): + # Largely copied from above...not great + equation = self.equation + y_dot_equation = self.y_dot_equation + graph = self.graph + + all_terms = [ + ("?", "?", "?", "?"), + ("-", "(1/2)", "g", "t^2", "+", "v_0", "t"), + ("-", "(1/2)", "g", "t^2", "+", "v_0", "t", "+", "y_0"), + ] + tex_question, answer1, answer2 = derivs = [ + TexMobject( + "{d", "(", *term, ")", "\\over", "dt}", "(t)", + "=", + "-g", "t", "+", "v_0", + tex_to_color_map={ + "g": YELLOW, + "v_0": RED, + "?": BLUE, + "y_0": BLUE, + } + ) + for term in all_terms + ] + answer1.scale(0.8) + answer2.scale(0.8) + for deriv, terms in zip(derivs, all_terms): + for x in range(len(all_terms[-1]) - len(terms)): + n = 2 + len(terms) + deriv.submobjects.insert( + n, VectorizedPoint(deriv[n].get_left()) + ) + deriv.next_to( + VGroup(equation, y_dot_equation), + DOWN, MED_LARGE_BUFF + SMALL_BUFF + ) + deriv.shift_onto_screen() + deriv.add_background_rectangle_to_submobjects() + + y_equation = TexMobject( + "y", "(t)", "=", + "-", "(1/2)", "g", "t^2", + "+", "v_0", "t", + "+", "y_0", + tex_to_color_map={ + "y": BLUE, + "g": YELLOW, + "v_0": RED, + } + ) + y_equation.next_to( + VGroup(equation, y_dot_equation), + DOWN, MED_LARGE_BUFF, + ) + + self.play( + FadeInFrom(tex_question, DOWN), + ) + self.wait() + self.add(graph, tex_question) + self.play( + ReplacementTransform(tex_question, answer1), + ShowCreation(graph), + ) + self.add(graph, answer1) + self.wait() + self.play(ReplacementTransform(answer1, answer2)) + self.add(graph, answer2) + g_updaters = graph.updaters + graph.clear_updaters() + self.play( + graph.shift, 2 * DOWN, + rate_func=there_and_back, + run_time=2, + ) + graph.add_updater(g_updaters[0]) + self.wait() + br = BackgroundRectangle(y_equation) + self.play( + FadeIn(br), + ReplacementTransform( + answer2[2:11], + y_equation[3:] + ), + FadeIn(y_equation[:3]), + FadeOut(answer2[:2]), + FadeOut(answer2[11:]), + ) + self.play(ShowCreationThenFadeAround(y_equation)) + self.play_trajectory() + + # + def play_trajectory(self, *added_anims, **kwargs): + self.t_tracker.set_value(0) + self.play( + ApplyMethod( + self.t_tracker.set_value, 5, + rate_func=linear, + run_time=self.total_time, + ), + *added_anims, + ) + self.wait() + class ShowDerivativeVideo(Scene): def construct(self): @@ -1736,8 +2254,476 @@ class TwoBodiesWithZPart(TwoBodiesInSpace): body.velocity += (6 / 60) * OUT -class DefineODECopy(DefineODE): - pass +class LoveExample(PiCreatureScene): + def construct(self): + self.show_hearts() + self.add_love_trackers() + self.break_down_your_rule() + self.break_down_their_rule() + + def create_pi_creatures(self): + you = You() + you.shift(FRAME_WIDTH * LEFT / 4) + you.to_edge(DOWN) + + tau = TauCreature(color=GREEN) + tau.flip() + tau.shift(FRAME_WIDTH * RIGHT / 4) + tau.to_edge(DOWN) + + self.you = you + self.tau = tau + return (you, tau) + + def show_hearts(self): + you, tau = self.you, self.tau + hearts = VGroup() + n_hearts = 20 + for x in range(n_hearts): + heart = SuitSymbol("hearts") + heart.scale(0.5 + 2 * np.random.random()) + heart.shift(np.random.random() * 4 * RIGHT) + heart.shift(np.random.random() * 4 * UP) + hearts.add(heart) + hearts.move_to(2 * DOWN) + hearts.add_updater(lambda m, dt: m.shift(2 * dt * UP)) + + self.add(hearts) + self.play( + LaggedStartMap( + UpdateFromAlphaFunc, hearts, + lambda heart: ( + heart, + lambda h, a: h.set_opacity( + there_and_back(a) + ).shift(0.02 * UP) + ), + lag_ratio=0.01, + run_time=3, + suspend_mobject_updating=False, + ), + ApplyMethod( + you.change, 'hooray', tau.eyes, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ), + ApplyMethod( + tau.change, 'hooray', you.eyes, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ), + ) + self.remove(hearts) + self.wait() + + def add_love_trackers(self): + self.init_ps_point() + self.add_love_decimals() + self.add_love_number_lines() + self.tie_creature_state_to_ps_point() + + self.play(Rotating( + self.ps_point, + radians=-7 * TAU / 8, + about_point=ORIGIN, + run_time=10, + rate_func=linear, + )) + self.wait() + + def break_down_your_rule(self): + label1 = self.love_1_label + label2 = self.love_2_label + ps_point = self.ps_point + + up_arrow = Vector(UP, color=GREEN) + down_arrow = Vector(DOWN, color=RED) + for arrow in (up_arrow, down_arrow): + arrow.next_to(label1, RIGHT) + + self.play(GrowArrow(up_arrow)) + self.play( + self.tau.love_eyes.scale, 1.25, + self.tau.love_eyes.set_color, BLUE_C, + rate_func=there_and_back, + ) + self.play( + ps_point.shift, 6 * RIGHT, + run_time=2, + ) + self.wait() + ps_point.shift(13 * DOWN) + self.play( + FadeOut(up_arrow), + GrowArrow(down_arrow), + ) + self.play( + ps_point.shift, 11 * LEFT, + run_time=3, + ) + self.wait() + + # Derivative + equation = get_love_equation1() + equation.shift(0.5 * UP) + deriv, equals, a, h2 = equation + + self.play( + Write(deriv[:-1]), + Write(equals), + Write(a), + TransformFromCopy(label1[0], deriv.heart), + TransformFromCopy(label2[0], h2), + ) + self.wait() + self.play( + equation.scale, 0.5, + equation.to_corner, UL, + FadeOut(down_arrow) + ) + + def break_down_their_rule(self): + label1 = self.love_1_label + label2 = self.love_2_label + ps_point = self.ps_point + + up_arrow = Vector(UP, color=GREEN) + down_arrow = Vector(DOWN, color=RED) + for arrow in (up_arrow, down_arrow): + arrow.next_to(label2, RIGHT) + + # Derivative + equation = get_love_equation2() + equation.shift(0.5 * UP) + deriv, equals, mb, h1 = equation + + self.play( + Write(deriv[:-1]), + Write(equals), + Write(mb), + TransformFromCopy(label1[0], h1), + TransformFromCopy(label2[0], deriv.heart), + ) + + self.play(GrowArrow(up_arrow)) + self.play( + ps_point.shift, 13 * UP, + run_time=3, + ) + self.wait() + self.play( + ps_point.shift, 11 * RIGHT, + ) + self.play( + FadeOut(up_arrow), + GrowArrow(down_arrow), + ) + self.play( + ps_point.shift, 13 * DOWN, + run_time=3, + ) + self.wait() + + # + def init_ps_point(self): + self.ps_point = VectorizedPoint(np.array([5.0, 5.0, 0])) + + def get_love1(self): + return self.ps_point.get_location()[0] + + def get_love2(self): + return self.ps_point.get_location()[1] + + def set_loves(self, love1=None, love2=None): + if love1 is not None: + self.ps_point.set_x(love1) + if love2 is not None: + self.ps_point.set_x(love2) + + def add_love_decimals(self): + self.love_1_label = self.add_love_decimal( + 1, self.get_love1, self.you.get_color(), -3, + ) + self.love_2_label = self.add_love_decimal( + 2, self.get_love2, self.tau.get_color(), 3, + ) + + def add_love_decimal(self, index, value_func, color, x_coord): + d = DecimalNumber(include_sign=True) + d.add_updater(lambda d: d.set_value(value_func())) + + label = get_heart_var(index) + label.move_to(x_coord * RIGHT) + label.to_edge(UP) + eq = TexMobject("=") + eq.next_to(label, RIGHT, SMALL_BUFF) + eq.shift(SMALL_BUFF * UP) + d.next_to(eq, RIGHT, SMALL_BUFF) + + self.add(label, eq, d) + return VGroup(label, eq, d) + + def add_love_number_lines(self): + nl1 = NumberLine( + x_min=-8, + x_max=8, + unit_size=0.25, + tick_frequency=2, + number_scale_val=0.25, + ) + nl1.set_stroke(width=1) + nl1.next_to(self.love_1_label, DOWN) + nl1.add_numbers(*range(-6, 8, 2)) + + nl2 = nl1.copy() + nl2.next_to(self.love_2_label, DOWN) + + dot1 = Dot(color=self.you.get_color()) + dot1.add_updater(lambda d: d.move_to( + nl1.number_to_point(self.get_love1()) + )) + dot2 = Dot(color=self.tau.get_color()) + dot2.add_updater(lambda d: d.move_to( + nl2.number_to_point(self.get_love2()) + )) + + self.add(nl1, nl2, dot1, dot2) + + def get_love_eyes(self, eyes): + hearts = VGroup() + for eye in eyes: + heart = SuitSymbol("hearts") + heart.match_width(eye) + heart.move_to(eye) + heart.scale(1.25) + heart.set_stroke(BLACK, 1) + hearts.add(heart) + hearts.add_updater( + lambda m: m.move_to(eyes) + ) + return hearts + + def tie_creature_state_to_ps_point(self): + # Quite a mess, but I'm coding in a rush here... + you = self.you + you_copy = you.copy() + tau = self.tau + tau_copy = tau.copy() + + you.love_eyes = self.get_love_eyes(you.eyes) + tau.love_eyes = self.get_love_eyes(tau.eyes) + + self.add(you.love_eyes) + self.add(tau.love_eyes) + + you_height = you.get_height() + tau_height = tau.get_height() + + you_bottom = you.get_bottom() + tau_bottom = tau.get_bottom() + + def update_you(y): + love = self.get_love1() + + cutoff_values = [ + -5, -3, -1, 1, 3, 5 + ] + modes = [ + "angry", "sassy", "hesitant", + "plain", + "happy", "hooray", "surprised", + ] + + if love < cutoff_values[0]: + y.change(modes[0]) + elif love >= cutoff_values[-1]: + y.change(modes[-1]) + else: + i = 0 + while cutoff_values[i] < love: + i += 1 + m1 = modes[i - 1] + m2 = modes[i] + y.change(m1) + you_copy.change(m2) + for mob in y, you_copy: + mob.set_height(you_height) + mob.move_to(you_bottom, DOWN) + + alpha = inverse_interpolate( + cutoff_values[i - 1], + cutoff_values[i], + love, + ) + s_alpha = squish_rate_func(smooth, 0.25, 1)(alpha) + if s_alpha > 0: + y.align_data(you_copy) + f1 = y.family_members_with_points() + f2 = you_copy.family_members_with_points() + for sm1, sm2 in zip(f1, f2): + sm1.interpolate(sm1, sm2, s_alpha) + y.look_at(tau.eyes) + if love < -4: + y.look_at(LEFT_SIDE) + # y.move_to( + # you_bottom + 0.025 * love * RIGHT, DOWN, + # ) + + l_alpha = np.clip( + inverse_interpolate(5, 5.5, love), + 0, 1 + ) + y.eyes.set_opacity(1 - l_alpha) + y.love_eyes.set_opacity(l_alpha) + + return y + + def update_tau(t): + love = self.get_love2() + + cutoff_values = [ + -5, -1.7, 1.7, 5 + ] + modes = [ + "angry", "confused", "plain", + "hooray", "hooray" + ] + + if love < cutoff_values[0]: + t.change(modes[0]) + elif love >= cutoff_values[-1]: + t.change(modes[-1]) + else: + i = 0 + while cutoff_values[i] < love: + i += 1 + m1 = modes[i - 1] + m2 = modes[i] + t.change(m1) + tau_copy.change(m2) + for mob in t, tau_copy: + mob.set_height(tau_height) + mob.move_to(tau_bottom, DOWN) + + alpha = inverse_interpolate( + cutoff_values[i - 1], + cutoff_values[i], + love, + ) + s_alpha = squish_rate_func(smooth, 0.25, 1)(alpha) + if s_alpha > 0: + t.align_data(tau_copy) + f1 = t.family_members_with_points() + f2 = tau_copy.family_members_with_points() + for sm1, sm2 in zip(f1, f2): + sm1.interpolate(sm1, sm2, s_alpha) + # t.move_to( + # tau_bottom + 0.025 * love * LEFT, DOWN, + # ) + t.look_at(you.eyes) + if love < -4: + t.look_at(RIGHT_SIDE) + + l_alpha = np.clip( + inverse_interpolate(5, 5.5, love), + 0, 1 + ) + t.eyes.set_opacity(1 - l_alpha) + t.love_eyes.set_opacity(l_alpha) + + you.add_updater(update_you) + tau.add_updater(update_tau) + + self.pi_creatures = VGroup() + + +class ComparePhysicsToLove(Scene): + def construct(self): + ode = get_ode() + ode.to_edge(UP) + thetas = ode.get_parts_by_tex("theta") + + love = VGroup( + get_love_equation1(), + get_love_equation2(), + ) + love.scale(0.5) + love.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + love.move_to(DOWN) + hearts = VGroup(*filter( + lambda sm: isinstance(sm, SuitSymbol), + love.get_family() + )) + + arrow = DoubleArrow(love.get_top(), ode.get_bottom()) + + self.play(FadeInFrom(ode, DOWN)) + self.play(FadeInFrom(love, UP)) + self.wait() + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + thetas, + )) + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + hearts, + )) + self.wait() + self.play(ShowCreation(arrow)) + self.wait() + + +class FramesComparingPhysicsToLove(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + ode = get_ode() + ode.to_edge(UP) + + love = VGroup( + get_love_equation1(), + get_love_equation2(), + ) + love.scale(0.5) + love.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_color=BLACK, + fill_opacity=1, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + frames.shift(DOWN) + + animated_frames = AnimatedBoundary(frames) + + ode.next_to(frames[0], UP) + love.next_to(frames[1], UP) + + self.add(frames, animated_frames) + self.add(ode, love) + + self.wait(15) + + +class PassageOfTime(Scene): + def construct(self): + clock = Clock() + clock[0].set_color(BLUE) + clock.set_stroke(width=1) + clock.scale(0.8) + clock.to_corner(UL) + passage = ClockPassesTime( + clock, + hours_passed=48, + ) + self.play(passage, run_time=10) class WriteODESolvingCode(ExternallyAnimatedScene): From 5121248c258197b9aa8fa01a69cc054506fd85e6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 2 Apr 2019 13:55:58 -0700 Subject: [PATCH 13/18] Add supporter exception --- manimlib/for_3b1b_videos/common_scenes.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/manimlib/for_3b1b_videos/common_scenes.py b/manimlib/for_3b1b_videos/common_scenes.py index ee2e4d1a..c4cfa7f8 100644 --- a/manimlib/for_3b1b_videos/common_scenes.py +++ b/manimlib/for_3b1b_videos/common_scenes.py @@ -102,7 +102,7 @@ class PatreonThanks(Scene): patreon_logo = PatreonLogo() patreon_logo.to_edge(UP) - patrons = list(map(TextMobject, self.specific_patrons)) + patrons = list(map(TextMobject, self.specific_patronds)) num_groups = float(len(patrons)) / self.max_patron_group_size proportion_range = np.linspace(0, 1, num_groups + 1) indices = (len(patrons) * proportion_range).astype('int') @@ -213,7 +213,14 @@ class PatreonEndScreen(PatreonThanks, PiCreatureScene): underline.next_to(thanks, DOWN, SMALL_BUFF) thanks.add(underline) - patrons = VGroup(*list(map(TextMobject, self.specific_patrons))) + changed_patron_names = map( + self.modify_patron_name, + self.specific_patrons, + ) + patrons = VGroup(*map( + TextMobject, + changed_patron_names, + )) patrons.scale(self.patron_scale_val) for patron in patrons: if patron.get_width() > self.max_patron_width: @@ -252,6 +259,11 @@ class PatreonEndScreen(PatreonThanks, PiCreatureScene): self.add(columns, black_rect, line, thanks) self.wait(wait_time) + def modify_patron_name(self, name): + if name is "RedAgent14": + return "Brian Shepetofsky" + return name + class LogoGenerationTemplate(MovingCameraScene): def setup(self): From 4019a80c4b703ec913ce6a59918f2930b9c1890f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 2 Apr 2019 13:56:11 -0700 Subject: [PATCH 14/18] Final changes before de chapter 1 --- active_projects/ode/all_part1_scenes.py | 7 + active_projects/ode/part1/phase_space.py | 64 +++++++ active_projects/ode/part1/pi_scenes.py | 54 +++++- active_projects/ode/part1/staging.py | 203 ++++++++++++++++++++++ active_projects/ode/part1/wordy_scenes.py | 2 +- 5 files changed, 328 insertions(+), 2 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 3ab32a2a..6187244b 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -6,6 +6,7 @@ from active_projects.ode.part1.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part1" ALL_SCENE_CLASSES = [ + WhenChangeIsEasier, VectorFieldTest, IntroducePendulum, MultiplePendulumsOverlayed, @@ -30,10 +31,14 @@ ALL_SCENE_CLASSES = [ # Something... ShowSimpleTrajectory, SimpleProjectileEquation, + SimpleProjectileEquationVGraphFreedom, ShowGravityAcceleration, + UniversalGravityLawSymbols, + ExampleTypicalODE, AnalyzePendulumForce, ShowSineValues, BuildUpEquation, + AirResistanceBrace, ShowDerivativeVideo, SubtleAirCurrents, SimpleDampenedPendulum, @@ -50,6 +55,7 @@ ALL_SCENE_CLASSES = [ SoWhatIsThetaThen, ReallyHardToSolve, ReasonForSolution, + PhysicistPhaseSpace, GleickQuote, SpectrumOfStartingStates, WritePhaseFlow, @@ -97,4 +103,5 @@ ALL_SCENE_CLASSES = [ ChaosTitle, RevisitQuote, EndScreen, + Thumbnail, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index ea4ad0c9..1a6c7a8f 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -2007,3 +2007,67 @@ class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps): run_time=5, rate_func=linear, ) + + +class Thumbnail(IntroduceVectorField): + CONFIG = { + "vector_field_config": { + "delta_x": 1, + "delta_y": 1, + "max_magnitude": 5, + "length_func": lambda norm: 0.75 * sigmoid(norm), + } + } + + def construct(self): + self.initialize_plane() + self.initialize_vector_field() + + field = self.vector_field + field.set_stroke(width=5) + + title = TextMobject("Differential\\\\", "equations") + title.space_out_submobjects(0.8) + # title.scale(3) + title.set_width(FRAME_WIDTH - 3) + # title.to_edge(UP) + # title[1].to_edge(DOWN) + + subtitle = TextMobject("Studying the unsolvable") + subtitle.set_width(FRAME_WIDTH - 1) + subtitle.set_color(WHITE) + subtitle.to_edge(DOWN, buff=1) + + # title.center() + title.to_edge(UP, buff=1) + title.add(subtitle) + # title.set_stroke(BLACK, 15, background=True) + # title.add_background_rectangle_to_submobjects(opacity=0.5) + title.set_stroke(BLACK, 15, background=True) + subtitle.set_stroke(RED, 2, background=True) + # for part in title: + # part[0].set_fill(opacity=0.25) + # part[0].set_stroke(width=0) + black_parts = VGroup() + for mob in title.family_members_with_points(): + for sp in mob.get_subpaths(): + new_mob = VMobject() + new_mob.set_points(sp) + new_mob.set_fill(BLACK, 0.25) + new_mob.set_stroke(width=0) + black_parts.add(new_mob) + + for vect in field: + for mob in title.family_members_with_points(): + for p in [vect.get_start(), vect.get_end()]: + x, y = p[:2] + x0, y0 = mob.get_corner(DL)[:2] + x1, y1 = mob.get_corner(UR)[:2] + if x0 < x < x1 and y0 < y < y1: + vect.set_opacity(0.25) + vect.tip.set_stroke(width=0) + + self.add(self.plane) + self.add(field) + self.add(black_parts) + self.add(title) diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index 09066905..23a4232d 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -230,6 +230,58 @@ class ProveTeacherWrong(TeacherStudentsScene): self.wait(8) +class PhysicistPhaseSpace(PiCreatureScene): + def construct(self): + physy = self.pi_creature + name = TextMobject("Physicist") + name.scale(1.5) + name.to_corner(DL, buff=MED_SMALL_BUFF) + physy.next_to(name, UP, SMALL_BUFF) + VGroup(name, physy).shift_onto_screen() + + axes = Axes( + x_min=-1, + x_max=10, + y_min=-1, + y_max=7, + ) + axes.set_height(6) + axes.next_to(physy, RIGHT) + axes.to_edge(UP) + axes.set_stroke(width=1) + x_label = TextMobject("Position") + x_label.next_to(axes.x_axis.get_right(), UP) + y_label = TextMobject("Momentum") + y_label.next_to(axes.y_axis.get_top(), RIGHT) + + title = TextMobject("Phase space") + title.scale(1.5) + title.set_color(YELLOW) + title.move_to(axes) + + self.add(name, physy) + + self.play( + physy.change, "angry", + Write(axes), + FadeInFromDown(title) + ) + self.wait(2) + self.play( + GrowFromPoint(x_label, physy.get_corner(UR)), + physy.change, "raise_right_hand", + axes.x_axis.get_right() + ) + self.play( + GrowFromPoint(y_label, physy.get_corner(UR)), + physy.look_at, axes.y_axis.get_top(), + ) + self.wait(3) + + def create_pi_creature(self): + return PiCreature(color=GREY).to_corner(DL) + + class AskAboutActuallySolving(TeacherStudentsScene): def construct(self): ode = get_ode() @@ -239,7 +291,7 @@ class AskAboutActuallySolving(TeacherStudentsScene): self.student_says( "Yeah yeah, but how do\\\\" - "you acutally \\emph{solve} it?", + "you actually \\emph{solve} it?", student_index=1, target_mode="sassy", added_anims=[morty.change, "thinking"], diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 8fcb852a..1deb25b0 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -69,6 +69,20 @@ class ShowSquare(ShowRect): } +class WhenChangeIsEasier(Scene): + def construct(self): + pass + + +class AirResistanceBrace(Scene): + def construct(self): + brace = Brace(Line(ORIGIN, RIGHT), DOWN) + word = TextMobject("Air resistance") + word.next_to(brace, DOWN) + self.play(GrowFromCenter(brace), FadeInFrom(word, UP)) + self.wait() + + class PeriodFormula(Scene): def construct(self): formula = get_period_formula() @@ -1024,6 +1038,195 @@ class SimpleProjectileEquation(ShowGravityAcceleration): self.wait() +class SimpleProjectileEquationVGraphFreedom(SimpleProjectileEquation): + def construct(self): + self.add_axes() + self.setup_trajectory() + self.clear() + v_graph = self.v_graph + self.t_tracker.set_value(5) + v_graph.update() + v_graph.clear_updaters() + self.add(v_graph) + self.play(v_graph.shift, 5 * DOWN, run_time=2) + self.play(v_graph.shift, 5 * UP, run_time=2) + + +class UniversalGravityLawSymbols(Scene): + def construct(self): + x1_tex = "\\vec{\\textbf{x}}_1" + x2_tex = "\\vec{\\textbf{x}}_2" + a1_tex = "\\vec{\\textbf{a}}_1" + new_brown = interpolate_color(LIGHT_GREY, LIGHT_BROWN, 0.5) + law = TexMobject( + "F_1", "=", "m_1", a1_tex, "=", + "G", "m_1", "m_2", + "\\left({", x2_tex, "-", x1_tex, "\\over", + "||", x2_tex, "-", x1_tex, "||", "}\\right)", + "\\left({", "1", "\\over", + "||", x2_tex, "-", x1_tex, "||^2", "}\\right)", + tex_to_color_map={ + x1_tex: BLUE_C, + "m_1": BLUE_C, + x2_tex: new_brown, + "m_2": new_brown, + a1_tex: YELLOW, + } + ) + law.to_edge(UP) + + force = law[:4] + constants = law[4:8] + unit_vect = law[8:19] + inverse_square = law[19:] + parts = VGroup( + force, unit_vect, inverse_square + ) + + words = VGroup( + TextMobject("Force on\\\\mass 1"), + TextMobject("Unit vector\\\\towards mass 2"), + TextMobject("Inverse square\\\\law"), + ) + + self.add(law) + + braces = VGroup() + rects = VGroup() + for part, word in zip(parts, words): + brace = Brace(part, DOWN) + word.scale(0.8) + word.next_to(brace, DOWN) + rect = SurroundingRectangle(part) + rect.set_stroke(YELLOW, 1) + braces.add(brace) + rects.add(rect) + + self.play( + ShowCreationThenFadeOut(rects[0]), + GrowFromCenter(braces[0]), + FadeInFrom(words[0], UP) + ) + self.wait() + self.play( + ShowCreationThenFadeOut(rects[1]), + GrowFromCenter(braces[1]), + FadeInFrom(words[1], UP) + ) + self.wait() + self.play( + ShowCreationThenFadeOut(rects[2]), + TransformFromCopy(*braces[1:3]), + FadeInFrom(words[2], UP), + ) + self.wait() + + # Position derivative + v1_tex = "\\vec{\\textbf{v}}_1" + kw = { + "tex_to_color_map": { + x1_tex: BLUE_C, + v1_tex: RED, + } + } + x_deriv = TexMobject( + "{d", x1_tex, "\\over", "dt}", "=", v1_tex, **kw + ) + x_deriv.to_corner(UL) + v_deriv = TexMobject( + "{d", v1_tex, "\\over", "dt}", "=", **kw + ) + + # Make way + law.generate_target() + lt = law.target + lt.to_edge(RIGHT) + lt[6].fade(1) + lt[:6].align_to(lt[6], RIGHT) + lt[:3].fade(1) + v_deriv.next_to(lt[3], LEFT) + + self.play( + FadeInFromDown(x_deriv), + MoveToTarget(law), + braces[1:].align_to, lt, RIGHT, + MaintainPositionRelativeTo(words[1:], braces[1:]), + FadeOut(words[0]), + FadeOut(braces[0]), + ) + self.play(ShowCreationThenFadeAround(x_deriv)) + + self.play( + TransformFromCopy( + x_deriv.get_part_by_tex(v1_tex), + v_deriv.get_part_by_tex(v1_tex), + ), + Write(VGroup(*filter( + lambda m: m is not v_deriv.get_part_by_tex(v1_tex), + v_deriv, + ))) + ) + + x_parts = law.get_parts_by_tex(x1_tex) + self.play( + TransformFromCopy( + x_deriv.get_parts_by_tex(x1_tex), + x_parts.copy(), + remover=True, + path_arc=30 * DEGREES, + ) + ) + self.play( + LaggedStartMap( + ShowCreationThenFadeAround, + x_parts + ) + ) + self.wait() + + +class ExampleTypicalODE(TeacherStudentsScene): + def construct(self): + examples = VGroup( + TexMobject( + "{\\dot x}(t) = k{x}(t)", + tex_to_color_map={ + "{\\dot x}": BLUE, + "{x}": BLUE, + }, + ), + get_ode(), + TexMobject( + "{\\partial T", "\\over", "\\partial t} = ", + "{\\partial^2 T", "\\over", "\\partial x^2}", "+", + "{\\partial^2 T", "\\over", "\\partial y^2}", "+", + "{\\partial^2 T", "\\over", "\\partial z^2}", + tex_to_color_map={ + "T": RED, + } + ), + ) + examples[1].get_parts_by_tex("theta").set_color(GREEN) + examples.arrange(DOWN, buff=MED_LARGE_BUFF) + examples.to_edge(UP) + + self.play( + FadeInFrom(examples[0], UP), + self.teacher.change, "raise_right_hand", + ) + self.play( + FadeInFrom(examples[1], UP), + self.get_student_changes( + *3 * ["pondering"], + look_at_arg=examples, + ), + ) + self.play( + FadeInFrom(examples[2], UP) + ) + self.wait(5) + + class ShowDerivativeVideo(Scene): def construct(self): title = TextMobject("Essence of", "Calculus") diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py index 8529bc67..a6c53ed7 100644 --- a/active_projects/ode/part1/wordy_scenes.py +++ b/active_projects/ode/part1/wordy_scenes.py @@ -667,7 +667,7 @@ class JumpToThisPoint(Scene): class ChaosTitle(Scene): def construct(self): - title = TextMobject("Chaos theorey") + title = TextMobject("Chaos theory") title.scale(1.5) title.to_edge(UP) line = Line(LEFT, RIGHT) From 043e3986f688fc843bddceec6a7a0ab1b40691db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 2 Apr 2019 17:42:50 -0700 Subject: [PATCH 15/18] Changes to AnimatedBoundary --- manimlib/mobject/changing.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 0b58c280..1827f1ef 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -8,6 +8,9 @@ class AnimatedBoundary(VGroup): "colors": [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN], "max_stroke_width": 3, "cycle_rate": 0.5, + "back_and_forth": True, + "draw_rate_func": smooth, + "fade_rate_func": smooth, } def __init__(self, vmobject, **kwargs): @@ -37,12 +40,14 @@ class AnimatedBoundary(VGroup): vmobject = self.vmobject index = int(time % len(colors)) - alpha = smooth(time % 1) + alpha = time % 1 + draw_alpha = self.draw_rate_func(alpha) + fade_alpha = self.fade_rate_func(alpha) - if int(time) % 2 == 0: - bounds = (0, alpha) + if self.back_and_forth and int(time) % 1 == 0: + bounds = (1 - draw_alpha, 1) else: - bounds = (1 - alpha, 1) + bounds = (0, draw_alpha) self.full_family_become_partial(growing, vmobject, *bounds) growing.set_stroke(colors[index], width=msw) @@ -50,7 +55,7 @@ class AnimatedBoundary(VGroup): self.full_family_become_partial(fading, vmobject, 0, 1) fading.set_stroke( color=colors[index - 1], - width=(1 - alpha) * msw + width=(1 - fade_alpha) * msw ) self.total_time += dt From db6958462f92bb7c069e333efac734ff9fd6b433 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 2 Apr 2019 17:43:18 -0700 Subject: [PATCH 16/18] Better behavior for turn_animation_into_updater and cycle_animations --- manimlib/mobject/mobject_update_utils.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/manimlib/mobject/mobject_update_utils.py b/manimlib/mobject/mobject_update_utils.py index 767adf20..0f256f2c 100644 --- a/manimlib/mobject/mobject_update_utils.py +++ b/manimlib/mobject/mobject_update_utils.py @@ -77,16 +77,15 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs): def update(m, dt): run_time = animation.get_run_time() - alpha = np.clip( - animation.total_time / run_time, - 0, 1, - ) + time_ratio = animation.total_time / run_time if cycle: - animation.total_time = animation.total_time % run_time - elif alpha >= 1: - animation.finish() - m.remove_updater(update) - return + alpha = time_ratio % 1 + else: + alpha = np.clip(time_ratio, 0, 1) + if alpha >= 1: + animation.finish() + m.remove_updater(update) + return animation.interpolate(alpha) animation.update_mobjects(dt) animation.total_time += dt From 200c479759ffab4268582e783d63d0382f1d5e96 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 2 Apr 2019 17:43:58 -0700 Subject: [PATCH 17/18] Beginning heat equation animations, abstract fourier series circles scene implemented --- active_projects/ode/all_part2_scenes.py | 7 + active_projects/ode/part2/fourier_series.py | 188 ++++++++++++++++++++ active_projects/ode/part2/staging.py | 6 + 3 files changed, 201 insertions(+) create mode 100644 active_projects/ode/all_part2_scenes.py create mode 100644 active_projects/ode/part2/fourier_series.py create mode 100644 active_projects/ode/part2/staging.py diff --git a/active_projects/ode/all_part2_scenes.py b/active_projects/ode/all_part2_scenes.py new file mode 100644 index 00000000..76d702f1 --- /dev/null +++ b/active_projects/ode/all_part2_scenes.py @@ -0,0 +1,7 @@ +from active_projects.ode.part2.staging import * +from active_projects.ode.part2.fourier_series import * + +OUTPUT_DIRECTORY = "ode/part2" +ALL_SCENE_CLASSES = [ + CirclesDrawingWave, +] diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py new file mode 100644 index 00000000..58ce1559 --- /dev/null +++ b/active_projects/ode/part2/fourier_series.py @@ -0,0 +1,188 @@ +from big_ol_pile_of_manim_imports import * + + +class CirclesDrawing(Scene): + CONFIG = { + "n_circles": 4, + "big_radius": 2, + "colors": [ + BLUE_D, + BLUE_C, + BLUE_E, + GREY_BROWN, + ], + "circle_style": { + "stroke_width": 2, + }, + "base_frequency": 0.25 * TAU, + "center_point": ORIGIN, + } + + # + def get_freqs_and_radii(self): + raise Exception("Not implemented") + + def get_color_iterator(self): + return it.cycle(self.colors) + + def get_circles(self): + circles = VGroup() + color_iterator = self.get_color_iterator() + self.center_tracker = VectorizedPoint(self.center_point) + last_circle = None + for freq, radius in self.get_freqs_and_radii(): + if last_circle: + center_func = last_circle.get_start + else: + center_func = self.center_tracker.get_location + circle = self.get_circle( + radius=radius, + color=next(color_iterator), + freq=freq, + center_func=center_func + ) + circles.add(circle) + last_circle = circle + return circles + + def get_circle(self, radius, color, freq, center_func): + circle = Circle( + radius=radius, + color=color, + **self.circle_style, + ) + circle.radial_line = Line( + circle.get_center(), + circle.get_start(), + color=WHITE, + **self.circle_style, + ) + circle.add(circle.radial_line) + circle.freq = freq + circle.center_func = center_func + circle.add_updater(self.update_circle) + return circle + + def update_circle(self, circle, dt): + circle.rotate(circle.freq * dt) + circle.move_to(circle.center_func()) + return circle + + def get_circle_end_path(self, circles, color=YELLOW): + freqs = [c.freq for c in circles] + radii = [c.radius for c in circles] + total_time = TAU / self.base_frequency + center = circles[0].get_center() + + return ParametricFunction( + lambda t: center + reduce(op.add, [ + radius * rotate_vector(RIGHT, freq * t) + for freq, radius in zip(freqs, radii) + ]), + t_min=0, + t_max=total_time, + color=color, + step_size=0.005, + ) + + # TODO, this should be a general animated mobect + def get_drawn_path(self, circles, **kwargs): + path = self.get_circle_end_path(circles, **kwargs) + total_time = path.t_max + broken_path = CurvesAsSubmobjects(path) + broken_path.curr_time = 0 + + def update_path(path, dt): + alpha = (path.curr_time / total_time) + n_curves = len(path) + for a, sp in zip(np.linspace(0, 1, n_curves), path): + b = alpha - a + if b < 0: + width = 0 + else: + width = 2 * (1 - (b % 1)) + sp.set_stroke(YELLOW, width=width) + path.curr_time += dt + return path + + broken_path.add_updater(update_path) + return broken_path + + def get_y_component_wave(self, + circles, + left_x=1, + color=BLUE, + n_copies=2, + right_shift_rate=1.5): + path = self.get_circle_end_path(circles) + wave = ParametricFunction( + lambda t: op.add( + right_shift_rate * t * LEFT, + path.function(t)[1] * UP + ), + t_min=path.t_min, + t_max=path.t_max, + color=color, + ) + wave_copies = VGroup(*[ + wave.copy() + for x in range(n_copies) + ]) + wave_copies.arrange(RIGHT, buff=0) + top_point = wave_copies.get_top() + wave.creation = ShowCreation( + wave, + run_time=wave.t_max, + rate_func=linear, + ) + cycle_animation(wave.creation) + wave.add_updater(lambda m: m.shift( + (m.get_left()[0] - left_x) * LEFT + )) + + def update_wave_copies(wcs): + index = int(np.ceil(wave.creation.total_time / wave.t_max)) - 1 + wcs[:index].match_style(wave) + wcs[index:].set_stroke(width=0) + wcs.next_to(wave, RIGHT, buff=0) + wcs.align_to(top_point, UP) + wave_copies.add_updater(update_wave_copies) + + return VGroup(wave, wave_copies) + + def get_wave_y_line(self, circles, wave): + return DashedLine( + circles[-1].get_start(), + wave[0].get_end(), + ) + + +class CirclesDrawingWave(CirclesDrawing): + CONFIG = { + "n_circles": 4, + "center_point": 3 * LEFT, + } + + def construct(self): + circles = self.get_circles() + path = self.get_drawn_path(circles) + wave = self.get_y_component_wave(circles) + + # small_path = self.get_drawn_path(circles[:1]) + wave1 = self.get_y_component_wave(circles[:1], color=PINK) + wave2 = self.get_y_component_wave(circles[:2], color=RED) + # Why? + circles.update(-1 / self.camera.frame_rate) + # + h_line = always_redraw( + lambda: self.get_wave_y_line(circles, wave) + ) + self.add(circles, path, wave, h_line) + self.add(wave1, wave2) + self.wait(10) + + def get_freqs_and_radii(self): + return [ + (k * self.base_frequency, self.big_radius / k) + for k in range(1, 2 * self.n_circles + 1, 2) + ] diff --git a/active_projects/ode/part2/staging.py b/active_projects/ode/part2/staging.py new file mode 100644 index 00000000..2b8b4e67 --- /dev/null +++ b/active_projects/ode/part2/staging.py @@ -0,0 +1,6 @@ +from big_ol_pile_of_manim_imports import * + + +class NewSceneName(Scene): + def construct(self): + pass From 247c440d053def3fe5574b3221655931d471c557 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 3 Apr 2019 20:40:22 -0700 Subject: [PATCH 18/18] A few more Fourier series scenes --- active_projects/ode/all_part2_scenes.py | 14 +- active_projects/ode/part1/phase_space.py | 12 +- active_projects/ode/part2/fourier_series.py | 412 ++++++++++++++++++-- active_projects/ode/part2/heat_equation.py | 6 + active_projects/ode/part2/staging.py | 20 +- 5 files changed, 417 insertions(+), 47 deletions(-) create mode 100644 active_projects/ode/part2/heat_equation.py diff --git a/active_projects/ode/all_part2_scenes.py b/active_projects/ode/all_part2_scenes.py index 76d702f1..c39a02e1 100644 --- a/active_projects/ode/all_part2_scenes.py +++ b/active_projects/ode/all_part2_scenes.py @@ -1,7 +1,19 @@ from active_projects.ode.part2.staging import * from active_projects.ode.part2.fourier_series import * +from active_projects.ode.part2.heat_equation import * OUTPUT_DIRECTORY = "ode/part2" ALL_SCENE_CLASSES = [ - CirclesDrawingWave, + # Tests + FourierOfPiSymbol, + FourierOfPiSymbol5, + FourierOfTrebleClef, + # CirclesDrawingWave, + # Scenes for video + ExplainCircleAnimations, + FourierSeriesIntro, + FourierSeriesIntroBackground4, + FourierSeriesIntroBackground8, + FourierSeriesIntroBackground12, + FourierSeriesIntroBackground20, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 1a6c7a8f..85243172 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -2012,19 +2012,25 @@ class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps): class Thumbnail(IntroduceVectorField): CONFIG = { "vector_field_config": { - "delta_x": 1, - "delta_y": 1, + "delta_x": 0.5, + "delta_y": 0.5, "max_magnitude": 5, - "length_func": lambda norm: 0.75 * sigmoid(norm), + "length_func": lambda norm: 0.5 * sigmoid(norm), } } def construct(self): self.initialize_plane() + self.plane.axes.set_stroke(width=0.5) self.initialize_vector_field() field = self.vector_field field.set_stroke(width=5) + for vector in field: + vector.set_stroke(width=3) + vector.tip.set_stroke(width=0) + vector.tip.scale(1.5, about_point=vector.get_last_point()) + vector.set_opacity(1) title = TextMobject("Differential\\\\", "equations") title.space_out_submobjects(0.8) diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py index 58ce1559..8b22b4b3 100644 --- a/active_projects/ode/part2/fourier_series.py +++ b/active_projects/ode/part2/fourier_series.py @@ -1,9 +1,10 @@ from big_ol_pile_of_manim_imports import * +# import scipy -class CirclesDrawing(Scene): +class FourierCirclesScene(Scene): CONFIG = { - "n_circles": 4, + "n_circles": 10, "big_radius": 2, "colors": [ BLUE_D, @@ -14,38 +15,54 @@ class CirclesDrawing(Scene): "circle_style": { "stroke_width": 2, }, - "base_frequency": 0.25 * TAU, + "base_frequency": 1, + "slow_factor": 0.25, "center_point": ORIGIN, + "parametric_function_step_size": 0.001, } # - def get_freqs_and_radii(self): - raise Exception("Not implemented") + def get_freqs(self): + n = self.n_circles + all_freqs = list(range(n // 2, -n // 2, -1)) + all_freqs.sort(key=abs) + return all_freqs + + def get_coefficients(self): + return [complex(0) for x in range(self.n_circles)] def get_color_iterator(self): return it.cycle(self.colors) - def get_circles(self): + def get_circles(self, freqs=None, coefficients=None): circles = VGroup() color_iterator = self.get_color_iterator() self.center_tracker = VectorizedPoint(self.center_point) + + if freqs is None: + freqs = self.get_freqs() + if coefficients is None: + coefficients = self.get_coefficients() + last_circle = None - for freq, radius in self.get_freqs_and_radii(): + for freq, coefficient in zip(freqs, coefficients): if last_circle: center_func = last_circle.get_start else: center_func = self.center_tracker.get_location circle = self.get_circle( - radius=radius, - color=next(color_iterator), + coefficient=coefficient, freq=freq, - center_func=center_func + color=next(color_iterator), + center_func=center_func, ) circles.add(circle) last_circle = circle return circles - def get_circle(self, radius, color, freq, center_func): + def get_circle(self, coefficient, freq, color, center_func): + radius = abs(coefficient) + phase = np.log(coefficient).imag circle = Circle( radius=radius, color=color, @@ -59,41 +76,60 @@ class CirclesDrawing(Scene): ) circle.add(circle.radial_line) circle.freq = freq + circle.phase = phase + circle.rotate(phase) + circle.coefficient = coefficient circle.center_func = center_func circle.add_updater(self.update_circle) return circle def update_circle(self, circle, dt): - circle.rotate(circle.freq * dt) + circle.rotate( + self.slow_factor * circle.freq * dt * TAU + ) circle.move_to(circle.center_func()) return circle + def get_rotating_vectors(self, circles): + return VGroup(*[ + self.get_rotating_vector(circle) + for circle in circles + ]) + + def get_rotating_vector(self, circle): + vector = Vector(RIGHT, color=WHITE) + vector.add_updater(lambda v: v.put_start_and_end_on( + *circle.radial_line.get_start_and_end() + )) + return vector + def get_circle_end_path(self, circles, color=YELLOW): + coefs = [c.coefficient for c in circles] freqs = [c.freq for c in circles] - radii = [c.radius for c in circles] - total_time = TAU / self.base_frequency center = circles[0].get_center() - return ParametricFunction( + path = ParametricFunction( lambda t: center + reduce(op.add, [ - radius * rotate_vector(RIGHT, freq * t) - for freq, radius in zip(freqs, radii) + complex_to_R3( + coef * np.exp(TAU * 1j * freq * t) + ) + for coef, freq in zip(coefs, freqs) ]), t_min=0, - t_max=total_time, + t_max=1, color=color, - step_size=0.005, + step_size=self.parametric_function_step_size, ) + return path # TODO, this should be a general animated mobect def get_drawn_path(self, circles, **kwargs): path = self.get_circle_end_path(circles, **kwargs) - total_time = path.t_max broken_path = CurvesAsSubmobjects(path) broken_path.curr_time = 0 def update_path(path, dt): - alpha = (path.curr_time / total_time) + alpha = path.curr_time * self.slow_factor n_curves = len(path) for a, sp in zip(np.linspace(0, 1, n_curves), path): b = alpha - a @@ -111,9 +147,9 @@ class CirclesDrawing(Scene): def get_y_component_wave(self, circles, left_x=1, - color=BLUE, + color=PINK, n_copies=2, - right_shift_rate=1.5): + right_shift_rate=5): path = self.get_circle_end_path(circles) wave = ParametricFunction( lambda t: op.add( @@ -132,7 +168,7 @@ class CirclesDrawing(Scene): top_point = wave_copies.get_top() wave.creation = ShowCreation( wave, - run_time=wave.t_max, + run_time=(1 / self.slow_factor), rate_func=linear, ) cycle_animation(wave.creation) @@ -141,7 +177,9 @@ class CirclesDrawing(Scene): )) def update_wave_copies(wcs): - index = int(np.ceil(wave.creation.total_time / wave.t_max)) - 1 + index = int( + wave.creation.total_time * self.slow_factor + ) wcs[:index].match_style(wave) wcs[index:].set_stroke(width=0) wcs.next_to(wave, RIGHT, buff=0) @@ -154,35 +192,327 @@ class CirclesDrawing(Scene): return DashedLine( circles[-1].get_start(), wave[0].get_end(), + stroke_width=1, + dash_length=DEFAULT_DASH_LENGTH * 0.5, ) + # Computing Fourier series + def get_coefficients_of_path(self, path, n_samples=10000, freqs=None): + if freqs is None: + freqs = self.get_freqs() + dt = 1 / n_samples + ts = np.arange(0, 1, dt) + samples = np.array([ + path.point_from_proportion(t) + for t in ts + ]) + samples -= self.center_point + complex_samples = samples[:, 0] + 1j * samples[:, 1] -class CirclesDrawingWave(CirclesDrawing): + result = [] + for freq in freqs: + riemann_sum = np.array([ + np.exp(-TAU * 1j * freq * t) * cs + for t, cs in zip(ts, complex_samples) + ]).sum() * dt + result.append(riemann_sum) + + return result + + +class FourierSeriesIntroBackground4(FourierCirclesScene): CONFIG = { "n_circles": 4, - "center_point": 3 * LEFT, + "center_point": 4 * LEFT, + "run_time": 30, + "big_radius": 1.5, } def construct(self): circles = self.get_circles() path = self.get_drawn_path(circles) wave = self.get_y_component_wave(circles) - - # small_path = self.get_drawn_path(circles[:1]) - wave1 = self.get_y_component_wave(circles[:1], color=PINK) - wave2 = self.get_y_component_wave(circles[:2], color=RED) - # Why? - circles.update(-1 / self.camera.frame_rate) - # h_line = always_redraw( lambda: self.get_wave_y_line(circles, wave) ) - self.add(circles, path, wave, h_line) - self.add(wave1, wave2) - self.wait(10) - def get_freqs_and_radii(self): - return [ - (k * self.base_frequency, self.big_radius / k) - for k in range(1, 2 * self.n_circles + 1, 2) + # Why? + circles.update(-1 / self.camera.frame_rate) + # + self.add(circles, path, wave, h_line) + self.wait(self.run_time) + + def get_ks(self): + return np.arange(1, 2 * self.n_circles + 1, 2) + + def get_freqs(self): + return self.base_frequency * self.get_ks() + + def get_coefficients(self): + return self.big_radius / self.get_ks() + + +class FourierSeriesIntroBackground8(FourierSeriesIntroBackground4): + CONFIG = { + "n_circles": 8, + } + + +class FourierSeriesIntroBackground12(FourierSeriesIntroBackground4): + CONFIG = { + "n_circles": 12, + } + + +class FourierSeriesIntroBackground20(FourierSeriesIntroBackground4): + CONFIG = { + "n_circles": 20, + } + + +class FourierOfPiSymbol(FourierCirclesScene): + CONFIG = { + "n_circles": 50, + "center_point": ORIGIN, + "slow_factor": 0.1, + "run_time": 30, + "tex": "\\pi", + "start_drawn": False, + } + + def construct(self): + path = self.get_path() + coefs = self.get_coefficients_of_path(path) + + circles = self.get_circles(coefficients=coefs) + for k, circle in zip(it.count(1), circles): + circle.set_stroke(width=max( + 1 / np.sqrt(k), + 1, + )) + + # approx_path = self.get_circle_end_path(circles) + drawn_path = self.get_drawn_path(circles) + if self.start_drawn: + drawn_path.curr_time = 1 / self.slow_factor + + self.add(path) + self.add(circles) + self.add(drawn_path) + self.wait(self.run_time) + + def get_path(self): + tex_mob = TexMobject(self.tex) + tex_mob.set_height(6) + path = tex_mob.family_members_with_points()[0] + path.set_fill(opacity=0) + path.set_stroke(WHITE, 1) + return path + + +class FourierOfPiSymbol5(FourierOfPiSymbol): + CONFIG = { + "n_circles": 5, + "run_time": 10, + } + + +class FourierOfTrebleClef(FourierOfPiSymbol): + CONFIG = { + "n_circles": 100, + "run_time": 10, + "start_drawn": True, + } + + def get_path(self): + path = SVGMobject("TrebleClef") + path = path.family_members_with_points()[0] + path.set_height(7.5) + path.set_fill(opacity=0) + path.set_stroke(WHITE, 0) + return path + + +class ExplainCircleAnimations(FourierCirclesScene): + CONFIG = { + # "n_circles": 100, + "n_circles": 20, + "center_point": 2 * DOWN, + "n_top_circles": 9, + # "slow_factor": 0.1, + "path_height": 3, + } + + def construct(self): + self.add_path() + self.add_circles() + self.organize_circles_in_a_row() + self.show_frequencies() + self.show_examples_for_frequencies() + self.show_as_vectors() + self.show_vector_sum() + self.moons_of_moons_of_moons() + self.tweak_starting_vectors() + + def add_path(self): + self.path = self.get_path() + self.add(self.path) + + def add_circles(self): + coefs = self.get_coefficients_of_path(self.path) + self.circles = self.get_circles(coefficients=coefs) + + self.add(self.circles) + self.drawn_path = self.get_drawn_path(self.circles) + self.add(self.drawn_path) + + self.wait(8) + + def organize_circles_in_a_row(self): + circles = self.circles + top_circles = circles[:self.n_top_circles].deepcopy() + + center_trackers = VGroup() + for circle in top_circles: + tracker = VectorizedPoint(circle.center_func()) + circle.center_func = tracker.get_location + center_trackers.add(tracker) + tracker.freq = circle.freq + tracker.circle = circle + + center_trackers.submobjects.sort( + key=lambda m: m.freq + ) + center_trackers.generate_target() + right_buff = 1.45 + center_trackers.target.arrange(RIGHT, buff=right_buff) + center_trackers.target.to_edge(UP, buff=1.25) + + self.add(top_circles) + self.play( + MoveToTarget(center_trackers), + run_time=2 + ) + self.wait(4) + + self.top_circles = top_circles + self.center_trackers = center_trackers + + def show_frequencies(self): + center_trackers = self.center_trackers + + freq_numbers = VGroup() + for ct in center_trackers: + number = Integer(ct.freq) + number.next_to(ct, DOWN, buff=1) + freq_numbers.add(number) + ct.circle.number = number + + ld, rd = [ + TexMobject("\\dots") + for x in range(2) ] + ld.next_to(freq_numbers, LEFT, MED_LARGE_BUFF) + rd.next_to(freq_numbers, RIGHT, MED_LARGE_BUFF) + freq_numbers.add_to_back(ld) + freq_numbers.add(rd) + + freq_word = TextMobject("Frequencies") + freq_word.scale(1.5) + freq_word.set_color(YELLOW) + freq_word.next_to(freq_numbers, DOWN, MED_LARGE_BUFF) + + self.play( + LaggedStartMap( + FadeInFromDown, freq_numbers + ) + ) + self.play( + Write(freq_word), + LaggedStartMap( + ShowCreationThenFadeAround, freq_numbers, + ) + ) + self.wait(2) + + def show_examples_for_frequencies(self): + top_circles = self.top_circles + c1, c2, c3 = [ + list(filter( + lambda c: c.freq == k, + top_circles + ))[0] + for k in (1, 2, 3) + ] + + neg_circles = VGroup(*filter( + lambda c: c.freq < 0, + top_circles + )) + + for c in [c1, c2, c3, *neg_circles]: + c.rect = SurroundingRectangle(c) + + self.play( + ShowCreation(c2.rect), + WiggleOutThenIn(c2.number), + ) + self.wait(2) + self.play( + ReplacementTransform(c2.rect, c1.rect), + ) + self.play(FadeOut(c1.rect)) + self.wait() + self.play( + ShowCreation(c3.rect), + WiggleOutThenIn(c3.number), + ) + self.play( + FadeOut(c3.rect), + ) + self.wait(2) + self.play( + LaggedStart(*[ + ShowCreationThenFadeOut(c.rect) + for c in neg_circles + ]) + ) + self.wait(3) + + def show_as_vectors(self): + top_circles = self.top_circles + top_vectors = self.get_rotating_vectors(top_circles) + + self.play( + top_circles.set_stroke, {"width": 0.5}, + FadeIn(top_vectors), + ) + self.wait(3) + + self.top_vectors = top_vectors + + def show_vector_sum(self): + top_circles = self.top_circles + top_vectors = self.top_vectors + + self.play( + FadeOut(self.path), + FadeOut(self.circles), + ) + + def moons_of_moons_of_moons(self): + pass + + def tweak_starting_vectors(self): + pass + + # + def get_path(self): + tex = TexMobject("f") + path = tex.family_members_with_points()[0] + path.set_stroke(WHITE, 1) + path.set_fill(opacity=0) + path.set_height(self.path_height) + path.move_to(self.center_point) + return path + # return Square().set_height(3) diff --git a/active_projects/ode/part2/heat_equation.py b/active_projects/ode/part2/heat_equation.py new file mode 100644 index 00000000..2b8b4e67 --- /dev/null +++ b/active_projects/ode/part2/heat_equation.py @@ -0,0 +1,6 @@ +from big_ol_pile_of_manim_imports import * + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part2/staging.py b/active_projects/ode/part2/staging.py index 2b8b4e67..a4ceac3b 100644 --- a/active_projects/ode/part2/staging.py +++ b/active_projects/ode/part2/staging.py @@ -1,6 +1,22 @@ from big_ol_pile_of_manim_imports import * -class NewSceneName(Scene): +class FourierSeriesIntro(Scene): def construct(self): - pass + title = TextMobject( + "Fourier ", "Series", ":", + " An origin story", + arg_separator="", + ) + title.scale(2) + title.to_edge(UP) + image = ImageMobject("Joseph Fourier") + image.set_height(5) + image.next_to(title, DOWN, MED_LARGE_BUFF) + image.to_edge(LEFT) + name = TextMobject("Joseph", "Fourier") + name.next_to(image, DOWN) + + self.add(title) + self.add(image) + self.add(name)