From 9a452f3fee83591ea4d5763ad91b85a64a8beedf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2019 16:44:08 -0700 Subject: [PATCH] Up to 3 body problem illustration --- active_projects/ode/all_part1_scenes.py | 5 + active_projects/ode/part1/phase_space.py | 138 +++++++++++++-- active_projects/ode/part1/pi_scenes.py | 14 ++ .../ode/part1/shared_constructs.py | 19 +- active_projects/ode/part1/staging.py | 162 ++++++++++++++++++ active_projects/ode/part1/wordy_scenes.py | 121 +++++++++++++ 6 files changed, 441 insertions(+), 18 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 9f4592cb..2d2306b5 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -37,4 +37,9 @@ ALL_SCENE_CLASSES = [ ShowHighVelocityCase, TweakMuInFormula, TweakMuInVectorField, + FromODEToVectorField, + LorenzVectorField, + ThreeBodiesInSpace, + ThreeBodySymbols, + AskAboutActuallySolving, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index ffd722c7..3e7a0812 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -836,20 +836,17 @@ class IntroduceVectorField(VisualizeStates): # def get_vector_symbol(self, tex1, tex2): - return Matrix( - [[tex1], [tex2]], - include_background_rectangle=True, - bracket_h_buff=SMALL_BUFF, - bracket_v_buff=SMALL_BUFF, + t2c = { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\omega}": YELLOW, + "{\\ddot\\theta}": RED, + } + return get_vector_symbol( + tex1, tex2, element_to_mobject_config={ - "tex_to_color_map": { - "{\\theta}": BLUE, - "{\\dot\\theta}": YELLOW, - "{\\omega}": YELLOW, - "{\\ddot\\theta}": RED, - }, - }, - element_alignment_corner=ORIGIN, + "tex_to_color_map": t2c, + } ).scale(0.9) def vector_field_func(self, point): @@ -1001,9 +998,118 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): class TweakMuInFormula(Scene): def construct(self): - pass + ode = get_ode() + ode.to_edge(DOWN, buff=LARGE_BUFF) + mu = ode.get_part_by_tex("\\mu") + lil_rect = SurroundingRectangle(mu, buff=0.5 * SMALL_BUFF) + lil_rect.stretch(1.2, 1, about_edge=DOWN) + lil_rect.set_stroke(PINK, 2) + + interval = UnitInterval() + interval.add_numbers( + *np.arange(0, 1.2, 0.2) + ) + interval.next_to(ode, UP, LARGE_BUFF) + big_rect_seed = SurroundingRectangle(interval, buff=MED_SMALL_BUFF) + big_rect_seed.stretch(1.5, 1, about_edge=DOWN) + big_rect_seed.stretch(1.2, 0) + big_rect = VGroup(*[ + DashedLine(v1, v2) + for v1, v2 in adjacent_pairs(big_rect_seed.get_vertices()) + ]) + big_rect.set_stroke(PINK, 2) + + arrow = Arrow( + lil_rect.get_top(), + big_rect_seed.point_from_proportion(0.65), + buff=SMALL_BUFF, + ) + arrow.match_color(lil_rect) + + mu_tracker = ValueTracker(0.1) + get_mu = mu_tracker.get_value + + triangle = Triangle( + start_angle=-90 * DEGREES, + stroke_width=0, + fill_opacity=1, + fill_color=WHITE, + ) + triangle.set_height(0.2) + triangle.add_updater(lambda t: t.next_to( + interval.number_to_point(get_mu()), + UP, buff=0, + )) + + equation = VGroup( + TexMobject("\\mu = "), + DecimalNumber(), + ) + equation.add_updater( + lambda e: e.arrange(RIGHT).next_to( + triangle, UP, SMALL_BUFF, + ).shift(0.4 * RIGHT) + ) + equation[-1].add_updater( + lambda d: d.set_value(get_mu()).shift(0.05 * UL) + ) + + self.add(ode) + self.play(ShowCreation(lil_rect)) + self.play( + GrowFromPoint(interval, mu.get_center()), + GrowFromPoint(triangle, mu.get_center()), + GrowFromPoint(equation, mu.get_center()), + TransformFromCopy(lil_rect, big_rect), + ShowCreation(arrow) + ) + self.wait() + self.play(mu_tracker.set_value, 0.9, run_time=5) + self.wait() -class TweakMuInVectorField(Scene): +class TweakMuInVectorField(ShowPendulumPhaseFlow): def construct(self): - pass + self.initialize_plane() + plane = self.plane + self.add(plane) + + mu_tracker = ValueTracker(0.1) + get_mu = mu_tracker.get_value + + def vector_field_func(p): + x, y = plane.point_to_coords(p) + mu = get_mu() + g = self.big_pendulum_config.get("gravity") + L = self.big_pendulum_config.get("length") + return pendulum_vector_field_func( + x * RIGHT + y * UP, + mu=mu, g=g, L=L + ) + + def get_vector_field(): + return VectorField( + vector_field_func, + **self.vector_field_config, + ) + + field = always_redraw(get_vector_field) + self.add(field) + + self.play( + mu_tracker.set_value, 0.9, + run_time=5, + ) + field.suspend_updating() + + stream_lines = StreamLines( + field.func, + delta_x=0.3, + delta_y=0.3, + ) + animated_stream_lines = AnimatedStreamLines( + stream_lines, + line_anim_class=ShowPassingFlashWithThinningStrokeWidth, + ) + self.add(animated_stream_lines) + self.wait(self.flow_time) diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index bd181753..284ff054 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -195,3 +195,17 @@ class ProveTeacherWrong(TeacherStudentsScene): self.teacher.change, "maybe" ) self.wait(8) + + +class AskAboutActuallySolving(Scene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + morty = self.teacher + + self.student_says( + "Yeah, yeah, but how do\\\\" + "you acutally \\emph{solve} it?", + target_mode="sassy", + added_anims=[morty.change, "thinking"], + ) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 52ee9ca8..265a4585 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -20,12 +20,15 @@ def get_ode(): tex_config = { "tex_to_color_map": { "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, "{t}": WHITE, + "{\\mu}": WHITE, } } ode = TexMobject( - "\\ddot {\\theta}({t})", "=", - "-\\mu \\dot {\\theta}({t})", + "{\\ddot\\theta}({t})", "=", + "-{\\mu} {\\dot\\theta}({t})", "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", **tex_config, ) @@ -39,3 +42,15 @@ def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): -np.sqrt(g / L) * np.sin(theta) - mu * omega, 0, ]) + + +def get_vector_symbol(*texs, **kwargs): + config = { + "include_background_rectangle": True, + "bracket_h_buff": SMALL_BUFF, + "bracket_v_buff": SMALL_BUFF, + "element_alignment_corner": ORIGIN, + } + config.update(kwargs) + array = [[tex] for tex in texs] + return Matrix(array, **config) diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 78c4a23a..471586f4 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -841,6 +841,168 @@ class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): return system +class FromODEToVectorField(Scene): + def construct(self): + matrix_config = { + "bracket_v_buff": 2 * SMALL_BUFF, + "element_to_mobject_config": { + "tex_to_color_map": { + "x": GREEN, + "y": RED, + "z": BLUE, + }, + } + } + vect = get_vector_symbol( + "x(t)", "y(t)", "z(t)", + **matrix_config, + ) + d_vect = get_vector_symbol( + "\\sigma\\big(y(t) - x(t)\\big)", + "x(t)\\big(\\rho - z(t)\\big) - y(t)", + "x(t)y(t) - \\beta z(t)", + **matrix_config + ) + equation = VGroup( + TexMobject("d \\over dt").scale(1.5), + vect, + TexMobject("="), + d_vect + ) + equation.scale(0.8) + equation.arrange(RIGHT) + equation.to_edge(UP) + + arrow = Vector(DOWN, color=YELLOW) + arrow.next_to(equation, DOWN) + + self.add(equation) + self.play(ShowCreation(arrow)) + self.wait() + + +class LorenzVectorField(ExternallyAnimatedScene): + pass + + +class ThreeBodiesInSpace(SpecialThreeDScene): + CONFIG = { + "masses": [1, 2, 3], + "G": 0.5, + "play_time": 60, + } + + def construct(self): + self.add_axes() + self.add_bodies() + self.add_trajectories() + self.let_play() + + def add_axes(self): + axes = self.axes = self.get_axes() + self.add(axes) + + def add_bodies(self): + bodies = self.bodies = VGroup() + velocity_vectors = VGroup() + + for mass in self.masses: + body = self.get_sphere( + checkerboard_colors=[DARK_BROWN, DARK_BROWN], + stroke_width=0.1, + ) + body.mass = mass + body.set_width(0.2 * mass) + + point = np.dot( + 2 * (np.random.random(3) - 0.5), + [RIGHT, UP, OUT] + ) + velocity = normalize(np.cross(point, OUT)) + body.move_to(point) + body.velocity = velocity + body.add_updater(self.update_body) + + 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 + for body in bodies + ]) + average_momentum = reduce(op.add, [ + body.mass * body.velocity / total_mass + for body in bodies + ]) + for body in bodies: + body.shift(-center_of_mass) + body.velocity -= average_momentum + + def add_trajectories(self): + def update_trajectory(traj, dt): + new_point = traj.body.get_center() + if get_norm(new_point - traj.points[-1]) > 0.01: + traj.add_smooth_curve_to(new_point) + + for body in self.bodies: + traj = VMobject() + traj.body = body + traj.start_new_path(body.get_center()) + traj.set_stroke(WHITE, 1) + traj.add_updater(update_trajectory) + self.add(traj, body) + + def let_play(self): + self.move_camera( + phi=70 * DEGREES, + theta=-110 * DEGREES, + run_time=3, + ) + self.begin_ambient_camera_rotation() + for x in range(6): + self.wait(self.play_time / 6) + + # + def get_velocity_vector_mob(self, body): + def draw_vector(): + center = body.get_center() + vect = Arrow( + center, + center + body.velocity, + buff=0, + color=RED, + ) + vect.set_shade_in_3d(True) + return vect + # length = vect.get_length() + # if length > 2: + # vect.scale( + # 2 / length, + # about_point=vect.get_start(), + # ) + return always_redraw(draw_vector) + + def update_body(self, body, dt): + G = self.G + acceleration = np.zeros(3) + for body2 in self.bodies: + if body2 is body: + continue + diff = body2.get_center() - body.get_center() + m2 = body2.mass + R = get_norm(diff) + acceleration += G * m2 * diff / (R**3) + + body.shift(body.velocity * dt) + body.velocity += acceleration * dt + + class NewSceneName(Scene): def construct(self): pass diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py index e7ffae3c..6a966789 100644 --- a/active_projects/ode/part1/wordy_scenes.py +++ b/active_projects/ode/part1/wordy_scenes.py @@ -139,3 +139,124 @@ class SetAsideSeekingSolution(Scene): eyes.blink, rate_func=squish_rate_func(there_and_back) ) + + +class ThreeBodySymbols(Scene): + def construct(self): + self.init_coord_groups() + self.introduce_coord_groups() + self.count_coords() + + def init_coord_groups(self): + kwargs = { + "bracket_v_buff": 2 * SMALL_BUFF + } + positions = VGroup(*[ + get_vector_symbol(*[ + "{}_{}".format(s, i) + for s in "xyz" + ], **kwargs) + for i in range(1, 4) + ]) + velocities = VGroup(*[ + get_vector_symbol(*[ + "p^{}_{}".format(s, i) + for s in "xyz" + ], **kwargs) + for i in range(1, 4) + ]) + groups = VGroup(positions, velocities) + colors = [GREEN, RED, BLUE] + for group in groups: + for matrix in group: + matrix.coords = matrix.get_entries() + for coord, color in zip(matrix.coords, colors): + coord.set_color(color) + group.arrange(RIGHT) + groups.arrange(DOWN, buff=LARGE_BUFF) + groups.to_edge(LEFT) + + self.coord_groups = groups + + def introduce_coord_groups(self): + groups = self.coord_groups + x_group, p_group = groups + x_word = TextMobject("Positions") + p_word = TextMobject("Momenta") + words = VGroup(x_word, p_word) + for word, group in zip(words, groups): + word.next_to(group, UP) + + rect_groups = VGroup() + for group in groups: + rect_group = VGroup(*[ + SurroundingRectangle( + VGroup(*[ + tm.coords[i] + for tm in group + ]), + color=WHITE, + stroke_width=2, + ) + for i in range(3) + ]) + rect_groups.add(rect_group) + + self.play( + *[ + LaggedStartMap( + FadeInFrom, group, + lambda m: (m, UP), + run_time=1, + ) + for group in groups + ], + *map(FadeInFromDown, words), + ) + for rect_group in rect_groups: + self.play( + ShowCreationThenFadeOut( + rect_group, + lag_ratio=0.5, + ) + ) + self.wait() + + def count_coords(self): + coord_copies = VGroup() + for group in self.coord_groups: + for tex_mob in group: + for coord in tex_mob.coords: + coord_copy = coord.copy() + coord_copy.set_stroke( + WHITE, 2, background=True + ) + coord_copies.add(coord_copy) + + count = Integer() + count_word = TextMobject("18", "degrees \\\\ of freedom")[1] + count_group = VGroup(count, count_word) + count_group.arrange( + RIGHT, + aligned_edge=DOWN, + ) + count_group.scale(1.5) + count_group.next_to( + self.coord_groups, RIGHT, + aligned_edge=DOWN, + ) + count.add_updater( + lambda m: m.set_value(len(coord_copies)) + ) + count.add_updater( + lambda m: m.next_to(count_word[0][0], LEFT, aligned_edge=DOWN) + ) + + self.add(count_group) + self.play( + # ChangeDecimalToValue(count, len(coord_copies)), + ShowIncreasingSubsets(coord_copies), + run_time=1.5, + rate_func=linear, + ) + self.play(FadeOut(coord_copies))