from manimlib.imports import * from active_projects.diffyq.part1.shared_constructs import * from active_projects.diffyq.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, }, "run_time": 25, "initial_theta": 0, "initial_theta_dot": 4, "frame_shift_vect": TAU * RIGHT, } def setup(self): MovingCameraScene.setup(self) def construct(self): self.initialize_plane_and_field() self.add_flexible_state() self.show_high_vector() self.show_trajectory() def initialize_plane_and_field(self): self.initialize_plane() self.add(self.plane) self.initialize_vector_field() self.add(self.vector_field) def add_flexible_state(self): super().add_flexible_state() state = self.state plane = self.plane state.to_edge(DOWN, buff=SMALL_BUFF), start_point = plane.coords_to_point( self.initial_theta, self.initial_theta_dot, ) dot = self.get_state_controlling_dot(state) dot.move_to(start_point) state.update() self.dot = dot self.start_point = start_point def show_high_vector(self): field = self.vector_field top_vectors = VGroup(*filter( lambda a: np.all(a.get_center() > [-10, 1.5, -10]), field )).copy() top_vectors.set_stroke(PINK, 3) top_vectors.sort(lambda p: p[0]) self.play( ShowCreationThenFadeOut( top_vectors, run_time=2, lag_ratio=0.01, ) ) def show_trajectory(self): state = self.state frame = self.camera_frame dot = self.dot start_point = self.start_point traj = self.get_trajectory(start_point, self.run_time) self.add(traj, dot) anims = [ ShowCreation( traj, rate_func=linear, ), UpdateFromFunc( dot, lambda d: d.move_to(traj.points[-1]) ), ] if get_norm(self.frame_shift_vect) > 0: anims += [ ApplyMethod( frame.shift, self.frame_shift_vect, rate_func=squish_rate_func( smooth, 0, 0.3, ) ), MaintainPositionRelativeTo(state.rect, frame), ] self.play(*anims, run_time=total_time) def get_trajectory(self, start_point, time, dt=0.1, added_steps=100): field = self.vector_field traj = VMobject() traj.start_new_path(start_point) for x in range(int(time / dt)): last_point = traj.points[-1] for y in range(added_steps): dp_dt = field.func(last_point) last_point += dp_dt * dt / added_steps traj.add_smooth_curve_to(last_point) traj.make_smooth() traj.set_stroke(WHITE, 2) return traj class TweakMuInFormula(Scene): def construct(self): self.add(FullScreenFadeRectangle( opacity=0.75, )) ode = get_ode() ode.to_edge(DOWN, buff=LARGE_BUFF) mu = ode.get_part_by_tex("\\mu") 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 HighAmplitudePendulum(ShowHighVelocityCase): CONFIG = { "big_pendulum_config": { "damping": 0.02, }, "initial_theta": 175 * DEGREES, "initial_theta_dot": 0, "frame_shift_vect": 0 * RIGHT, } def construct(self): self.initialize_plane_and_field() self.add_flexible_state() self.show_trajectory() class SpectrumOfStartingStates(ShowHighVelocityCase): CONFIG = { "run_time": 15, } def construct(self): self.initialize_plane_and_field() self.vector_field.set_opacity(0.5) self.show_many_trajectories() def show_many_trajectories(self): plane = self.plane delta_x = 0.5 delta_y = 0.5 n = 20 start_points = [ plane.coords_to_point(x, y) for x in np.linspace(PI - delta_x, PI + delta_x, n) for y in np.linspace(-delta_y, delta_y, n) ] start_points.sort( key=lambda p: np.dot(p, UL) ) time = self.run_time # Count points dots = VGroup(*[ Dot(sp, radius=0.025) for sp in start_points ]) dots.set_color_by_gradient(PINK, BLUE, YELLOW) words = TextMobject( "Spectrum of\\\\", "initial conditions" ) words.set_stroke(BLACK, 5, background=True) words.next_to(dots, UP) self.play( # ShowIncreasingSubsets(dots, run_time=2), LaggedStartMap( FadeInFromLarge, dots, lambda m: (m, 10), run_time=2 ), FadeInFromDown(words), ) self.wait() trajs = VGroup() for sp in start_points: trajs.add( self.get_trajectory( sp, time, added_steps=10, ) ) for traj, dot in zip(trajs, dots): traj.set_stroke(dot.get_color(), 1) def update_dots(ds): for d, t in zip(ds, trajs): d.move_to(t.points[-1]) return ds dots.add_updater(update_dots) self.add(dots, trajs, words) self.play( ShowCreation( trajs, lag_ratio=0, ), rate_func=linear, run_time=time, ) self.wait() class AskAboutStability(ShowHighVelocityCase): CONFIG = { "initial_theta": 60 * DEGREES, "initial_theta_dot": 1, } def construct(self): self.initialize_plane_and_field() self.add_flexible_state() self.show_fixed_points() self.label_fixed_points() self.ask_about_stability() self.show_nudges() def show_fixed_points(self): state1 = self.state plane = self.plane dot1 = self.dot state2 = self.get_flexible_state_picture() state2.to_corner(DR, buff=SMALL_BUFF) dot2 = self.get_state_controlling_dot(state2) dot2.set_color(BLUE) fp1 = plane.coords_to_point(0, 0) fp2 = plane.coords_to_point(PI, 0) self.play( dot1.move_to, fp1, run_time=3, ) self.wait() self.play(FadeIn(state2)) self.play( dot2.move_to, fp2, path_arc=-30 * DEGREES, run_time=2, ) self.wait() self.state1 = state1 self.state2 = state2 self.dot1 = dot1 self.dot2 = dot2 def label_fixed_points(self): dots = VGroup(self.dot1, self.dot2) label = TextMobject("Fixed points") label.scale(1.5) label.set_stroke(BLACK, 5, background=True) label.next_to(dots, UP, buff=2) label.shift(SMALL_BUFF * DOWN) arrows = VGroup(*[ Arrow( label.get_bottom(), dot.get_center(), color=dot.get_color(), ) for dot in dots ]) self.play( self.vector_field.set_opacity, 0.5, FadeInFromDown(label) ) self.play(ShowCreation(arrows)) self.wait(2) self.to_fade = VGroup(label, arrows) def ask_about_stability(self): question = TextMobject("Stable?") question.scale(2) question.shift(FRAME_WIDTH * RIGHT / 4) question.to_edge(UP) question.set_stroke(BLACK, 5, background=True) self.play(Write(question)) self.play(FadeOut(self.to_fade)) def show_nudges(self): dots = VGroup(self.dot1, self.dot2) time = 20 self.play(*[ ApplyMethod( dot.shift, 0.1 * UL, rate_func=rush_from, ) for dot in dots ]) trajs = VGroup() for dot in dots: traj = self.get_trajectory( dot.get_center(), time, ) traj.set_stroke(dot.get_color(), 2) trajs.add(traj) def update_dots(ds): for t, d in zip(trajs, ds): d.move_to(t.points[-1]) dots.add_updater(update_dots) self.add(trajs, dots) self.play( ShowCreation(trajs, lag_ratio=0), rate_func=linear, run_time=time ) self.wait() class LovePhaseSpace(ShowHighVelocityCase): CONFIG = { "vector_field_config": { "max_magnitude": 4, # "delta_x": 2, # "delta_y": 2, }, "a": 0.5, "b": 0.3, "mu": 0.2, } def construct(self): self.setup_plane() self.add_equations() self.show_vector_field() self.show_example_trajectories() self.add_resistance_term() self.show_new_trajectories() def setup_plane(self): plane = self.plane = NumberPlane() plane.add_coordinates() self.add(plane) h1, h2 = hearts = VGroup(*[ get_heart_var(i) for i in (1, 2) ]) hearts.scale(0.5) hearts.set_stroke(BLACK, 5, background=True) h1.next_to(plane.x_axis.get_right(), UL, SMALL_BUFF) h2.next_to(plane.y_axis.get_top(), DR, SMALL_BUFF) for h in hearts: h.shift_onto_screen(buff=MED_SMALL_BUFF) plane.add(hearts) self.axis_hearts = hearts def add_equations(self): equations = VGroup( get_love_equation1(), get_love_equation2(), ) equations.scale(0.5) equations.arrange( DOWN, aligned_edge=LEFT, buff=MED_LARGE_BUFF ) equations.to_corner(UL) equations.add_background_rectangle_to_submobjects() # for eq in equations: # eq.add_background_rectangle_to_submobjects() self.add(equations) self.equations = equations def show_vector_field(self): field = VectorField( lambda p: np.array([ self.a * p[1], -self.b * p[0], 0 ]), **self.vector_field_config ) field.sort(get_norm) x_range = np.arange(-7, 7.5, 0.5) y_range = np.arange(-4, 4.5, 0.5) x_axis_arrows = VGroup(*[ field.get_vector([x, 0, 0]) for x in x_range ]) y_axis_arrows = VGroup(*[ field.get_vector([0, y, 0]) for y in y_range ]) axis_arrows = VGroup(*x_axis_arrows, *y_axis_arrows) axis_arrows.save_state() for arrow in axis_arrows: real_len = get_norm(field.func(arrow.get_start())) arrow.scale( 0.5 * real_len / arrow.get_length(), about_point=arrow.get_start() ) self.play( LaggedStartMap(GrowArrow, x_axis_arrows), ) self.play( LaggedStartMap(GrowArrow, y_axis_arrows), ) self.wait() self.add(field, self.equations, self.axis_hearts) self.play( axis_arrows.restore, # axis_arrows.fade, 1, ShowCreation(field), run_time=3 ) self.remove(axis_arrows) self.wait() self.field = self.vector_field = field def show_example_trajectories(self): n_points = 20 total_time = 30 start_points = self.start_points = [ 2.5 * np.random.random() * rotate_vector( RIGHT, TAU * np.random.random() ) for x in range(n_points) ] dots = VGroup(*[Dot(sp) for sp in start_points]) dots.set_color_by_gradient(BLUE, WHITE) words = TextMobject("Possible initial\\\\", "conditions") words.scale(1.5) words.add_background_rectangle_to_submobjects() words.set_stroke(BLACK, 5, background=True) words.shift(FRAME_WIDTH * RIGHT / 4) words.to_edge(UP) self.possibility_words = words self.play( LaggedStartMap( FadeInFromLarge, dots, lambda m: (m, 5) ), FadeInFromDown(words) ) trajs = VGroup(*[ self.get_trajectory( sp, total_time, added_steps=10, ) for sp in start_points ]) trajs.set_color_by_gradient(BLUE, WHITE) dots.trajs = trajs def update_dots(ds): for d, t in zip(ds, ds.trajs): d.move_to(t.points[-1]) dots.add_updater(update_dots) self.add(trajs, dots) self.play( ShowCreation( trajs, lag_ratio=0, run_time=10, rate_func=linear, ) ) self.trajs = trajs self.dots = dots def add_resistance_term(self): added_term = VGroup( TexMobject("-\\mu"), get_heart_var(2).scale(0.5), ) added_term.arrange(RIGHT, buff=SMALL_BUFF) equation2 = self.equations[1] equation2.generate_target() br, deriv, eq, neg_b, h1 = equation2.target added_term.next_to(eq, RIGHT, SMALL_BUFF) added_term.align_to(h1, DOWN) VGroup(neg_b, h1).next_to( added_term, RIGHT, SMALL_BUFF, aligned_edge=DOWN, ) br.stretch(1.2, 0, about_edge=LEFT) brace = Brace(added_term, DOWN, buff=SMALL_BUFF) words = brace.get_text( "``Resistance'' term" ) words.set_stroke(BLACK, 5, background=True) words.add_background_rectangle() self.add(equation2, added_term) self.play( MoveToTarget(equation2), FadeInFromDown(added_term), GrowFromCenter(brace), Write(words), ) self.play(ShowCreationThenFadeAround(added_term)) equation2.add(added_term, brace, words) def show_new_trajectories(self): dots = self.dots trajs = self.trajs field = self.field new_field = VectorField( lambda p: np.array([ self.a * p[1], -self.mu * p[1] - self.b * p[0], 0 ]), **self.vector_field_config ) new_field.sort(get_norm) field.generate_target() for vect in field.target: vect.become(new_field.get_vector(vect.get_start())) self.play(*map( FadeOut, [trajs, dots, self.possibility_words] )) self.play(MoveToTarget(field)) self.vector_field = new_field total_time = 30 new_trajs = VGroup(*[ self.get_trajectory( sp, total_time, added_steps=10, ) for sp in self.start_points ]) new_trajs.set_color_by_gradient(BLUE, WHITE) dots.trajs = new_trajs self.add(new_trajs, dots) self.play( ShowCreation( new_trajs, lag_ratio=0, run_time=10, rate_func=linear, ), ) self.wait() class TakeManyTinySteps(IntroduceVectorField): CONFIG = { "initial_theta": 60 * DEGREES, "initial_theta_dot": 0, "initial_theta_tex": "\\pi / 3", "initial_theta_dot_tex": "0", } def construct(self): self.initialize_plane_and_field() self.take_many_time_steps() def initialize_plane_and_field(self): self.initialize_plane() self.initialize_vector_field() field = self.vector_field field.set_opacity(0.35) self.add(self.plane, field) def take_many_time_steps(self): self.setup_trackers() delta_t_tracker = self.delta_t_tracker get_delta_t = delta_t_tracker.get_value time_tracker = self.time_tracker get_t = time_tracker.get_value traj = always_redraw( lambda: self.get_time_step_trajectory( get_delta_t(), get_t(), self.initial_theta, self.initial_theta_dot, ) ) vectors = always_redraw( lambda: self.get_path_vectors( get_delta_t(), get_t(), self.initial_theta, self.initial_theta_dot, ) ) # Labels labels, init_labels = self.get_labels(get_t, get_delta_t) t_label, dt_label = labels theta_t_label = TexMobject("\\theta(t)...\\text{ish}") theta_t_label.scale(0.75) theta_t_label.add_updater(lambda m: m.next_to( vectors[-1].get_end(), vectors[-1].get_vector(), SMALL_BUFF, )) self.add(traj, vectors, init_labels, labels) time_tracker.set_value(0) target_time = 10 self.play( VFadeIn(theta_t_label), ApplyMethod( time_tracker.set_value, target_time, run_time=5, rate_func=linear, ) ) self.wait() t_label[-1].clear_updaters() self.remove(theta_t_label) target_delta_t = 0.01 self.play( delta_t_tracker.set_value, target_delta_t, run_time=7, ) self.wait() traj.clear_updaters() vectors.clear_updaters() # Show steps count_tracker = ValueTracker(0) count = Integer() count.scale(1.5) count.to_edge(LEFT) count.shift(UP + MED_SMALL_BUFF * UR) count.add_updater(lambda c: c.set_value( count_tracker.get_value() )) count_label = TextMobject("steps") count_label.scale(1.5) count_label.add_updater( lambda m: m.next_to( count[-1], RIGHT, submobject_to_align=m[0][0], aligned_edge=DOWN ) ) scaled_vectors = vectors.copy() scaled_vectors.clear_updaters() for vector in scaled_vectors: vector.scale( 1 / vector.get_length(), about_point=vector.get_start() ) vector.set_color(YELLOW) def update_scaled_vectors(group): group.set_opacity(0) group[min( int(count.get_value()), len(group) - 1, )].set_opacity(1) scaled_vectors.add_updater(update_scaled_vectors) self.add(count, count_label, scaled_vectors) self.play( ApplyMethod( count_tracker.set_value, int(target_time / target_delta_t), rate_func=linear, ), run_time=5, ) self.play(FadeOut(scaled_vectors)) self.wait() def setup_trackers(self): self.delta_t_tracker = ValueTracker(0.5) self.time_tracker = ValueTracker(10) def get_labels(self, get_t, get_delta_t): t_label, dt_label = labels = VGroup(*[ VGroup( TexMobject("{} = ".format(s)), DecimalNumber(0) ).arrange(RIGHT, aligned_edge=DOWN) for s in ("t", "{\\Delta t}") ]) dt_label[-1].add_updater( lambda d: d.set_value(get_delta_t()) ) t_label[-1].add_updater( lambda d: d.set_value( int(np.ceil(get_t() / get_delta_t())) * get_delta_t() ) ) init_labels = VGroup( TexMobject( "\\theta_0", "=", self.initial_theta_tex, tex_to_color_map={"\\theta": BLUE}, ), TexMobject( "{\\dot\\theta}_0 =", self.initial_theta_dot_tex, tex_to_color_map={"{\\dot\\theta}": YELLOW}, ), ) for group in labels, init_labels: for label in group: label.scale(1.25) label.add_background_rectangle() group.arrange(DOWN) group.shift(FRAME_WIDTH * RIGHT / 4) labels.to_edge(UP) init_labels.shift(2 * DOWN) return labels, init_labels # def get_time_step_points(self, delta_t, total_time, theta_0, theta_dot_0): plane = self.plane field = self.vector_field curr_point = plane.coords_to_point( theta_0, theta_dot_0, ) 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, theta_0, theta_dot_0): traj = VMobject() traj.set_points_as_corners( self.get_time_step_points( delta_t, total_time, theta_0, theta_dot_0, ) ) traj.set_stroke(WHITE, 2) return traj def get_path_vectors(self, delta_t, total_time, theta_0, theta_dot_0): corners = self.get_time_step_points( delta_t, total_time, theta_0, theta_dot_0, ) 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 class SetupToTakingManyTinySteps(TakeManyTinySteps): CONFIG = { } def construct(self): self.initialize_plane_and_field() self.show_step() def show_step(self): self.setup_trackers() get_delta_t = self.delta_t_tracker.get_value get_t = self.time_tracker.get_value labels, init_labels = self.get_labels(get_t, get_delta_t) t_label, dt_label = labels dt_part = dt_label[1][0][:-1].copy() init_labels_rect = SurroundingRectangle(init_labels) init_labels_rect.set_color(PINK) field = self.vector_field point = self.plane.coords_to_point( self.initial_theta, self.initial_theta_dot, ) dot = Dot(point, color=init_labels_rect.get_color()) vector_value = field.func(point) vector = field.get_vector(point) vector.scale( get_norm(vector_value) / vector.get_length(), about_point=vector.get_start() ) scaled_vector = vector.copy() scaled_vector.scale( get_delta_t(), about_point=scaled_vector.get_start() ) v_label = TexMobject("\\vec{\\textbf{v}}") v_label.set_stroke(BLACK, 5, background=True) v_label.next_to(vector, LEFT, SMALL_BUFF) real_field = field.copy() for v in real_field: p = v.get_start() v.scale( get_norm(field.func(p)) / v.get_length(), about_point=p ) self.add(init_labels) self.play(ShowCreation(init_labels_rect)) self.play(ReplacementTransform( init_labels_rect, dot, )) self.wait() self.add(vector, dot) self.play( ShowCreation(vector), FadeInFrom(v_label, RIGHT), ) self.play(FadeInFromDown(dt_label)) self.wait() # v_label.generate_target() dt_part.generate_target() dt_part.target.next_to(scaled_vector, LEFT, SMALL_BUFF) v_label.target.next_to(dt_part.target, LEFT, SMALL_BUFF) rect = BackgroundRectangle( VGroup(v_label.target, dt_part.target) ) self.add(rect, v_label, dt_part) self.play( ReplacementTransform(vector, scaled_vector), FadeIn(rect), MoveToTarget(v_label), MoveToTarget(dt_part), ) self.add(scaled_vector, dot) self.wait() self.play( LaggedStart(*[ Transform( sm1, sm2, rate_func=there_and_back_with_pause, ) for sm1, sm2 in zip(field, real_field) ], lag_ratio=0.001, run_time=3) ) self.wait() class ShowClutterPrevention(SetupToTakingManyTinySteps): def construct(self): self.initialize_plane_and_field() # Copied from above scene field = self.vector_field real_field = field.copy() for v in real_field: p = v.get_start() v.scale( get_norm(field.func(p)) / v.get_length(), about_point=p ) self.play( LaggedStart(*[ Transform( sm1, sm2, rate_func=there_and_back_with_pause, ) for sm1, sm2 in zip(field, real_field) ], lag_ratio=0.001, run_time=3) ) self.wait() class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps): CONFIG = { "initial_thetas": np.linspace(0.1, PI - 0.1, 10), "initial_theta_dot": 0, } def construct(self): self.initialize_plane_and_field() self.take_many_time_steps() def take_many_time_steps(self): delta_t_tracker = ValueTracker(0.2) get_delta_t = delta_t_tracker.get_value time_tracker = ValueTracker(10) get_t = time_tracker.get_value # traj = always_redraw( # lambda: VGroup(*[ # self.get_time_step_trajectory( # get_delta_t(), # get_t(), # theta, # self.initial_theta_dot, # ) # for theta in self.initial_thetas # ]) # ) vectors = always_redraw( lambda: VGroup(*[ self.get_path_vectors( get_delta_t(), get_t(), theta, self.initial_theta_dot, ) for theta in self.initial_thetas ]) ) self.add(vectors) time_tracker.set_value(0) self.play( time_tracker.set_value, 5, run_time=5, rate_func=linear, ) class Thumbnail(IntroduceVectorField): CONFIG = { "vector_field_config": { # "delta_x": 0.5, # "delta_y": 0.5, # "max_magnitude": 5, # "length_func": lambda norm: 0.5 * sigmoid(norm), "delta_x": 1, "delta_y": 1, "max_magnitude": 5, "length_func": lambda norm: 0.9 * sigmoid(norm), }, "big_pendulum_config": { "damping": 0.4, }, } def construct(self): self.initialize_plane() self.plane.axes.set_stroke(width=0.5) self.initialize_vector_field() field = self.vector_field field.set_stroke(width=5) for vector in field: vector.set_stroke(width=8) vector.tip.set_stroke(width=0) vector.tip.scale(1.5, about_point=vector.get_last_point()) vector.set_opacity(1) title = TextMobject("Differential\\\\", "equations") title.space_out_submobjects(0.8) # title.scale(3) title.set_width(FRAME_WIDTH - 3) # title.to_edge(UP) # title[1].to_edge(DOWN) subtitle = TextMobject("Studying the unsolvable") subtitle.set_width(FRAME_WIDTH - 1) subtitle.set_color(WHITE) subtitle.to_edge(DOWN, buff=1) # title.center() title.to_edge(UP, buff=1) title.add(subtitle) # title.set_stroke(BLACK, 15, background=True) # title.add_background_rectangle_to_submobjects(opacity=0.5) title.set_stroke(BLACK, 15, background=True) subtitle.set_stroke(RED, 2, background=True) # for part in title: # part[0].set_fill(opacity=0.25) # part[0].set_stroke(width=0) black_parts = VGroup() for mob in title.family_members_with_points(): for sp in mob.get_subpaths(): new_mob = VMobject() new_mob.set_points(sp) new_mob.set_fill(BLACK, 0.25) new_mob.set_stroke(width=0) black_parts.add(new_mob) # for vect in field: # for mob in title.family_members_with_points(): # for p in [vect.get_start(), vect.get_end()]: # x, y = p[:2] # x0, y0 = mob.get_corner(DL)[:2] # x1, y1 = mob.get_corner(UR)[:2] # if x0 < x < x1 and y0 < y < y1: # vect.set_opacity(0.25) # vect.tip.set_stroke(width=0) self.add(self.plane) self.add(field) # self.add(black_parts) # self.add(title) self.add_line(self.plane) def add_line(self, axes): func = self.vector_field_func line = VMobject() line.start_new_path(axes.c2p(-TAU, 3.5)) dt = 0.1 t = 0 total_time = 40 while t < total_time: t += dt last_point = line.get_last_point() new_point = last_point + dt * func(last_point) if new_point[0] > FRAME_WIDTH / 2: new_point = last_point + FRAME_WIDTH * LEFT line.start_new_path(new_point) else: line.add_smooth_curve_to(new_point) line.set_stroke(WHITE, 6) line.make_smooth() self.add(line)