diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index ebcb0d3d..8065580c 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -1,6 +1,8 @@ from active_projects.ode.part1.pendulum import * from active_projects.ode.part1.staging import * from active_projects.ode.part1.pi_scenes import * +from active_projects.ode.part1.phase_space import * +from active_projects.ode.part1.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part1" ALL_SCENE_CLASSES = [ @@ -14,9 +16,36 @@ ALL_SCENE_CLASSES = [ SomeOfYouWatching, SmallAngleApproximationTex, VeryLowAnglePendulum, - FollowThisThread, + FormulasAreLies, + TourOfDifferentialEquations, + # FollowThisThread, StrogatzQuote, # Something... ShowGravityAcceleration, + AnalyzePendulumForce, BuildUpEquation, + ShowDerivativeVideo, + SubtleAirCurrents, + DefineODE, + ODEvsPDEinFrames, + ProveTeacherWrong, + SetAsideSeekingSolution, + ReferencePiCollisionStateSpaces, + VisualizeStates, + IntroduceVectorField, + BreakingSecondOrderIntoTwoFirstOrder, + ShowPendulumPhaseFlow, + ShowHighVelocityCase, + TweakMuInFormula, + TweakMuInVectorField, + FromODEToVectorField, + LorenzVectorField, + ThreeBodiesInSpace, + AltThreeBodiesInSpace, + ThreeBodySymbols, + AskAboutActuallySolving, + WriteODESolvingCode, + TakeManyTinySteps, + InaccurateComputation, + HungerForExactness, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 9627b3b9..5feba2d6 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -41,7 +41,12 @@ class Pendulum(VGroup): "color": RED, }, "theta_label_height": 0.25, + "set_theta_label_height_cap": False, "n_steps_per_frame": 100, + "include_theta_label": True, + "include_velocity_vector": False, + "velocity_vector_multiple": 0.5, + "max_velocity_vector_length_to_length_ratio": 0.5, } def __init__(self, **kwargs): @@ -52,7 +57,10 @@ class Pendulum(VGroup): self.rotating_group = VGroup(self.rod, self.weight) self.create_dashed_line() self.create_angle_arc() - self.add_theta_label() + if self.include_theta_label: + self.add_theta_label() + if self.include_velocity_vector: + self.add_velocity_vector() self.set_theta(self.initial_theta) self.update() @@ -82,23 +90,36 @@ class Pendulum(VGroup): self.get_fixed_point() + self.length * DOWN, **self.dashed_line_config ) + line.add_updater( + lambda l: l.move_to(self.get_fixed_point(), UP) + ) self.add_to_back(line) def create_angle_arc(self): self.angle_arc = always_redraw(lambda: Arc( arc_center=self.get_fixed_point(), start_angle=-90 * DEGREES, - angle=self.get_theta(), + angle=self.get_arc_angle_theta(), **self.angle_arc_config, )) self.add(self.angle_arc) + def get_arc_angle_theta(self): + # Might be changed in certain scenes + return self.get_theta() + def add_velocity_vector(self): def make_vector(): omega = self.get_omega() theta = self.get_theta() + mvlr = self.max_velocity_vector_length_to_length_ratio + max_len = mvlr * self.rod.get_length() + vvm = self.velocity_vector_multiple + multiple = np.clip( + vvm * omega, -max_len, max_len + ) vector = Vector( - 0.5 * omega * RIGHT, + multiple * RIGHT, **self.velocity_vector_config, ) vector.rotate(theta, about_point=ORIGIN) @@ -110,17 +131,23 @@ class Pendulum(VGroup): return self def add_theta_label(self): - label = self.theta_label = TexMobject("\\theta") - label.set_height(self.theta_label_height) + self.theta_label = always_redraw(self.get_label) + self.add(self.theta_label) - def update_label(l): - top = self.get_fixed_point() - arc_center = self.angle_arc.point_from_proportion(0.5) - vect = arc_center - top - vect = normalize(vect) * (1 + self.theta_label_height) - l.move_to(top + vect) - label.add_updater(update_label) - self.add(label) + def get_label(self): + label = TexMobject("\\theta") + label.set_height(self.theta_label_height) + if self.set_theta_label_height_cap: + max_height = self.angle_arc.get_width() + if label.get_height() > max_height: + label.set_height(max_height) + top = self.get_fixed_point() + arc_center = self.angle_arc.point_from_proportion(0.5) + vect = arc_center - top + norm = get_norm(vect) + vect = normalize(vect) * (norm + self.theta_label_height) + label.move_to(top + vect) + return label # def get_theta(self): @@ -841,12 +868,13 @@ class VeryLowAnglePendulum(LowAnglePendulum): } -class BuildUpEquation(MovingCameraScene): +class AnalyzePendulumForce(MovingCameraScene): CONFIG = { "pendulum_config": { "length": 5, - "top_point": 3 * UP, - "initial_theta": 45 * DEGREES, + "top_point": 3.5 * UP, + "initial_theta": 60 * DEGREES, + "set_theta_label_height_cap": True, }, "g_vect_config": { "length_multiple": 0.25, @@ -857,25 +885,33 @@ class BuildUpEquation(MovingCameraScene): def construct(self): self.add_pendulum() + self.add_g_vect() self.show_constraint() self.break_g_vect_into_components() - self.show_angle_geometry() self.show_gsin_formula() self.show_acceleration_at_different_angles() + self.show_angle_geometry() self.ask_about_what_to_do() - self.show_velocity_and_position() - self.show_derivatives() - self.show_equation() - self.talk_about_sine_component() - self.add_air_resistance() + + self.emphasize_theta() + self.show_arc_length() + self.show_angular_velocity() + self.show_angular_acceleration() + self.circle_g_sin_formula() def add_pendulum(self): - self.pendulum = Pendulum(**self.pendulum_config) - self.add(self.pendulum) + pendulum = Pendulum(**self.pendulum_config) + theta_tracker = ValueTracker(pendulum.get_theta()) + pendulum.add_updater(lambda p: p.set_theta( + theta_tracker.get_value() + )) - def show_constraint(self): + self.add(pendulum) + self.pendulum = pendulum + self.theta_tracker = theta_tracker + + def add_g_vect(self): pendulum = self.pendulum - weight = pendulum.weight g_vect = self.g_vect = GravityVector( pendulum, **self.g_vect_config, @@ -887,24 +923,20 @@ class BuildUpEquation(MovingCameraScene): g_vect, RIGHT, buff=-SMALL_BUFF, )) - theta_tracker = ValueTracker(pendulum.get_theta()) - - p = weight.get_center() - path = CubicBezier([p, p + 3 * DOWN, p + 3 * UP, p]) - - g_word.suspend_updating() self.play( GrowArrow(g_vect), FadeInFrom(g_word, UP, lag_ratio=0.1), ) - g_word.resume_updating() + + 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() - pendulum.add_updater(lambda p: p.set_theta( - theta_tracker.get_value() - )) arcs = VGroup() for u in [-1, 2, -1]: d_theta = 40 * DEGREES * u @@ -914,17 +946,17 @@ class BuildUpEquation(MovingCameraScene): radius=pendulum.length, arc_center=pendulum.get_fixed_point(), stroke_width=2, - stroke_color=RED, + stroke_color=GREEN, stroke_opacity=0.5, ) self.play( - theta_tracker.increment_value, d_theta, + self.theta_tracker.increment_value, d_theta, ShowCreation(arc) ) arcs.add(arc) - pendulum.clear_updaters() self.wait() - self.play(FadeOut(arc)) + + self.traced_arcs = arcs def break_g_vect_into_components(self): g_vect = self.g_vect @@ -945,40 +977,691 @@ class BuildUpEquation(MovingCameraScene): color=self.perp_line_color, )) - self.play( - ShowCreation(g_vect.component_lines), - ) + self.play(ShowCreation(g_vect.component_lines)) self.play(GrowArrow(g_vect.tangent)) self.wait() self.play(GrowArrow(g_vect.perp)) self.wait() - def show_angle_geometry(self): - g_vect = self.g_vect - def show_gsin_formula(self): - pass + g_vect = self.g_vect + g_word = self.g_word + g_word.clear_updaters() + + g_term = self.g_term = TexMobject("-g") + g_term.add_updater(lambda m: m.next_to( + g_vect, + RIGHT if self.pendulum.get_theta() >= 0 else LEFT, + SMALL_BUFF + )) + + def create_vect_label(vect, tex, direction): + label = TexMobject(tex) + label.set_stroke(width=0, background=True) + label.add_background_rectangle() + label.scale(0.7) + max_width = 0.9 * vect.get_length() + if label.get_width() > max_width: + label.set_width(max_width) + angle = vect.get_angle() + angle = (angle + PI / 2) % PI - PI / 2 + label.next_to(ORIGIN, direction, SMALL_BUFF) + label.rotate(angle, about_point=ORIGIN) + label.shift(vect.get_center()) + return label + + g_sin_label = always_redraw(lambda: create_vect_label( + g_vect.tangent, "-g\\sin(\\theta)", UP, + )) + g_cos_label = always_redraw(lambda: create_vect_label( + g_vect.perp, "-g\\cos(\\theta)", DOWN, + )) + + self.play( + ReplacementTransform(g_word[0][0], g_term[0][1]), + FadeOut(g_word[0][1:]), + Write(g_term[0][0]), + ) + self.add(g_term) + self.wait() + for label in g_sin_label, g_cos_label: + self.play( + GrowFromPoint(label[0], g_term.get_center()), + TransformFromCopy(g_term, label[1][:2]), + GrowFromPoint(label[1][2:], g_term.get_center()), + remover=True + ) + self.add(label) + self.wait() + + self.g_sin_label = g_sin_label + self.g_cos_label = g_cos_label def show_acceleration_at_different_angles(self): - pass + 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)) + + get_theta = self.pendulum.get_theta + theta_decimal = DecimalNumber(include_sign=True) + theta_decimal.add_updater(lambda d: d.set_value( + get_theta() + )) + theta_decimal.add_updater(lambda m: m.next_to( + self.pendulum.theta_label, DOWN + )) + theta_decimal.add_updater(lambda m: m.set_color( + GREEN if get_theta() > 0 else RED + )) + + 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.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 + g_sin_label = self.g_sin_label + angle = g_vect.tangent.get_angle() + angle = (angle - PI) % TAU + + randy = You() + randy.to_corner(DL) + bubble = randy.get_bubble( + height=2, + width=3.5, + ) + g_sin_copy = g_sin_label.copy() + g_sin_copy.remove(g_sin_copy[0]) + g_sin_copy.generate_target() + g_sin_copy.target.scale(1 / 0.75) + g_sin_copy.target.rotate(-angle) + a_eq = TexMobject("a=") + thought_term = VGroup(a_eq, g_sin_copy.target) + thought_term.arrange(RIGHT, buff=SMALL_BUFF) + thought_term.move_to(bubble.get_bubble_center()) + + rect = SurroundingRectangle(g_sin_copy.target) + rect.rotate(angle) + rect.move_to(g_sin_label) + + randy.save_state() + randy.fade(1) + self.play(randy.restore, randy.change, "pondering") + self.play(ShowCreationThenFadeOut(rect)) + self.play( + ShowCreation(bubble), + Write(a_eq), + MoveToTarget(g_sin_copy), + randy.look_at, bubble, + ) + thought_term.remove(g_sin_copy.target) + thought_term.add(g_sin_copy) + self.play(Blink(randy)) + self.wait() + self.play( + ShowCreationThenDestruction( + thought_term.copy().set_style( + stroke_color=YELLOW, + stroke_width=2, + fill_opacity=0, + ), + run_time=2, + lag_ratio=0.2, + ), + randy.change, "confused", thought_term, + ) + self.play(Blink(randy)) + self.play( + FadeOut(randy), + FadeOut(bubble), + thought_term.next_to, self.pendulum, DOWN, LARGE_BUFF + ) + + self.accleration_equation = thought_term + + def emphasize_theta(self): + pendulum = self.pendulum + + 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() + self.wait(5) + pendulum.clear_updaters() + for updater in old_updaters: + pendulum.add_updater(updater) + + 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 - def show_velocity_and_position(self): + def show_angular_acceleration(self): pass + def circle_g_sin_formula(self): + self.play( + ShowCreationThenFadeAround( + self.accleration_equation + ) + ) + + # + def set_theta(self, value, *added_anims, **kwargs): + kwargs["run_time"] = kwargs.get("run_time", 2) + self.play( + self.theta_tracker.set_value, value, + *added_anims, + **kwargs, + ) + + +class BuildUpEquation(Scene): + CONFIG = { + "tex_config": { + "tex_to_color_map": { + "{a}": YELLOW, + "{v}": RED, + "{x}": GREEN, + "\\theta": BLUE, + "{L}": WHITE, + } + } + } + + def construct(self): + # self.add_center_line() + self.show_derivatives() + self.show_theta_double_dot_equation() + self.talk_about_sine_component() + self.add_air_resistance() + + def add_center_line(self): + line = Line(UP, DOWN) + line.set_height(FRAME_HEIGHT) + line.set_stroke(WHITE, 1) + self.add(line) + def show_derivatives(self): - pass + a_eq = TexMobject( + "{a}", "=", "{d{v} \\over dt}", + **self.tex_config, + ) + v_eq = TexMobject( + "{v}", "=", "{d{x} \\over dt}", + **self.tex_config, + ) + x_eq = TexMobject( + "{x} = {L} \\theta", + **self.tex_config, + ) + eqs = VGroup(a_eq, v_eq, x_eq) + eqs.arrange(DOWN, buff=LARGE_BUFF) + eqs.to_corner(UL) - def show_equation(self): - pass + v_rhs = TexMobject( + "={L}{d\\theta \\over dt}", + "=", "{L}\\dot{\\theta}", + **self.tex_config, + ) + + v_rhs.next_to(v_eq, RIGHT, SMALL_BUFF) + v_rhs.shift( + UP * (v_eq[1].get_bottom()[1] - v_rhs[0].get_bottom()[1]) + ) + a_rhs = TexMobject( + "={L}{d", "\\dot{\\theta}", "\\over dt}", + "=", "{L}\\ddot{\\theta}", + **self.tex_config, + ) + a_rhs.next_to(a_eq, RIGHT, SMALL_BUFF) + a_rhs.shift( + UP * (a_eq[1].get_bottom()[1] - a_rhs[0].get_bottom()[1]) + ) + + # a_eq + self.play(Write(a_eq)) + self.wait() + + # v_eq + self.play( + TransformFromCopy( + a_eq.get_part_by_tex("{v}"), + v_eq.get_part_by_tex("{v}"), + ) + ) + self.play(TransformFromCopy(v_eq[:1], v_eq[1:])) + self.wait() + + # x_eq + self.play( + TransformFromCopy( + v_eq.get_part_by_tex("{x}"), + x_eq.get_part_by_tex("{x}"), + ) + ) + self.play(Write(x_eq[1:])) + self.wait() + for tex in "L", "\\theta": + self.play(ShowCreationThenFadeAround( + x_eq.get_part_by_tex(tex) + )) + self.wait() + + # v_rhs + self.play(*[ + TransformFromCopy( + x_eq.get_part_by_tex(tex), + v_rhs.get_part_by_tex(tex), + ) + for tex in ("=", "{L}", "\\theta") + ]) + self.play( + TransformFromCopy(v_eq[-3], v_rhs[2]), + TransformFromCopy(v_eq[-1], v_rhs[4]), + ) + self.wait() + self.play( + Write(v_rhs[-5]), + TransformFromCopy(*v_rhs.get_parts_by_tex("{L}")), + TransformFromCopy(v_rhs[3:4], v_rhs[-3:]) + ) + self.wait() + self.play(ShowCreationThenFadeAround(v_rhs[2:4])) + self.play(ShowCreationThenFadeAround(v_rhs[4])) + self.wait() + + # a_rhs + self.play(*[ + TransformFromCopy( + v_rhs.get_parts_by_tex(tex)[-1], + a_rhs.get_part_by_tex(tex), + ) + for tex in ("=", "{L}", "\\theta", "\\dot") + ]) + self.play( + TransformFromCopy(a_eq[-3], a_rhs[2]), + TransformFromCopy(a_eq[-1], a_rhs[6]), + ) + self.wait() + self.play( + Write(a_rhs[-5]), + TransformFromCopy(*a_rhs.get_parts_by_tex("{L}")), + TransformFromCopy(a_rhs[3:4], a_rhs[-3:]), + ) + self.wait() + + self.equations = VGroup( + a_eq, v_eq, x_eq, + v_rhs, a_rhs, + ) + + def show_theta_double_dot_equation(self): + equations = self.equations + a_deriv = equations[0] + a_rhs = equations[-1][-5:].copy() + + shift_vect = 1.5 * DOWN + + equals = TexMobject("=") + equals.rotate(90 * DEGREES) + equals.next_to(a_deriv[0], UP, MED_LARGE_BUFF) + g_sin_eq = TexMobject( + "-", "g", "\\sin", "(", "\\theta", ")", + **self.tex_config, + ) + g_sin_eq.next_to( + equals, UP, + buff=MED_LARGE_BUFF, + aligned_edge=LEFT, + ) + g_sin_eq.to_edge(LEFT) + g_sin_eq.shift(shift_vect) + + shift_vect += ( + g_sin_eq[1].get_center() - + a_deriv[0].get_center() + )[0] * RIGHT + + equals.shift(shift_vect) + a_rhs.shift(shift_vect) + + self.play( + equations.shift, shift_vect, + Write(equals), + GrowFromPoint( + g_sin_eq, 2 * RIGHT + 3 * DOWN + ) + ) + self.wait() + self.play( + a_rhs.next_to, g_sin_eq, RIGHT, + a_rhs.shift, SMALL_BUFF * UP, + ) + self.wait() + + # Fade equations + self.play( + FadeOut(equals), + equations.shift, DOWN, + equations.fade, 0.5, + ) + + # Rotate sides + equals, L, ddot, theta, junk = a_rhs + L_dd_theta = VGroup(L, ddot, theta) + minus, g, sin, lp, theta2, rp = g_sin_eq + m2, g2, over, L2 = frac = TexMobject("-", "{g", "\\over", "L}") + frac.next_to(equals, RIGHT) + + self.play( + L_dd_theta.next_to, equals, LEFT, + L_dd_theta.shift, SMALL_BUFF * UP, + g_sin_eq.next_to, equals, RIGHT, + path_arc=PI / 2, + ) + self.play( + ReplacementTransform(g, g2), + ReplacementTransform(minus, m2), + ReplacementTransform(L, L2), + Write(over), + g_sin_eq[2:].next_to, over, RIGHT, SMALL_BUFF, + ) + self.wait() + + # Surround + rect = SurroundingRectangle(VGroup(g_sin_eq, frac, ddot)) + rect.stretch(1.1, 0) + dashed_rect = DashedVMobject( + rect, num_dashes=50, positive_space_ratio=1, + ) + dashed_rect.shuffle() + dashed_rect.save_state() + dashed_rect.space_out_submobjects(1.1) + for piece in dashed_rect: + piece.rotate(90 * DEGREES) + dashed_rect.fade(1) + self.play(Restore(dashed_rect, lag_ratio=0.05)) + dashed_rect.generate_target() + dashed_rect.target.space_out_submobjects(0.9) + dashed_rect.target.fade(1) + for piece in dashed_rect.target: + piece.rotate(90 * DEGREES) + self.play(MoveToTarget( + dashed_rect, + lag_ratio=0.05, + remover=True + )) + self.wait() + + self.main_equation = VGroup( + ddot, theta, equals, + m2, L2, over, g2, + sin, lp, theta2, rp, + ) def talk_about_sine_component(self): - pass + main_equation = self.main_equation + gL_part = main_equation[4:7] + sin_part = main_equation[7:] + sin = sin_part[0] + + morty = Mortimer(height=1.5) + morty.next_to(sin, DR, buff=LARGE_BUFF) + morty.add_updater(lambda m: m.look_at(sin)) + + self.play(ShowCreationThenFadeAround(gL_part)) + self.wait() + self.play(ShowCreationThenFadeAround(sin_part)) + self.wait() + + self.play(FadeIn(morty)) + sin.save_state() + self.play( + morty.change, "angry", + sin.next_to, morty, LEFT, {"aligned_edge": UP}, + ) + self.play(Blink(morty)) + morty.clear_updaters() + self.play( + morty.change, "concerned_musician", + morty.look, DR, + ) + self.play(Restore(sin)) + self.play(FadeOut(morty)) + self.wait() + + # Emphasize theta as input + theta = sin_part[2] + arrow = Vector(0.5 * UP, color=WHITE) + arrow.next_to(theta, DOWN, SMALL_BUFF) + word = TextMobject("Input") + word.next_to(arrow, DOWN) + + self.play( + FadeInFrom(word, UP), + GrowArrow(arrow) + ) + self.play( + ShowCreationThenDestruction( + theta.copy().set_style( + fill_opacity=0, + stroke_width=2, + stroke_color=YELLOW, + ), + lag_ratio=0.1, + ) + ) + self.play(FadeOut(arrow), FadeOut(word)) def add_air_resistance(self): - pass + main_equation = self.main_equation + tdd_eq = main_equation[:3] + rhs = main_equation[3:] + + new_term = TexMobject( + "-", "\\mu", "\\dot{", "\\theta}", + ) + new_term.set_color_by_tex("\\theta", BLUE) + new_term.move_to(main_equation) + new_term.shift(0.5 * SMALL_BUFF * UP) + new_term[0].align_to(rhs[0], UP) + + brace = Brace(new_term, DOWN) + words = brace.get_text("Air resistance") + + self.play( + FadeInFromDown(new_term), + tdd_eq.next_to, new_term, LEFT, + tdd_eq.align_to, tdd_eq, UP, + rhs.next_to, new_term, RIGHT, + rhs.align_to, rhs, UP, + ) + self.play( + GrowFromCenter(brace), + Write(words) + ) + self.wait() class NewSceneName(Scene): diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py new file mode 100644 index 00000000..baf80d8d --- /dev/null +++ b/active_projects/ode/part1/phase_space.py @@ -0,0 +1,1239 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * +from active_projects.ode.part1.pendulum import Pendulum + + +# TODO: Arguably separate the part showing many +# configurations with the part showing just one. +class VisualizeStates(Scene): + CONFIG = { + "coordinate_plane_config": { + "y_line_frequency": PI / 2, + # "x_line_frequency": PI / 2, + "x_line_frequency": 1, + "y_axis_config": { + # "unit_size": 1.75, + "unit_size": 1, + }, + "y_max": 4, + "faded_line_ratio": 4, + "background_line_style": { + "stroke_width": 1, + }, + }, + "little_pendulum_config": { + "length": 1, + "gravity": 4.9, + "weight_diameter": 0.3, + "include_theta_label": False, + "include_velocity_vector": True, + "angle_arc_config": { + "radius": 0.2, + }, + "velocity_vector_config": { + "max_tip_length_to_length_ratio": 0.35, + "max_stroke_width_to_length_ratio": 6, + }, + "velocity_vector_multiple": 0.25, + "max_velocity_vector_length_to_length_ratio": 0.8, + }, + "big_pendulum_config": { + "length": 1.6, + "gravity": 4.9, + "damping": 0.2, + "weight_diameter": 0.3, + "include_velocity_vector": True, + "angle_arc_config": { + "radius": 0.5, + }, + "initial_theta": 80 * DEGREES, + "omega": -1, + "set_theta_label_height_cap": True, + }, + "n_thetas": 11, + "n_omegas": 7, + # "n_thetas": 5, + # "n_omegas": 3, + "initial_grid_wait_time": 15, + } + + def construct(self): + self.initialize_plane() + + simple = False + if simple: + self.add(self.plane) + else: + self.initialize_grid_of_states() + self.show_all_states_evolving() + self.show_grid_of_states_creation() + self.collapse_grid_into_points() + self.show_state_with_pair_of_numbers() + self.show_acceleration_dependence() + self.show_evolution_from_a_start_state() + + def initialize_grid_of_states(self): + pendulums = VGroup() + rects = VGroup() + state_grid = VGroup() + thetas = self.get_initial_thetas() + omegas = self.get_initial_omegas() + for omega in omegas: + row = VGroup() + for theta in thetas: + rect = Rectangle( + height=3, + width=3, + stroke_color=WHITE, + stroke_width=2, + fill_color=DARKER_GREY, + fill_opacity=1, + ) + pendulum = Pendulum( + initial_theta=theta, + omega=omega, + top_point=rect.get_center(), + **self.little_pendulum_config, + ) + pendulum.add_velocity_vector() + pendulums.add(pendulum) + rects.add(rect) + state = VGroup(rect, pendulum) + state.rect = rect + state.pendulum = pendulum + row.add(state) + row.arrange(RIGHT, buff=0) + state_grid.add(row) + state_grid.arrange(UP, buff=0) + + state_grid.set_height(FRAME_HEIGHT) + state_grid.center() + state_grid.save_state(use_deepcopy=True) + + self.state_grid = state_grid + self.pendulums = pendulums + + def initialize_plane(self): + plane = self.plane = NumberPlane( + **self.coordinate_plane_config + ) + plane.axis_labels = VGroup( + plane.get_x_axis_label( + "\\theta", RIGHT, UL, buff=SMALL_BUFF + ), + plane.get_y_axis_label( + "\\dot \\theta", UP, DR, buff=SMALL_BUFF + ).set_color(YELLOW), + ) + for label in plane.axis_labels: + label.add_background_rectangle() + plane.add(plane.axis_labels) + + plane.y_axis.add_numbers(direction=DL) + + x_axis = plane.x_axis + label_texs = ["\\pi \\over 2", "\\pi", "3\\pi \\over 2", "\\tau"] + values = [PI / 2, PI, 3 * PI / 2, TAU] + x_axis.coordinate_labels = VGroup() + x_axis.add(x_axis.coordinate_labels) + for value, label_tex in zip(values, label_texs): + for u in [-1, 1]: + tex = label_tex + if u < 0: + tex = "-" + tex + label = TexMobject(tex) + label.scale(0.5) + if label.get_height() > 0.4: + label.set_height(0.4) + point = x_axis.number_to_point(u * value) + label.next_to(point, DR, SMALL_BUFF) + x_axis.coordinate_labels.add(label) + return plane + + def show_all_states_evolving(self): + state_grid = self.state_grid + pendulums = self.pendulums + + for pendulum in pendulums: + pendulum.start_swinging() + + self.add(state_grid) + self.wait(self.initial_grid_wait_time) + + def show_grid_of_states_creation(self): + self.remove(self.state_grid) + self.initialize_grid_of_states() # Again + state_grid = self.state_grid + + title = TextMobject("All states") + title.to_edge(UP, buff=MED_SMALL_BUFF) + self.all_states_title = title + + state_grid.set_height( + FRAME_HEIGHT - title.get_height() - 2 * MED_SMALL_BUFF + ) + state_grid.to_edge(DOWN, buff=0) + + def place_at_top(state): + state.set_height(3) + state.center() + state.next_to(title, DOWN) + + middle_row = state_grid[len(state_grid) // 2] + middle_row_copy = middle_row.deepcopy() + right_column_copy = VGroup(*[ + row[-1] for row in state_grid + ]).deepcopy() + + for state in it.chain(middle_row_copy, right_column_copy): + place_at_top(state) + + self.add(title) + self.play( + ShowIncreasingSubsets(middle_row), + ShowIncreasingSubsets(middle_row_copy), + run_time=2, + rate_func=linear, + ) + self.wait() + self.play( + ShowIncreasingSubsets(state_grid), + ShowIncreasingSubsets(right_column_copy), + run_time=2, + rate_func=linear, + ) + self.remove(middle_row_copy) + self.remove(middle_row) + self.add(state_grid) + self.remove(right_column_copy) + self.play( + ReplacementTransform( + right_column_copy[-1], + state_grid[-1][-1], + remover=True + ) + ) + self.wait() + + def collapse_grid_into_points(self): + state_grid = self.state_grid + plane = self.plane + + dots = VGroup() + for row in state_grid: + for state in row: + dot = Dot( + self.get_state_point(state.pendulum), + radius=0.05, + color=YELLOW, + background_stroke_width=3, + background_stroke_color=BLACK, + ) + dot.state = state + dots.add(dot) + + self.add(plane) + self.remove(state_grid) + flat_state_group = VGroup(*it.chain(*state_grid)) + flat_dot_group = VGroup(*it.chain(*dots)) + self.clear() # The nuclear option + self.play( + ShowCreation(plane), + FadeOut(self.all_states_title), + LaggedStart(*[ + TransformFromCopy(m1, m2) + for m1, m2 in zip(flat_state_group, flat_dot_group) + ], lag_ratio=0.1, run_time=4) + ) + self.clear() # Again, not sure why I need this + self.add(plane, dots) + self.wait() + + self.state_dots = dots + + def show_state_with_pair_of_numbers(self): + axis_labels = self.plane.axis_labels + + state = self.get_flexible_state_picture() + dot = self.get_state_controlling_dot(state) + h_line = always_redraw( + lambda: self.get_tracking_h_line(dot.get_center()) + ) + v_line = always_redraw( + lambda: self.get_tracking_v_line(dot.get_center()) + ) + + self.add(dot) + anims = [GrowFromPoint(state, dot.get_center())] + if hasattr(self, "state_dots"): + anims.append(FadeOut(self.state_dots)) + self.play(*anims) + + for line, label in zip([h_line, v_line], axis_labels): + # self.add(line, dot) + self.play( + ShowCreation(line), + ShowCreationThenFadeAround(label), + run_time=1 + ) + for vect in LEFT, 3 * UP: + self.play( + ApplyMethod( + dot.shift, vect, + rate_func=there_and_back, + run_time=2, + ) + ) + self.wait() + for vect in 2 * LEFT, 3 * UP, 2 * RIGHT, 2 * DOWN: + self.play(dot.shift, vect, run_time=1.5) + self.wait() + + self.state = state + self.state_dot = dot + self.h_line = h_line + self.v_line = v_line + + def show_acceleration_dependence(self): + ode = get_ode() + thetas = ode.get_parts_by_tex("\\theta") + thetas[0].set_color(RED) + thetas[1].set_color(YELLOW) + ode.move_to( + FRAME_WIDTH * RIGHT / 4 + + FRAME_HEIGHT * UP / 4, + ) + ode.add_background_rectangle_to_submobjects() + + self.play(Write(ode)) + self.wait() + self.play(FadeOut(ode)) + + def show_evolution_from_a_start_state(self): + state = self.state + dot = self.state_dot + + self.play( + Rotating( + dot, + about_point=dot.get_center() + UR, + rate_func=smooth, + ) + ) + self.wait() + + # Show initial trajectory + state.pendulum.clear_updaters(recursive=False) + self.tie_dot_position_to_state(dot, state) + state.pendulum.start_swinging() + trajectory = self.get_evolving_trajectory(dot) + self.add(trajectory) + for x in range(20): + self.wait() + + # Talk through start + trajectory.suspend_updating() + state.pendulum.end_swinging() + dot.clear_updaters() + self.tie_state_to_dot_position(state, dot) + + alphas = np.linspace(0, 0.1, 1000) + index = np.argmin([ + trajectory.point_from_proportion(a)[1] + for a in alphas + ]) + alpha = alphas[index] + sub_traj = trajectory.copy() + sub_traj.suspend_updating() + sub_traj.pointwise_become_partial(trajectory, 0, alpha) + sub_traj.match_style(trajectory) + sub_traj.set_stroke(width=3) + + self.wait() + self.play(dot.move_to, sub_traj.get_start()) + self.wait() + self.play( + ShowCreation(sub_traj), + UpdateFromFunc( + dot, lambda d: d.move_to(sub_traj.get_end()) + ), + run_time=6, + ) + self.wait() + + # Comment on physical velocity vs. space position + v_vect = state.pendulum.velocity_vector + v_line_copy = self.v_line.copy() + v_line_copy.clear_updaters() + v_line_copy.set_stroke(PINK, 5) + td_label = self.plane.axis_labels[1] + y_axis_copy = self.plane.y_axis.copy() + y_axis_copy.submobjects = [] + y_axis_copy.match_style(v_line_copy) + + self.play(ShowCreationThenFadeAround(v_vect)) + self.play( + ShowCreationThenFadeAround(td_label), + ShowCreationThenFadeOut(y_axis_copy) + ) + self.play(ShowCreationThenFadeOut(v_line_copy)) + self.wait() + + # Abstract vs. physical + abstract = TextMobject("Abstract") + abstract.add_background_rectangle() + abstract.scale(2) + abstract.to_corner(UR) + physical = TextMobject("Physical") + physical.next_to(state.get_top(), DOWN) + + self.play( + ApplyMethod( + self.plane.set_stroke, YELLOW, 0.5, + rate_func=there_and_back, + lag_ratio=0.2, + ), + FadeInFromDown(abstract), + Animation(state), + ) + self.wait() + self.play(FadeInFromDown(physical)) + self.wait() + + # Continue on spiral + sub_traj.resume_updating() + state.pendulum.clear_updaters(recursive=False) + state.pendulum.start_swinging() + dot.clear_updaters() + self.tie_dot_position_to_state(dot, state) + self.wait(20) + + # + def get_initial_thetas(self): + angle = 3 * PI / 4 + return np.linspace(-angle, angle, self.n_thetas) + + def get_initial_omegas(self): + return np.linspace(-1.5, 1.5, self.n_omegas) + + def get_state(self, pendulum): + return (pendulum.get_theta(), pendulum.get_omega()) + + def get_state_point(self, pendulum): + return self.plane.coords_to_point( + *self.get_state(pendulum) + ) + + def get_flexible_state_picture(self): + buff = MED_SMALL_BUFF + height = FRAME_HEIGHT / 2 - buff + rect = Square( + side_length=height, + stroke_color=WHITE, + stroke_width=2, + fill_color="#111111", + fill_opacity=1, + ) + rect.to_corner(UL, buff=buff / 2) + pendulum = Pendulum( + top_point=rect.get_center(), + **self.big_pendulum_config + ) + pendulum.fixed_point_tracker.add_updater( + lambda m: m.move_to(rect.get_center()) + ) + + state = VGroup(rect, pendulum) + state.rect = rect + state.pendulum = pendulum + return state + + def get_state_dot(self, state): + dot = Dot(color=PINK) + dot.move_to(self.get_state_point(state.pendulum)) + return dot + + def get_state_controlling_dot(self, state): + dot = self.get_state_dot(state) + self.tie_state_to_dot_position(state, dot) + return dot + + def tie_state_to_dot_position(self, state, dot): + def update_pendulum(pend): + theta, omega = self.plane.point_to_coords( + dot.get_center() + ) + pend.set_theta(theta) + pend.set_omega(omega) + return pend + state.pendulum.add_updater(update_pendulum) + state.pendulum.get_arc_angle_theta = \ + lambda: self.plane.x_axis.point_to_number(dot.get_center()) + return self + + def tie_dot_position_to_state(self, dot, state): + dot.add_updater(lambda d: d.move_to( + self.get_state_point(state.pendulum) + )) + return dot + + def get_tracking_line(self, point, axis, color=WHITE): + number = axis.point_to_number(point) + axis_point = axis.number_to_point(number) + return DashedLine( + axis_point, point, + dash_length=0.025, + color=color, + ) + + def get_tracking_h_line(self, point): + return self.get_tracking_line( + point, self.plane.y_axis, WHITE, + ) + + def get_tracking_v_line(self, point): + return self.get_tracking_line( + point, self.plane.x_axis, YELLOW, + ) + + def get_evolving_trajectory(self, mobject): + trajectory = VMobject() + trajectory.start_new_path(mobject.get_center()) + trajectory.set_stroke(RED, 1) + + def update_trajectory(traj): + point = mobject.get_center() + if get_norm(trajectory.points[-1] == point) > 0.05: + traj.add_smooth_curve_to(point) + trajectory.add_updater(update_trajectory) + return trajectory + + +class IntroduceVectorField(VisualizeStates): + CONFIG = { + "vector_field_config": { + "max_magnitude": 3, + # "delta_x": 2, + # "delta_y": 2, + }, + "big_pendulum_config": { + "initial_theta": -80 * DEGREES, + "omega": 1, + } + } + + def construct(self): + self.initialize_plane() + self.add_flexible_state() + self.initialize_vector_field() + self.add_equation() + self.preview_vector_field() + self.write_vector_derivative() + self.interpret_first_coordinate() + self.interpret_second_coordinate() + self.show_full_vector_field() + self.show_trajectory() + + def initialize_plane(self): + super().initialize_plane() + self.add(self.plane) + + def initialize_vector_field(self): + self.vector_field = VectorField( + self.vector_field_func, + **self.vector_field_config, + ) + self.vector_field.sort(get_norm) + + def add_flexible_state(self): + self.state = self.get_flexible_state_picture() + self.add(self.state) + + def add_equation(self): + ode = get_ode() + ode.set_width(self.state.get_width() - MED_LARGE_BUFF) + ode.next_to(self.state.get_top(), DOWN, SMALL_BUFF) + thetas = ode.get_parts_by_tex("\\theta") + thetas[0].set_color(RED) + thetas[1].set_color(YELLOW) + ode_word = TextMobject("Differential equation") + ode_word.match_width(ode) + ode_word.next_to(ode, DOWN) + + self.play( + FadeInFrom(ode, 0.5 * DOWN), + FadeInFrom(ode_word, 0.5 * UP), + ) + + self.ode = ode + self.ode_word = ode_word + + def preview_vector_field(self): + vector_field = self.vector_field + + growth = LaggedStartMap( + GrowArrow, vector_field, + run_time=3, + lag_ratio=0.01, + ) + self.add( + growth.mobject, + vector_field, + self.state, self.ode, self.ode_word + ) + + self.play(growth) + self.wait() + self.play(FadeOut(vector_field)) + self.remove(growth.mobject) + + def write_vector_derivative(self): + state = self.state + plane = self.plane + + dot = self.get_state_dot(state) + + # Vector + vect = Arrow( + plane.coords_to_point(0, 0), + dot.get_center(), + buff=0, + color=dot.get_color() + ) + vect_sym, d_vect_sym = [ + self.get_vector_symbol( + "{" + a + "\\theta}(t)", + "{" + b + "\\theta}(t)", + ) + for a, b in [("", "\\dot"), ("\\dot", "\\ddot")] + ] + # vect_sym.get_entries()[1][0][1].set_color(YELLOW) + # d_vect_sym.get_entries()[0][0][1].set_color(YELLOW) + # d_vect_sym.get_entries()[1][0][1].set_color(RED) + vect_sym.next_to(vect.get_end(), UP, MED_LARGE_BUFF) + time_inputs = VGroup(*[ + e[-1][-2] for e in vect_sym.get_entries() + ]) + + # Derivative + ddt = TexMobject("d \\over dt") + ddt.set_height(0.9 * vect_sym.get_height()) + ddt.next_to(vect_sym, LEFT) + ddt.set_stroke(BLACK, 5, background=True) + equals = TexMobject("=") + equals.add_background_rectangle() + equals.next_to(vect_sym, RIGHT, SMALL_BUFF) + d_vect_sym.next_to(equals, RIGHT, SMALL_BUFF) + + # Little vector + angle_tracker = ValueTracker(0) + mag_tracker = ValueTracker(0.75) + d_vect = always_redraw( + lambda: Vector( + rotate_vector( + mag_tracker.get_value() * RIGHT, + angle_tracker.get_value(), + ), + color=WHITE + ).shift(dot.get_center()), + ) + d_vect_magnitude_factor_tracker = ValueTracker(2) + real_d_vect = always_redraw( + lambda: self.vector_field.get_vector( + dot.get_center() + ).scale( + d_vect_magnitude_factor_tracker.get_value(), + about_point=dot.get_center() + ) + ) + + # Show vector + self.play(TransformFromCopy(state[1], vect)) + self.play(FadeInFromDown(vect_sym)) + self.wait() + self.play(ReplacementTransform(vect, dot)) + self.wait() + self.play(LaggedStartMap( + ShowCreationThenFadeAround, time_inputs, + lag_ratio=0.1, + )) + self.wait() + + # Write Derivative + self.play(Write(ddt)) + self.play( + plane.y_axis.numbers.fade, 1, + FadeInFrom(equals, LEFT), + TransformFromCopy(vect_sym, d_vect_sym) + ) + self.wait() + + # Show as little vector + equation_group = VGroup( + ddt, vect_sym, equals, d_vect_sym + ) + self.play( + # equation_group.shift, 4 * DOWN, + equation_group.to_edge, RIGHT, LARGE_BUFF, + GrowArrow(d_vect), + ) + self.wait() + self.play(angle_tracker.set_value, 120 * DEGREES) + self.play(mag_tracker.set_value, 1.5) + self.wait() + + # Highlight new vector + self.play( + ShowCreationThenFadeAround(d_vect_sym), + FadeOut(d_vect) + ) + self.wait() + self.play( + TransformFromCopy(d_vect_sym, real_d_vect), + dot.set_color, WHITE, + ) + self.wait() + + # Take a walk + trajectory = VMobject() + trajectory.start_new_path(dot.get_center()) + dt = 0.01 + for x in range(130): + p = trajectory.points[-1] + dp_dt = self.vector_field_func(p) + trajectory.add_smooth_curve_to(p + dp_dt * dt) + self.tie_state_to_dot_position(state, dot) + self.play( + MoveAlongPath(dot, trajectory), + run_time=5, + rate_func=bezier([0, 0, 1, 1]), + ) + + self.state_dot = dot + self.d_vect = real_d_vect + self.equation_group = equation_group + self.d_vect_magnitude_factor_tracker = d_vect_magnitude_factor_tracker + + def interpret_first_coordinate(self): + equation = self.equation_group + ddt, vect_sym, equals, d_vect_sym = equation + dot = self.state_dot + + first_components_copy = VGroup( + vect_sym.get_entries()[0], + d_vect_sym.get_entries()[0], + ).copy() + rect = SurroundingRectangle(first_components_copy) + rect.set_stroke(YELLOW, 2) + + equation.save_state() + + self.play( + ShowCreation(rect), + equation.fade, 0.5, + Animation(first_components_copy), + ) + self.wait() + dot.save_state() + self.play(dot.shift, 2 * UP) + self.wait() + self.play(dot.shift, 6 * DOWN) + self.wait() + self.play(dot.restore) + self.wait() + + self.play( + equation.restore, + FadeOut(rect), + ) + self.remove(first_components_copy) + + def interpret_second_coordinate(self): + equation = self.equation_group + ddt, vect_sym, equals, d_vect_sym = equation + + second_components = VGroup( + vect_sym.get_entries()[1], + d_vect_sym.get_entries()[1], + ) + rect = SurroundingRectangle(second_components) + rect.set_stroke(YELLOW, 2) + + expanded_derivative = self.get_vector_symbol( + "{\\dot\\theta}(t)", + "-\\mu {\\dot\\theta}(t)" + + "-(g / L) \\sin\\big({\\theta}(t)\\big)", + ) + expanded_derivative.move_to(d_vect_sym) + expanded_derivative.to_edge(RIGHT, MED_SMALL_BUFF) + equals2 = TexMobject("=") + equals2.next_to(expanded_derivative, LEFT, SMALL_BUFF) + + equation.save_state() + self.play( + ShowCreation(rect), + ) + self.wait() + self.play( + FadeInFrom(expanded_derivative, LEFT), + FadeIn(equals2), + equation.next_to, equals2, LEFT, SMALL_BUFF, + MaintainPositionRelativeTo(rect, equation), + VFadeOut(rect), + ) + self.wait() + + self.full_equation = VGroup( + *equation, equals2, expanded_derivative, + ) + + def show_full_vector_field(self): + vector_field = self.vector_field + state = self.state + ode = self.ode + ode_word = self.ode_word + equation = self.full_equation + d_vect = self.d_vect + dot = self.state_dot + + equation.generate_target() + equation.target.scale(0.7) + equation.target.to_edge(DOWN, LARGE_BUFF) + equation.target.to_edge(LEFT, MED_SMALL_BUFF) + equation_rect = BackgroundRectangle(equation.target) + + growth = LaggedStartMap( + GrowArrow, vector_field, + run_time=3, + lag_ratio=0.01, + ) + self.add( + growth.mobject, + state, ode, ode_word, + equation_rect, equation, dot, + d_vect, + ) + self.play( + growth, + FadeIn(equation_rect), + MoveToTarget(equation), + self.d_vect_magnitude_factor_tracker.set_value, 1, + ) + + def show_trajectory(self): + state = self.state + dot = self.state_dot + + state.pendulum.clear_updaters(recursive=False) + self.tie_dot_position_to_state(dot, state) + state.pendulum.start_swinging() + + trajectory = self.get_evolving_trajectory(dot) + trajectory.set_stroke(WHITE, 3) + + self.add(trajectory, dot) + self.wait(25) + + # + def get_vector_symbol(self, tex1, tex2): + 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": t2c, + } + ).scale(0.9) + + def vector_field_func(self, point): + x, y = self.plane.point_to_coords(point) + mu, g, L = [ + self.big_pendulum_config.get(key) + for key in ["damping", "gravity", "length"] + ] + return pendulum_vector_field_func( + x * RIGHT + y * UP, + mu=mu, g=g, L=L + ) + + def ask_about_change(self): + state = self.state + + dot = self.get_state_dot(state) + d_vect = Vector(0.75 * RIGHT, color=WHITE) + d_vect.shift(dot.get_center()) + q_mark = always_redraw( + lambda: TexMobject("?").move_to( + d_vect.get_end() + 0.4 * rotate_vector( + d_vect.get_vector(), 90 * DEGREES, + ), + ) + ) + + self.play(TransformFromCopy(state[1], dot)) + self.tie_state_to_dot_position(state, dot) + self.play( + GrowArrow(d_vect), + FadeInFromDown(q_mark) + ) + for x in range(4): + angle = 90 * DEGREES + self.play( + Rotate( + d_vect, angle, + about_point=d_vect.get_start(), + ) + ) + self.play( + dot.shift, + 0.3 * d_vect.get_vector(), + rate_func=there_and_back, + ) + + +class ShowPendulumPhaseFlow(IntroduceVectorField): + CONFIG = { + "coordinate_plane_config": { + "x_axis_config": { + "unit_size": 0.8, + }, + "x_max": 9, + "x_min": -9, + }, + "flow_time": 20, + } + + def construct(self): + self.initialize_plane() + self.initialize_vector_field() + plane = self.plane + field = self.vector_field + self.add(plane, field) + + 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) + + +class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): + CONFIG = { + "coordinate_plane_config": { + "x_max": 15, + "x_min": -15, + }, + "vector_field_config": { + "x_max": 15, + }, + "big_pendulum_config": { + "max_velocity_vector_length_to_length_ratio": 1, + }, + } + + def setup(self): + MovingCameraScene.setup(self) + + def construct(self): + self.initialize_plane() + self.add(self.plane) + self.initialize_vector_field() + self.add(self.vector_field) + self.add_flexible_state() + self.show_trajectory() + + def show_trajectory(self): + 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) + dot = self.get_state_controlling_dot(state) + dot.move_to(start_point) + state.update() + + 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) + + self.add(traj, dot) + self.play( + ShowCreation( + traj, + rate_func=linear, + ), + 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, + ) + + +class TweakMuInFormula(Scene): + def construct(self): + 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(ShowPendulumPhaseFlow): + def construct(self): + 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) + + +class TakeManyTinySteps(IntroduceVectorField): + CONFIG = { + "initial_theta": 60 * DEGREES, + "initial_theta_dot": 0, + } + + def construct(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 + + time_tracker = ValueTracker(10) + get_t = time_tracker.get_value + + traj = always_redraw( + lambda: self.get_time_step_trajectory( + get_delta_t(), get_t() + ) + ) + vectors = always_redraw( + lambda: self.get_path_vectors( + get_delta_t(), get_t() + ) + ) + + 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() + ) + ) + + self.add(traj, vectors, init_labels, labels) + time_tracker.set_value(0) + self.play( + time_tracker.set_value, 10, + run_time=5, + rate_func=linear, + ) + self.wait() + t_label[-1].clear_updaters() + self.play( + delta_t_tracker.set_value, 0.01, + run_time=7, + ) + self.wait() + + # + def get_time_step_points(self, delta_t, total_time): + plane = self.plane + field = self.vector_field + curr_point = plane.coords_to_point( + self.initial_theta, + self.initial_theta_dot, + ) + points = [curr_point] + t = 0 + while t < total_time: + new_point = curr_point + field.func(curr_point) * delta_t + points.append(new_point) + curr_point = new_point + t += delta_t + return points + + def get_time_step_trajectory(self, delta_t, total_time): + traj = VMobject() + traj.set_points_as_corners( + self.get_time_step_points(delta_t, total_time) + ) + traj.set_stroke(WHITE, 2) + return traj + + def get_path_vectors(self, delta_t, total_time): + corners = self.get_time_step_points( + delta_t, total_time + ) + result = VGroup() + for a1, a2 in zip(corners, corners[1:]): + vector = Arrow( + a1, a2, buff=0, + ) + vector.match_style( + self.vector_field.get_vector(a1) + ) + result.add(vector) + return result diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index 4c53a9d6..f3d304a3 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -88,7 +88,7 @@ class FormulasAreLies(PiCreatureScene): ) self.wait() self.play(you.change, "confused") - self.wait(0) + self.wait() self.play( you.change, "angry", ShowCreation(bubble), @@ -103,6 +103,282 @@ class FormulasAreLies(PiCreatureScene): return You().flip().to_corner(DR) -class NewSceneName(Scene): +# class TourOfDifferentialEquations(Scene): +# def construct(self): +# pass + + +class ProveTeacherWrong(TeacherStudentsScene): def construct(self): - pass + tex_config = { + "tex_to_color_map": {"{\\theta}": BLUE} + } + func = TexMobject( + "{\\theta}(t)", "=", + "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)", + **tex_config, + ) + d_func = TexMobject( + "\\dot {\\theta}(t)", "=", + "-\\left(\\sqrt{g / L}\\right)", + "\\theta_0", "\\sin(\\sqrt{g / L} \\cdot t)", + **tex_config, + ) + dd_func = TexMobject( + "\\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, + ) + arrows = [TexMobject("\\Downarrow") for x in range(2)] + + VGroup(func, d_func, dd_func, ode, *arrows).scale(0.7) + + teacher = self.teacher + you = self.students[2] + + self.student_thinks(ode) + you.add_updater(lambda m: m.look_at(func)) + self.teacher_holds_up(func) + self.wait() + + group = VGroup(arrows[0], d_func, arrows[1], dd_func) + group.arrange(DOWN) + group.move_to(func, DOWN) + + arrow = Arrow( + group.get_corner(UL), + ode.get_top(), + path_arc=PI / 2, + ) + q_marks = VGroup(*[ + TexMobject("?").scale(1.5).next_to( + arrow.point_from_proportion(a), + UP + ) + for a in np.linspace(0.2, 0.8, 5) + ]) + cycle_animation(VFadeInThenOut( + q_marks, + lag_ratio=0.2, + run_time=4, + rate_func=squish_rate_func(smooth, 0, 0.5) + )) + + self.play( + func.next_to, group, UP, + LaggedStartMap( + FadeInFrom, group, + lambda m: (m, UP) + ), + teacher.change, "guilty", + you.change, "sassy", + ) + + rect = SurroundingRectangle( + VGroup(group, func) + ) + dashed_rect = DashedVMobject(rect, num_dashes=75) + animated_rect = AnimatedBoundary(dashed_rect, cycle_rate=1) + + self.wait() + self.add(animated_rect, q_marks) + self.play( + ShowCreation(arrow), + # FadeInFromDown(q_mark), + self.get_student_changes("confused", "confused") + ) + self.wait(4) + self.change_student_modes( + *3 * ["pondering"], + self.teacher.change, "maybe" + ) + self.wait(8) + + +class AskAboutActuallySolving(TeacherStudentsScene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + self.add(ode) + morty = self.teacher + + self.student_says( + "Yeah yeah, but how do\\\\" + "you acutally \\emph{solve} it?", + student_index=1, + target_mode="sassy", + added_anims=[morty.change, "thinking"], + ) + self.change_student_modes( + "confused", "sassy", "confused", + look_at_arg=ode, + ) + self.wait() + self.teacher_says( + "What do you mean\\\\ by ``solve''?", + target_mode="speaking", + added_anims=[self.get_student_changes( + *3 * ["erm"] + )] + ) + self.play(self.students[1].change, "angry") + self.wait(3) + + +class HungerForExactness(TeacherStudentsScene): + def construct(self): + students = self.students + you = students[2] + teacher = self.teacher + + ode = get_ode() + 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}" + ) + proposed_solution.next_to( + you.get_corner(UL), UP, buff=0.7 + ) + proposed_solution_rect = SurroundingRectangle( + proposed_solution, buff=MED_SMALL_BUFF, + ) + proposed_solution_rect.set_color(BLUE) + proposed_solution_rect.round_corners() + + solution_p1 = TexMobject( + """ + \\theta(t) = 2\\text{am}\\left( + \\frac{\\sqrt{2g + Lc_1} (t + c_2)}{2\\sqrt{L}}, + \\frac{4g}{2g + Lc_1} + \\right) + """, + ) + solution_p1.to_corner(UL) + solution_p2 = TexMobject( + "c_1, c_2 = \\text{Constants depending on initial conditions}" + ) + solution_p2.set_color(LIGHT_GREY) + solution_p2.scale(0.75) + solution_p3 = TexMobject( + """ + \\text{am}(u, k) = + \\int_0^u \\text{dn}(v, k)\\,dv + """ + ) + solution_p3.name = TextMobject( + "(Jacobi amplitude function)" + ) + solution_p4 = TexMobject( + """ + \\text{dn}(u, k) = + \\sqrt{1 - k^2 \\sin^2(\\phi)} + """ + ) + solution_p4.name = TextMobject( + "(Jacobi elliptic function)" + ) + solution_p5 = TextMobject("Where $\\phi$ satisfies") + solution_p6 = TexMobject( + """ + u = \\int_0^\\phi \\frac{dt}{\\sqrt{1 - k^2 \\sin^2(t)}} + """ + ) + + solution = VGroup( + solution_p1, + solution_p2, + solution_p3, + solution_p4, + solution_p5, + solution_p6, + ) + solution.arrange(DOWN) + solution.scale(0.7) + solution.to_corner(UL, buff=MED_SMALL_BUFF) + solution.set_stroke(width=0, background=True) + + solution.remove(solution_p2) + solution_p1.add(solution_p2) + solution.remove(solution_p5) + solution_p6.add(solution_p5) + + for part in [solution_p3, solution_p4]: + part.name.scale(0.7 * 0.7) + part.name.set_color(LIGHT_GREY) + part.name.next_to(part, RIGHT) + part.add(part.name) + + self.student_says( + "Right, but like,\\\\" + "what \\emph{is} $\\theta(t)$?", + target_mode="sassy", + added_anims=[teacher.change, "guilty"], + ) + self.wait() + self.play( + FadeInFromDown(proposed_solution), + RemovePiCreatureBubble( + you, + target_mode="raise_left_hand", + look_at_arg=proposed_solution, + ), + teacher.change, "pondering", + students[0].change, "pondering", + students[1].change, "hesitant", + ) + self.play(ShowCreation(proposed_solution_rect)) + self.play( + proposed_solution.shift, 3 * RIGHT, + proposed_solution_rect.shift, 3 * RIGHT, + you.change, "raise_right_hand", teacher.eyes, + ) + self.wait(3) + + self.play( + FadeOut(proposed_solution), + FadeOut(proposed_solution_rect), + ode.move_to, self.hold_up_spot, DOWN, + ode.shift, LEFT, + teacher.change, "raise_right_hand", + self.get_student_changes(*3 * ["pondering"]) + ) + self.wait() + ode.save_state() + self.play( + left_part.move_to, friction_part, RIGHT, + left_part.match_y, left_part, + friction_part.to_corner, DR, + friction_part.fade, 0.5, + ) + self.wait() + + modes = ["erm", "sad", "sad", "horrified"] + for part, mode in zip(solution, modes): + self.play( + FadeInFrom(part, UP), + self.get_student_changes( + *3 * [mode], + look_at_arg=part, + ) + ) + self.wait() + self.wait(3) + self.play( + FadeOutAndShift(solution, 2 * LEFT), + Restore(ode), + self.get_student_changes(*3 * ["sick"]) + ) + self.wait(3) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 606c00b6..265a4585 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -14,3 +14,43 @@ class You(PiCreature): CONFIG = { "color": BLUE_C, } + + +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})", + "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", + **tex_config, + ) + return ode + + +def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): + theta, omega = point[:2] + return np.array([ + omega, + -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 0ca51c39..8fb884da 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -1,14 +1,8 @@ from big_ol_pile_of_manim_imports import * from active_projects.ode.part1.shared_constructs import * - - -def pendulum_vector_field(point, mu=0.1, g=9.8, L=3): - theta, omega = point[:2] - return np.array([ - omega, - -np.sqrt(g / L) * np.sin(theta) - mu * omega, - 0, - ]) +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 # Scenes @@ -45,61 +39,37 @@ class VectorFieldTest(Scene): self.wait(10) -class SmallAngleApproximationTex(Scene): - def construct(self): - approx = TexMobject( - "\\sin", "(", "\\theta", ") \\approx \\theta", - tex_to_color_map={"\\theta": RED}, - arg_separator="", - ) - - implies = TexMobject("\\Downarrow") - period = TexMobject( - "\\text{Period}", "\\approx", - "2\\pi \\sqrt{\\,{L} / {g}}", - **Lg_formula_config, - ) - group = VGroup(approx, implies, period) - group.arrange(DOWN) - - approx_brace = Brace(approx, UP, buff=SMALL_BUFF) - approx_words = TextMobject( - "For small $\\theta$", - tex_to_color_map={"$\\theta$": RED}, - ) - approx_words.scale(0.75) - approx_words.next_to(approx_brace, UP, SMALL_BUFF) - - self.add(approx, approx_brace, approx_words) - self.play( - Write(implies), - FadeInFrom(period, LEFT) - ) - self.wait() - - -class FollowThisThread(Scene): +class TourOfDifferentialEquations(MovingCameraScene): CONFIG = { "screen_rect_style": { "stroke_width": 2, "stroke_color": WHITE, "fill_opacity": 1, - "fill_color": DARKER_GREY, - } + "fill_color": BLACK, + }, + "camera_config": {"background_color": DARKER_GREY}, } def construct(self): + self.add_title() self.show_thumbnails() - self.show_words() + # self.show_words() + + def add_title(self): + title = TextMobject( + "A Tourist's Guide \\\\to Differential\\\\Equations" + ) + title.scale(1.5) + title.to_corner(UR) + self.add(title) def show_thumbnails(self): - # TODO, replace each of these with a picture? - thumbnails = self.thumbnails = VGroup( - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), + thumbnails = self.thumbnails = Group( + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), ) n = len(thumbnails) thumbnails.set_height(1.5) @@ -110,8 +80,16 @@ class FollowThisThread(Scene): [-3, -3, 0], [5, -3, 0], ]) + line.shift(MED_SMALL_BUFF * LEFT) for thumbnail, a in zip(thumbnails, np.linspace(0, 1, n)): thumbnail.move_to(line.point_from_proportion(a)) + dots = TexMobject("\\dots") + dots.next_to(thumbnails[-1], RIGHT) + + self.add_heat_preview(thumbnails[1]) + self.add_fourier_series(thumbnails[2]) + self.add_matrix_exponent(thumbnails[3]) + self.add_laplace_symbol(thumbnails[4]) self.play( ShowCreation( @@ -130,6 +108,14 @@ class FollowThisThread(Scene): ], lag_ratio=1), run_time=5 ) + self.play(Write(dots)) + self.wait() + self.play( + self.camera_frame.replace, + thumbnails[0], + run_time=3, + ) + self.wait() def show_words(self): words = VGroup( @@ -162,63 +148,70 @@ class FollowThisThread(Scene): ) self.wait() + # + def add_heat_preview(self, thumbnail): + image = ImageMobject("HeatSurfaceExample") + image.replace(thumbnail) + thumbnail.add(image) -class StrogatzQuote(Scene): - def construct(self): - law_words = "laws of physics" - language_words = "language of differential equations" - author = "-Steven Strogatz" - quote = TextMobject( - """ - \\Large - ``Since Newton, mankind has come to realize - that the laws of physics are always expressed - in the language of differential equations.''\\\\ - """ + author, - alignment="", - arg_separator=" ", - substrings_to_isolate=[law_words, language_words, author] + def add_matrix_exponent(self, thumbnail): + matrix = IntegerMatrix( + [[3, 1], [4, 1]], + v_buff=MED_LARGE_BUFF, + h_buff=MED_LARGE_BUFF, + bracket_h_buff=SMALL_BUFF, + bracket_v_buff=SMALL_BUFF, ) - 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.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) + e = TexMobject("e") + t = TexMobject("t") + t.scale(1.5) + t.next_to(matrix, RIGHT, SMALL_BUFF) + e.scale(2) + e.move_to(matrix.get_corner(DL), UR) + group = VGroup(e, matrix, t) + group.set_height(0.7 * thumbnail.get_height()) + randy = Randolph(mode="confused", height=0.75) + randy.next_to(group, LEFT, aligned_edge=DOWN) + randy.look_at(matrix) + group.add(randy) + group.move_to(thumbnail) + thumbnail.add(group) - 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, + def add_fourier_series(self, thumbnail): + colors = [BLUE, GREEN, YELLOW, RED, RED_E, PINK] + + waves = VGroup(*[ + self.get_square_wave_approx(N, color) + for N, color in enumerate(colors) + ]) + waves.set_stroke(width=1.5) + waves.replace(thumbnail, stretch=True) + waves.scale(0.8) + waves.move_to(thumbnail) + thumbnail.add(waves) + + def get_square_wave_approx(self, N, color): + return FunctionGraph( + lambda x: sum([ + (1 / n) * np.sin(n * PI * x) + for n in range(1, 2 * N + 3, 2) + ]), + x_min=0, + x_max=2, + color=color ) - self.wait() - self.play( - Write(law_part.copy().set_color(YELLOW)), - run_time=1, + + def add_laplace_symbol(self, thumbnail): + mob = TexMobject( + "\\mathcal{L}\\left\\{f(t)\\right\\}" ) - self.wait() - self.play( - Write(language_part.copy().set_color(BLUE)), - run_time=1.5, - ) - self.wait(2) + mob.set_width(0.8 * thumbnail.get_width()) + mob.move_to(thumbnail) + thumbnail.add(mob) + + +class HeatEquationPreview(ExternallyAnimatedScene): + pass class ShowGravityAcceleration(Scene): @@ -412,3 +405,814 @@ class ShowGravityAcceleration(Scene): LaggedStartMap(FadeInFrom, amounts, lambda m: (m, LEFT)), ) 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.to_edge(UP) + rect = ScreenRectangle(height=6) + rect = rect.copy() + rect.set_style( + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + rect.next_to(title, DOWN) + animated_rect = AnimatedBoundary(rect) + + self.add(title, rect) + self.add(animated_rect) + self.wait(10) + + +class SubtleAirCurrents(Scene): + def construct(self): + pass + + +class DefineODE(Scene): + CONFIG = { + "pendulum_config": { + "length": 2, + "top_point": 5 * RIGHT + 2 * UP, + "initial_theta": 150 * DEGREES, + "mu": 0.3, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "y_max": PI, + "y_min": -PI, + "x_max": 10, + "x_axis_config": { + "numbers_to_show": range(2, 10, 2), + "unit_size": 1, + } + }, + } + + def construct(self): + self.add_graph() + self.write_differential_equation() + self.dont_know_the_value() + self.show_value_slope_curvature() + self.write_ode() + self.show_second_order() + self.show_higher_order_examples() + self.show_changing_curvature_group() + + def add_graph(self): + pendulum = Pendulum(**self.pendulum_config) + axes = ThetaVsTAxes(**self.axes_config) + + axes.center() + axes.to_corner(DL) + graph = axes.get_live_drawn_graph(pendulum) + + pendulum.start_swinging() + self.add(axes, pendulum, graph) + + self.pendulum = pendulum + self.axes = axes + self.graph = graph + + def write_differential_equation(self): + de_word = TextMobject("Differential", "Equation") + de_word.to_edge(UP) + + equation = get_ode() + equation.next_to(de_word, DOWN) + thetas = equation.get_parts_by_tex("\\theta") + + lines = VGroup(*[ + Line(v, 1.2 * v) + for v in compass_directions(25) + ]) + lines.replace(equation, stretch=True) + lines.scale(1.5) + lines.set_stroke(YELLOW) + lines.shuffle() + + self.add(equation) + self.wait(5) + self.play( + ShowPassingFlashWithThinningStrokeWidth( + lines, + lag_ratio=0.002, + run_time=1.5, + time_width=0.9, + n_segments=5, + ) + ) + self.play(FadeInFromDown(de_word)) + self.wait(2) + self.play( + LaggedStartMap( + ApplyMethod, thetas, + lambda m: (m.shift, 0.25 * DOWN), + rate_func=there_and_back, + ) + ) + self.wait() + + self.de_word = de_word + self.equation = equation + + def dont_know_the_value(self): + graph = self.graph + pendulum = self.pendulum + + q_marks = VGroup(*[ + TexMobject("?").move_to(graph.point_from_proportion(a)) + for a in np.linspace(0, 1, 20) + ]) + q_marks.set_stroke(width=0, background=True) + self.play( + VFadeOut(graph), + FadeOut(pendulum), + LaggedStart(*[ + UpdateFromAlphaFunc( + q_mark, + lambda m, a: m.set_height(0.5 * (1 + a)).set_fill( + opacity=there_and_back(a) + ), + ) + for q_mark in q_marks + ], lag_ratio=0.01, run_time=2) + ) + self.remove(q_marks) + + def show_value_slope_curvature(self): + axes = self.axes + p = self.pendulum + graph = axes.get_graph( + lambda t: p.initial_theta * np.cos( + np.sqrt(p.gravity / p.length) * t + ) * np.exp(-p.mu * t / 2) + ) + + tex_config = { + "tex_to_color_map": { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, + }, + "height": 0.5, + } + theta, d_theta, dd_theta = [ + TexMobject( + "{" + s + "\\theta}(t)", + **tex_config + ) + for s in ("", "\\dot", "\\ddot") + ] + + t_tracker = ValueTracker(2.5) + 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) + + theta.next_to(v_line, RIGHT, SMALL_BUFF) + d_theta.next_to(slope_line.get_end(), UP, SMALL_BUFF) + dd_theta.next_to(curve.get_end(), RIGHT, SMALL_BUFF) + thetas = VGroup(theta, d_theta, dd_theta) + + 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, thetas): + word.next_to(sym, RIGHT, buff=2 * SMALL_BUFF) + sym.word = word + + self.play( + ShowCreation(v_line), + FadeInFromPoint(dot, v_line.get_start()), + FadeInFrom(theta, DOWN), + FadeInFrom(theta.word, DOWN), + ) + self.add(slope_line, dot) + self.play( + ShowCreation(slope_line), + FadeInFrom(d_theta, LEFT), + FadeInFrom(d_theta.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_theta, LEFT), + FadeInFrom(dd_theta.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(thetas, words) + self.fake_graph = graph + + def write_ode(self): + equation = self.equation + axes = self.axes + de_word = self.de_word + + ts = equation.get_parts_by_tex("{t}") + t_rects = VGroup(*map(SurroundingRectangle, ts)) # Rawr + x_axis = axes.x_axis + x_axis_line = Line( + x_axis.get_start(), x_axis.get_end(), + stroke_color=YELLOW, + stroke_width=5, + ) + + ordinary = TextMobject("Ordinary") + de_word.generate_target() + group = VGroup(ordinary, de_word.target) + group.arrange(RIGHT) + group.to_edge(UP) + ordinary_underline = Line(LEFT, RIGHT) + ordinary_underline.replace(ordinary, dim_to_match=0) + ordinary_underline.next_to(ordinary, DOWN, SMALL_BUFF) + ordinary_underline.set_color(YELLOW) + + self.play( + ShowCreationThenFadeOut( + t_rects, + lag_ratio=0.8 + ), + ShowCreationThenFadeOut(x_axis_line) + ) + self.play( + MoveToTarget(de_word), + FadeInFrom(ordinary, RIGHT), + GrowFromCenter(ordinary_underline) + ) + self.play(FadeOut(ordinary_underline)) + self.wait() + + self.remove(ordinary, de_word) + ode_word = self.ode_word = VGroup(*ordinary, *de_word) + ode_initials = VGroup(*[word[0] for word in ode_word]) + ode_initials.generate_target() + ode_initials.target.scale(1.2) + ode_initials.target.set_color(PINK) + ode_initials.target.arrange( + RIGHT, buff=0.5 * SMALL_BUFF, aligned_edge=DOWN + ) + ode_initials.target.to_edge(UP, buff=MED_SMALL_BUFF) + + ode_remaining_letters = VGroup(*it.chain(*[ + word[1:] for word in ode_word + ])) + ode_remaining_letters.generate_target() + for mob in ode_remaining_letters.target: + mob.shift(0.2 * UP) + mob.fade(1) + + self.play( + MoveToTarget(ode_initials), + MoveToTarget(ode_remaining_letters, lag_ratio=0.05), + ) + self.wait() + + self.ode_initials = ode_initials + + def show_second_order(self): + so = TextMobject("Second order") + so.scale(1.4) + ode = self.ode_initials + ode.generate_target() + group = VGroup(so, ode.target) + group.arrange(RIGHT, aligned_edge=DOWN) + group.to_edge(UP, buff=MED_SMALL_BUFF) + + second_deriv = self.equation[:5] + + self.play( + Write(so), + MoveToTarget(ode), + ) + self.wait() + self.play(FocusOn(second_deriv)) + self.play( + Indicate(second_deriv, color=RED), + ) + self.wait() + + self.second_order_word = so + + def show_higher_order_examples(self): + main_example = VGroup( + self.second_order_word, + self.ode_initials, + self.equation + ) + tex_config = {"tex_to_color_map": {"{x}": BLUE}} + example3 = VGroup( + TextMobject("Third order ODE"), + TexMobject( + "\\dddot {x}(t) + \\dot {x}(t)^2 = 0", + **tex_config, + ) + ) + example4 = VGroup( + TextMobject("Fourth order ODE"), + TexMobject( + "\\ddddot {x}(t) +", + "a\\dddot {x}(t) \\dot {x}(t) + ", + "b \\ddot {x}(t) {x}(t)", + "= 1", + **tex_config, + ) + ) + for example in [example3, example4]: + example[0].scale(1.2) + example.arrange(DOWN, buff=MED_LARGE_BUFF) + example.to_edge(UP, buff=MED_SMALL_BUFF) + + self.play( + FadeOut(main_example), + FadeIn(example3), + ) + self.wait(2) + self.play( + FadeOut(example3), + FadeIn(example4), + ) + self.wait(2) + self.play( + FadeOut(example4), + FadeIn(main_example), + ) + self.wait(2) + + def show_changing_curvature_group(self): + t_tracker = self.t_tracker + curvature_group = self.curvature_group + labels = self.curvature_group_labels + graph = VMobject() + graph.pointwise_become_partial( + self.fake_graph, + 0.25, 1, + ) + dashed_graph = DashedVMobject(graph, num_dashes=100) + dashed_graph.set_stroke(GREEN, 1) + + self.play(FadeOut(labels)) + self.add(dashed_graph, curvature_group) + self.play( + t_tracker.set_value, 10, + ShowCreation(dashed_graph), + run_time=15, + rate_func=linear, + ) + self.wait() + + +class ODEvsPDEinFrames(Scene): + def construct(self): + pass + + +class ReferencePiCollisionStateSpaces(Scene): + def construct(self): + pass + + +class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): + def construct(self): + ode = TexMobject( + "{\\ddot\\theta}", "(t)", "=", + "-\\mu", "{\\dot\\theta}", "(t)" + "-(g / L)\\sin\\big(", "{\\theta}", "(t)\\big)", + tex_to_color_map={ + "{\\ddot\\theta}": RED, + "{\\dot\\theta}": YELLOW, + "{\\theta}": BLUE, + # "{t}": WHITE, + } + ) + so_word = TextMobject("Second order ODE") + sys_word = TextMobject("System of two first order ODEs") + + system1 = self.get_system("{\\theta}", "{\\dot\\theta}") + system2 = self.get_system("{\\theta}", "{\\omega}") + + so_word.to_edge(UP) + ode.next_to(so_word, DOWN) + sys_word.move_to(ORIGIN) + system1.next_to(sys_word, DOWN) + system2.move_to(system1) + + self.add(ode) + self.play(FadeInFrom(so_word, 0.5 * DOWN)) + self.wait() + + self.play( + TransformFromCopy( + ode[3:], system1[3].get_entries()[1], + ), + TransformFromCopy(ode[2], system1[2]), + TransformFromCopy( + ode[:2], VGroup( + system1[0], + system1[1].get_entries()[1], + ) + ), + ) + self.play( + FadeIn(system1[1].get_brackets()), + FadeIn(system1[1].get_entries()[0]), + FadeIn(system1[3].get_brackets()), + FadeIn(system1[3].get_entries()[0]), + ) + self.play( + FadeInFromDown(sys_word) + ) + self.wait() + + self.play(ReplacementTransform(system1, system2)) + self.wait() + + def get_system(self, tex1, tex2): + system = VGroup( + TexMobject("d \\over dt"), + self.get_vector_symbol( + tex1 + "(t)", + tex2 + "(t)", + ), + TexMobject("="), + self.get_vector_symbol( + tex2 + "(t)", + "".join([ + "-\\mu", tex2, "(t)", + "-(g / L) \\sin\\big(", + tex1, "(t)", "\\big)", + ]) + ) + ) + system.arrange(RIGHT) + 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, 6, 3], + "colors": [RED_E, GREEN_E, BLUE_E], + "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() + axes.set_stroke(width=0.5) + self.add(axes) + + def add_bodies(self): + masses = self.masses + colors = self.colors + + 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)) + ] + + for mass, color, center in zip(masses, colors, centers): + body = self.get_sphere( + checkerboard_colors=[ + color, color + ], + color=color, + stroke_width=0.1, + ) + body.set_opacity(0.75) + body.mass = mass + body.set_width(0.15 * np.sqrt(mass)) + + 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) + + 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.point + 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.point) + traj.set_stroke(body.color, 1, opacity=0.75) + traj.add_updater(update_trajectory) + 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)): + self.wait() + + # + 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.point - body.point + m2 = body2.mass + R = get_norm(diff) + acceleration += G * m2 * diff / (R**3) + + num_mid_steps = 100 + 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 + + +class AltThreeBodiesInSpace(ThreeBodiesInSpace): + CONFIG = { + "random_seed": 6, + "masses": [1, 2, 6], + } + + +class DefineODECopy(DefineODE): + pass + + +class WriteODESolvingCode(ExternallyAnimatedScene): + pass + + +class InaccurateComputation(Scene): + def construct(self): + h_line = DashedLine(LEFT_SIDE, RIGHT_SIDE) + h_line.to_edge(UP, buff=1.5) + words = VGroup( + TextMobject("Real number"), + TextMobject("IEEE 754\\\\representation"), + TextMobject("Error"), + ) + for i, word in zip([-1, 0, 1], words): + word.next_to(h_line, UP) + word.shift(i * FRAME_WIDTH * RIGHT / 3) + + lines = VGroup(*[ + DashedLine(TOP, BOTTOM) + for x in range(4) + ]) + lines.arrange(RIGHT) + lines.stretch_to_fit_width(FRAME_WIDTH) + + self.add(h_line, lines[1:-1], words) + + numbers = VGroup( + TexMobject("\\pi").scale(2), + TexMobject("e^{\\sqrt{163}\\pi}").scale(1.5), + ) + numbers.set_color(YELLOW) + numbers.set_stroke(width=0, background=True) + + bit_strings = VGroup( + TexMobject( + "01000000", + "01001001", + "00001111", + "11011011", + ), + TexMobject( + "01011100", + "01101001", + "00101110", + "00011001", + ) + ) + for mob in bit_strings: + mob.arrange(DOWN, buff=SMALL_BUFF) + for word in mob: + for submob, bit in zip(word, word.get_tex_string()): + if bit == "0": + submob.set_color(LIGHT_GREY) + errors = VGroup( + TexMobject( + "\\approx 8.7422 \\times 10^{-8}" + ), + TexMobject( + "\\approx 5{,}289{,}803{,}032.00", + ), + ) + errors.set_color(RED) + + content = VGroup(numbers, bit_strings, errors) + + for group, word in zip(content, words): + group[1].shift(3 * DOWN) + group.move_to(DOWN) + group.match_x(word) + + self.play(*map(Write, numbers)) + self.wait() + self.play( + TransformFromCopy(numbers, bit_strings), + lag_ratio=0.01, + run_time=2, + ) + self.wait() + self.play(FadeInFrom(errors, 3 * LEFT)) + self.wait() + + +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 new file mode 100644 index 00000000..6a966789 --- /dev/null +++ b/active_projects/ode/part1/wordy_scenes.py @@ -0,0 +1,262 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * + + +class SmallAngleApproximationTex(Scene): + def construct(self): + approx = TexMobject( + "\\sin", "(", "\\theta", ") \\approx \\theta", + tex_to_color_map={"\\theta": RED}, + arg_separator="", + ) + + implies = TexMobject("\\Downarrow") + period = TexMobject( + "\\text{Period}", "\\approx", + "2\\pi \\sqrt{\\,{L} / {g}}", + **Lg_formula_config, + ) + group = VGroup(approx, implies, period) + group.arrange(DOWN) + + approx_brace = Brace(approx, UP, buff=SMALL_BUFF) + approx_words = TextMobject( + "For small $\\theta$", + tex_to_color_map={"$\\theta$": RED}, + ) + approx_words.scale(0.75) + approx_words.next_to(approx_brace, UP, SMALL_BUFF) + + self.add(approx, approx_brace, approx_words) + self.play( + Write(implies), + FadeInFrom(period, LEFT) + ) + self.wait() + + +class StrogatzQuote(Scene): + def construct(self): + law_words = "laws of physics" + language_words = "language of differential equations" + author = "-Steven Strogatz" + quote = TextMobject( + """ + \\Large + ``Since Newton, mankind has come to realize + that the laws of physics are always expressed + in the language of differential equations.''\\\\ + """ + author, + alignment="", + 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.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) + + 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, + ) + self.wait() + self.play( + Write(law_part.copy().set_color(YELLOW)), + run_time=1, + ) + self.wait() + self.play( + Write(language_part.copy().set_color(BLUE)), + run_time=1.5, + ) + self.wait(2) + + +class SetAsideSeekingSolution(Scene): + def construct(self): + ode = get_ode() + ode.to_edge(UP) + q1 = TextMobject("Find an exact solution") + q1.set_color(YELLOW) + q2 = TexMobject( + "\\text{What is }", "\\theta", "(t)", + "\\text{'s personality?}", + tex_to_color_map={"\\theta": BLUE}, + arg_separator="", + ) + theta = q2.get_part_by_tex("\\theta") + + for q in q1, q2: + q.scale(1.5) + q.next_to(ode, DOWN, MED_LARGE_BUFF) + eyes = Eyes(theta, height=0.1) + + self.add(ode) + self.add(q1) + self.wait() + self.play( + q1.scale, 0.3, + q1.to_corner, UR, MED_SMALL_BUFF, + ) + self.play(FadeInFrom(q2, DOWN)) + self.play( + eyes.blink, + rate_func=lambda t: smooth(1 - t), + ) + self.play(eyes.look_at, q2.get_left()) + self.play(eyes.look_at, q2.get_right()) + self.play( + eyes.blink, + rate_func=squish_rate_func(there_and_back) + ) + self.wait() + self.play( + eyes.change_mode, "confused", + eyes.look_at, ode.get_left(), + ) + self.play( + 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)) diff --git a/big_ol_pile_of_manim_imports.py b/big_ol_pile_of_manim_imports.py index 746936c2..0b7334ec 100644 --- a/big_ol_pile_of_manim_imports.py +++ b/big_ol_pile_of_manim_imports.py @@ -35,6 +35,7 @@ from manimlib.camera.moving_camera import * from manimlib.camera.three_d_camera import * from manimlib.mobject.coordinate_systems import * +from manimlib.mobject.changing import * from manimlib.mobject.frame import * from manimlib.mobject.functions import * from manimlib.mobject.geometry import * diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index d559fada..b3e5325c 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -64,10 +64,11 @@ class DrawBorderThenFill(Animation): def get_outline(self): outline = self.mobject.copy() outline.set_fill(opacity=0) - outline.set_stroke( - color=self.get_stroke_color(outline), - width=self.stroke_width - ) + for sm in outline.family_members_with_points(): + sm.set_stroke( + color=self.get_stroke_color(sm), + width=self.stroke_width + ) return outline def get_stroke_color(self, vmobject): diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 8f900c97..9f5ea190 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -4,6 +4,7 @@ from manimlib.animation.transform import Transform from manimlib.constants import DOWN from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import interpolate +from manimlib.utils.rate_functions import there_and_back DEFAULT_FADE_LAG_RATIO = 0 @@ -149,3 +150,10 @@ class VFadeOut(VFadeIn): def interpolate_submobject(self, submob, start, alpha): super().interpolate_submobject(submob, start, 1 - alpha) + + +class VFadeInThenOut(VFadeIn): + CONFIG = { + "rate_func": there_and_back, + "remover": True, + } diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py new file mode 100644 index 00000000..9387ebd1 --- /dev/null +++ b/manimlib/mobject/changing.py @@ -0,0 +1,61 @@ +from manimlib.constants import * +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.utils.rate_functions import smooth + + +class AnimatedBoundary(VGroup): + CONFIG = { + "colors": [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN], + "max_stroke_width": 3, + "cycle_rate": 0.5, + } + + def __init__(self, vmobject, **kwargs): + super().__init__(**kwargs) + self.vmobject = vmobject + self.boundary_copies = [ + vmobject.copy().set_style( + stroke_width=0, + fill_opacity=0 + ) + for x in range(2) + ] + self.add(*self.boundary_copies) + self.total_time = 0 + self.add_updater(lambda m, dt: self.update(dt)) + + def update(self, dt): + # Not actual time, but something which passes at + # an altered rate to make the implementation below + # cleaner + time = self.total_time * self.cycle_rate + growing, fading = self.boundary_copies + colors = self.colors + msw = self.max_stroke_width + vmobject = self.vmobject + + index = int(time % len(colors)) + alpha = smooth(time % 1) + + if int(time) % 2 == 0: + bounds = (0, alpha) + else: + bounds = (1 - alpha, 1) + self.full_family_become_partial(growing, vmobject, *bounds) + growing.set_stroke(colors[index], width=msw) + + if time >= 1: + self.full_family_become_partial(fading, vmobject, 0, 1) + fading.set_stroke( + color=colors[index - 1], + width=(1 - alpha) * msw + ) + + self.total_time += dt + + def full_family_become_partial(self, mob1, mob2, a, b): + family1 = mob1.family_members_with_points() + family2 = mob2.family_members_with_points() + for sm1, sm2 in zip(family1, family2): + sm1.pointwise_become_partial(sm2, a, b) + return self diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 6d9a5afb..b3d2cc35 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -288,7 +288,7 @@ class NumberPlane(Axes): def get_lines_parallel_to_axis(self, axis1, axis2, freq, ratio): line = Line(axis1.get_start(), axis1.get_end()) dense_freq = (1 + ratio) - step = 1 / dense_freq + step = (1 / dense_freq) * freq lines1 = VGroup() lines2 = VGroup() @@ -336,6 +336,7 @@ class NumberPlane(Axes): axis.get_edge_center(edge), direction, buff=buff ) + label.shift_onto_screen(buff=MED_SMALL_BUFF) return label def get_axis_labels(self, x_label_tex="x", y_label_tex="y"): diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 15c0f0ce..db594e87 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -543,7 +543,7 @@ class Arrow(Line): "buff": MED_SMALL_BUFF, "tip_width_to_length_ratio": 1, "max_tip_length_to_length_ratio": 0.25, - "max_stroke_width_to_length_ratio": 6, + "max_stroke_width_to_length_ratio": 4, "preserve_tip_size_when_scaling": True, "rectangular_stem_width": 0.05, } @@ -557,6 +557,9 @@ class Arrow(Line): self.set_stroke_width_from_length() def scale(self, factor, **kwargs): + if self.get_length() == 0: + return self + has_tip = self.has_tip() has_start_tip = self.has_start_tip() if has_tip or has_start_tip: diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 17847562..a689729c 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -56,6 +56,8 @@ class Matrix(VMobject): CONFIG = { "v_buff": 0.8, "h_buff": 1.3, + "bracket_h_buff": MED_SMALL_BUFF, + "bracket_v_buff": MED_SMALL_BUFF, "add_background_rectangles_to_entries": False, "include_background_rectangle": False, "element_to_mobject": TexMobject, @@ -101,10 +103,12 @@ class Matrix(VMobject): def add_brackets(self): bracket_pair = TexMobject("\\big[", "\\big]") bracket_pair.scale(2) - bracket_pair.stretch_to_fit_height(self.get_height() + 0.5) + bracket_pair.stretch_to_fit_height( + self.get_height() + 2 * self.bracket_v_buff + ) l_bracket, r_bracket = bracket_pair.split() - l_bracket.next_to(self, LEFT) - r_bracket.next_to(self, RIGHT) + l_bracket.next_to(self, LEFT, self.bracket_h_buff) + r_bracket.next_to(self, RIGHT, self.bracket_h_buff) self.add(l_bracket, r_bracket) self.brackets = VGroup(l_bracket, r_bracket) return self diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 02e4d2b2..6eeeec74 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1107,7 +1107,7 @@ class Mobject(Container): # Errors def throw_error_if_no_points(self): if self.has_no_points(): - message = "Cannot call Mobject.{}" +\ + message = "Cannot call Mobject.{} " +\ "for a Mobject with no points" caller_name = sys._getframe(1).f_code.co_name raise Exception(message.format(caller_name)) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 8128193a..df372e3e 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -141,8 +141,10 @@ class NumberLine(Line): self.decimal_number_config, number_config or {}, ) - scale_val = scale_val or self.number_scale_val - direction = direction or self.label_direction + if scale_val is None: + scale_val = self.number_scale_val + if direction is None: + direction = self.label_direction buff = buff or self.line_to_number_buff num_mob = DecimalNumber(number, **number_config) @@ -181,4 +183,7 @@ class UnitInterval(NumberLine): "tick_frequency": 0.1, "numbers_with_elongated_ticks": [0, 1], "number_at_center": 0.5, + "decimal_number_config": { + "num_decimal_places": 1, + } } diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index d3e1e089..a0f46229 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -25,6 +25,7 @@ class BackgroundRectangle(SurroundingRectangle): CONFIG = { "color": BLACK, "stroke_width": 0, + "stroke_opacity": 0, "fill_opacity": 0.75, "buff": 0 } diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 3afd362e..b8efddf5 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -66,6 +66,7 @@ class SingleStringTexMobject(SVGMobject): # Need to add blank subscript or superscript tex.endswith("_"), tex.endswith("^"), + tex.endswith("dot"), ]) if should_add_filler: filler = "{\\quad}" diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 433f9f67..640a40d1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -577,6 +577,8 @@ class VMobject(Mobject): again. """ for submob in self.family_members_with_points(): + if len(submob.points) < self.n_points_per_cubic_curve: + continue a1, h1, h2, a2 = submob.get_anchors_and_handles() a1_to_h1 = h1 - a1 a2_to_h2 = h2 - a2 @@ -918,22 +920,22 @@ class DashedVMobject(VMobject): VMobject.__init__(self, **kwargs) num_dashes = self.num_dashes ps_ratio = self.positive_space_ratio + if num_dashes > 0: + # End points of the unit interval for division + alphas = np.linspace(0, 1, num_dashes + 1) - # End points of the unit interval for division - alphas = np.linspace(0, 1, num_dashes + 1) + # This determines the length of each "dash" + full_d_alpha = (1.0 / num_dashes) + partial_d_alpha = full_d_alpha * ps_ratio - # This determines the length of each "dash" - full_d_alpha = (1.0 / num_dashes) - partial_d_alpha = full_d_alpha * ps_ratio + # Rescale so that the last point of vmobject will + # be the end of the last dash + alphas /= (1 - full_d_alpha + partial_d_alpha) - # Rescale so that the last point of vmobject will - # be the end of the last dash - alphas /= (1 - full_d_alpha + partial_d_alpha) - - self.add(*[ - vmobject.get_subcurve(alpha, alpha + partial_d_alpha) - for alpha in alphas[:-1] - ]) + self.add(*[ + vmobject.get_subcurve(alpha, alpha + partial_d_alpha) + for alpha in alphas[:-1] + ]) # Family is already taken care of by get_subcurve # implementation self.match_style(vmobject, family=False) diff --git a/manimlib/scene/three_d_scene.py b/manimlib/scene/three_d_scene.py index ccf047d8..da120610 100644 --- a/manimlib/scene/three_d_scene.py +++ b/manimlib/scene/three_d_scene.py @@ -42,6 +42,7 @@ class ThreeDScene(Scene): def stop_ambient_camera_rotation(self): self.camera.theta_tracker.clear_updaters() + self.remove(self.camera.theta_tracker) def move_camera(self, phi=None, @@ -68,12 +69,8 @@ class ThreeDScene(Scene): self.camera.frame_center.move_to, frame_center )) - is_camera_rotating = self.ambient_camera_rotation in self.continual_animations - if is_camera_rotating: - self.remove(self.ambient_camera_rotation) + self.play(*anims + added_anims) - if is_camera_rotating: - self.add(self.ambient_camera_rotation) def get_moving_mobjects(self, *animations): moving_mobjects = Scene.get_moving_mobjects(self, *animations)