diff --git a/active_projects/diffyq/all_part1_scenes.py b/active_projects/diffyq/all_part1_scenes.py new file mode 100644 index 00000000..0ca648d4 --- /dev/null +++ b/active_projects/diffyq/all_part1_scenes.py @@ -0,0 +1,107 @@ +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" +SCENES_IN_ORDER = [ + WhenChangeIsEasier, + VectorFieldTest, + IntroducePendulum, + MultiplePendulumsOverlayed, + PeriodFormula, + FormulasAreLies, + MediumAnglePendulum, + MediumHighAnglePendulum, + HighAnglePendulum, + LowAnglePendulum, + SomeOfYouWatching, + SmallAngleApproximationTex, + VeryLowAnglePendulum, + FormulasAreLies, + TourOfDifferentialEquations, + WherePendulumLeads, + LongDoublePendulum, + # FollowThisThread, + StrogatzQuote, + ShowHorizontalDashedLine, + RabbitFoxPopulations, + RabbitFoxEquation, + # Something... + ShowSimpleTrajectory, + SimpleProjectileEquation, + SimpleProjectileEquationVGraphFreedom, + ShowGravityAcceleration, + UniversalGravityLawSymbols, + ExampleTypicalODE, + AnalyzePendulumForce, + ShowSineValues, + BuildUpEquation, + AirResistanceBrace, + ShowDerivativeVideo, + SubtleAirCurrents, + SimpleDampenedPendulum, + DefineODE, + SecondOrderEquationExample, + ODEvsPDEinFrames, + ProveTeacherWrong, + SetAsideSeekingSolution, + # + WriteInRadians, + XEqLThetaToCorner, + ComingUp, + InputLabel, + SoWhatIsThetaThen, + ReallyHardToSolve, + ReasonForSolution, + PhysicistPhaseSpace, + GleickQuote, + SpectrumOfStartingStates, + WritePhaseFlow, + AskAboutStability, + LoveExample, + PassageOfTime, + LovePhaseSpace, + ComparePhysicsToLove, + FramesComparingPhysicsToLove, + SetupToTakingManyTinySteps, + ShowClutterPrevention, + # VisualizeHeightSlopeCurvature, + VisualizeStates, + ReferencePiCollisionStateSpaces, + IntroduceVectorField, + XComponentArrows, + BreakingSecondOrderIntoTwoFirstOrder, + ShowPendulumPhaseFlow, + ShowHighVelocityCase, + TweakMuInFormula, + TweakMuInVectorField, + FromODEToVectorField, + LorenzVectorField, + ThreeBodiesInSpace, + AltThreeBodiesInSpace, + TwoBodiesInSpace, + TwoBodiesWithZPart, + ThreeBodyTitle, + ThreeBodySymbols, + # + HighAmplitudePendulum, + WritePhaseSpace, + # + AskAboutActuallySolving, + WriteODESolvingCode, + TakeManyTinySteps, + ManyStepsFromDifferentStartingPoints, + InaccurateComputation, + HungerForExactness, + ShowRect, + ShowSquare, + JumpToThisPoint, + ThreeBodyEquation, + ItGetsWorse, + ChaosTitle, + RevisitQuote, + EndScreen, + Thumbnail, +] diff --git a/active_projects/diffyq/all_part2_scenes.py b/active_projects/diffyq/all_part2_scenes.py new file mode 100644 index 00000000..c3883973 --- /dev/null +++ b/active_projects/diffyq/all_part2_scenes.py @@ -0,0 +1,41 @@ +from active_projects.ode.part2.staging import * +from active_projects.ode.part2.fourier_series import * +from active_projects.ode.part2.heat_equation import * +from active_projects.ode.part2.pi_scenes import * +from active_projects.ode.part2.wordy_scenes import * + +OUTPUT_DIRECTORY = "ode/part2" +SCENES_IN_ORDER = [ + PartTwoOfTour, + HeatEquationIntroTitle, + BrownianMotion, + BlackScholes, + ContrastChapters1And2, + FourierSeriesIntro, + FourierSeriesIntroBackground20, + ExplainCircleAnimations, + # FourierSeriesIntroBackground4, + # FourierSeriesIntroBackground8, + # FourierSeriesIntroBackground12, + TwoDBodyWithManyTemperatures, + TwoDBodyWithManyTemperaturesGraph, + TwoDBodyWithManyTemperaturesContour, + BringTwoRodsTogether, + ShowEvolvingTempGraphWithArrows, + # TodaysTargetWrapper, + WriteHeatEquation, + ReactionsToInitialHeatEquation, + TalkThrough1DHeatGraph, + ShowCubeFormation, + CompareInputsOfGeneralCaseTo1D, + ContrastXChangesToTChanges, + ShowPartialDerivativeSymbols, + WriteHeatEquation, + ShowCurvatureToRateOfChangeIntuition, + ContrastPDEToODE, + TransitionToTempVsTime, + Show1DAnd3DEquations, + # + AskAboutWhereEquationComesFrom, + DiscreteSetup, +] diff --git a/active_projects/diffyq/all_part3_scenes.py b/active_projects/diffyq/all_part3_scenes.py new file mode 100644 index 00000000..6fe24953 --- /dev/null +++ b/active_projects/diffyq/all_part3_scenes.py @@ -0,0 +1,70 @@ +from active_projects.ode.part3.staging import * +from active_projects.ode.part3.temperature_graphs import * +from active_projects.ode.part3.pi_creature_scenes import * +from active_projects.ode.part3.wordy_scenes import * +from active_projects.ode.part3.discrete_case import * + + +OUTPUT_DIRECTORY = "ode/part3" +SCENES_IN_ORDER = [ + LastChapterWrapper, + ThreeConstraints, + OceanOfPossibilities, + ThreeMainObservations, + SimpleCosExpGraph, + AddMultipleSolutions, + FourierSeriesIllustraiton, + BreakDownAFunction, + SineCurveIsUnrealistic, + AnalyzeSineCurve, + EquationAboveSineAnalysis, + ExponentialDecay, + InvestmentGrowth, + GrowingPileOfMoney, + CarbonDecayCurve, + CarbonDecayingInMammoth, + SineWaveScaledByExp, + ShowSinExpDerivatives, + IfOnly, + BoundaryConditionInterlude, + BoundaryConditionReference, + GiantCross, + SimulateRealSineCurve, + DerivativesOfLinearFunction, + StraightLine3DGraph, + SimulateLinearGraph, + EmphasizeBoundaryPoints, + ShowNewRuleAtDiscreteBoundary, + DiscreteEvolutionPoint25, + DiscreteEvolutionPoint1, + FlatEdgesForDiscreteEvolution, + FlatEdgesForDiscreteEvolutionTinySteps, + FlatEdgesContinuousEvolution, + FlatAtBoundaryWords, + SlopeToHeatFlow, + CloserLookAtStraightLine, + WriteOutBoundaryCondition, + SoWeGotNowhere, + ManipulateSinExpSurface, + HeatEquationFrame, + ShowFreq1CosExpDecay, + ShowFreq2CosExpDecay, + ShowFreq4CosExpDecay, + CompareFreqDecays1to2, + CompareFreqDecays1to4, + CompareFreqDecays2to4, + ShowHarmonics, + ShowHarmonicSurfaces, + + # SimpleCosExpGraph, + # AddMultipleSolutions, + # IveHeardOfThis, + # FourierSeriesOfLineIllustration, + # InFouriersShoes, +] + +PART_4_SCENES = [ + FourierSeriesIllustraiton, + FourierNameIntro, + CircleAnimationOfF, +] diff --git a/active_projects/diffyq/all_part4_scenes.py b/active_projects/diffyq/all_part4_scenes.py new file mode 100644 index 00000000..7e95486f --- /dev/null +++ b/active_projects/diffyq/all_part4_scenes.py @@ -0,0 +1,18 @@ +from active_projects.ode.part4.staging import * +from active_projects.ode.part4.fourier_series_scenes import * +from active_projects.ode.part4.pi_creature_scenes import * + +OUTPUT_DIRECTORY = "ode/part4" +SCENES_IN_ORDER = [ + ComplexFourierSeriesExample, + ComplexFourierSeriesExampleEnd, + FourierSeriesExampleWithRectForZoom, + ZoomedInFourierSeriesExample, + RelationToOtherVideos, + WhyWouldYouCare, + # Oldies + + # FourierSeriesIllustraiton, + # FourierNameIntro, + # CircleAnimationOfF, +] diff --git a/active_projects/diffyq/name_animations.py b/active_projects/diffyq/name_animations.py new file mode 100644 index 00000000..b96c00e9 --- /dev/null +++ b/active_projects/diffyq/name_animations.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +from manimlib.imports import * +from active_projects.ode.part2.fourier_series import FourierOfName + +name_color_pairs = [ + +] + +circle_counts = [ + # 10, + # 25, + 100, +] + +if __name__ == "__main__": + for name, color in name_color_pairs: + for n_circles in circle_counts: + try: + first_name = name.split(" ")[0] + scene = FourierOfName( + name_text=name, + name_color=color, + n_circles=n_circles, + file_writer_config={ + "write_to_movie": True, + "output_directory": os.path.join( + "patron_fourier_names", + first_name, + ), + "file_name": "{}_Fouierified_{}_Separate_paths".format( + first_name, + n_circles + ), + }, + camera_config={ + "frame_rate": 24, + }, + ) + except: + pass diff --git a/active_projects/diffyq/part1/pendulum.py b/active_projects/diffyq/part1/pendulum.py new file mode 100644 index 00000000..de801579 --- /dev/null +++ b/active_projects/diffyq/part1/pendulum.py @@ -0,0 +1,1893 @@ +from manimlib.imports import * +from active_projects.ode.part1.shared_constructs import * + + +class Pendulum(VGroup): + CONFIG = { + "length": 3, + "gravity": 9.8, + "weight_diameter": 0.5, + "initial_theta": 0.3, + "omega": 0, + "damping": 0.1, + "top_point": 2 * UP, + "rod_style": { + "stroke_width": 3, + "stroke_color": LIGHT_GREY, + "sheen_direction": UP, + "sheen_factor": 1, + }, + "weight_style": { + "stroke_width": 0, + "fill_opacity": 1, + "fill_color": GREY_BROWN, + "sheen_direction": UL, + "sheen_factor": 0.5, + "background_stroke_color": BLACK, + "background_stroke_width": 3, + "background_stroke_opacity": 0.5, + }, + "dashed_line_config": { + "num_dashes": 25, + "stroke_color": WHITE, + "stroke_width": 2, + }, + "angle_arc_config": { + "radius": 1, + "stroke_color": WHITE, + "stroke_width": 2, + }, + "velocity_vector_config": { + "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): + super().__init__(**kwargs) + self.create_fixed_point() + self.create_rod() + self.create_weight() + self.rotating_group = VGroup(self.rod, self.weight) + self.create_dashed_line() + self.create_angle_arc() + 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() + + def create_fixed_point(self): + self.fixed_point_tracker = VectorizedPoint(self.top_point) + self.add(self.fixed_point_tracker) + return self + + def create_rod(self): + rod = self.rod = Line(UP, DOWN) + rod.set_height(self.length) + rod.set_style(**self.rod_style) + rod.move_to(self.get_fixed_point(), UP) + self.add(rod) + + def create_weight(self): + weight = self.weight = Circle() + weight.set_width(self.weight_diameter) + weight.set_style(**self.weight_style) + weight.move_to(self.rod.get_end()) + self.add(weight) + + def create_dashed_line(self): + line = self.dashed_line = DashedLine( + self.get_fixed_point(), + 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_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( + multiple * RIGHT, + **self.velocity_vector_config, + ) + vector.rotate(theta, about_point=ORIGIN) + vector.shift(self.rod.get_end()) + return vector + + self.velocity_vector = always_redraw(make_vector) + self.add(self.velocity_vector) + return self + + def add_theta_label(self): + self.theta_label = always_redraw(self.get_label) + self.add(self.theta_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): + theta = self.rod.get_angle() - self.dashed_line.get_angle() + theta = (theta + PI) % TAU - PI + return theta + + def set_theta(self, theta): + self.rotating_group.rotate( + theta - self.get_theta() + ) + self.rotating_group.shift( + self.get_fixed_point() - self.rod.get_start(), + ) + return self + + def get_omega(self): + return self.omega + + def set_omega(self, omega): + self.omega = omega + return self + + def get_fixed_point(self): + return self.fixed_point_tracker.get_location() + + # + def start_swinging(self): + self.add_updater(Pendulum.update_by_gravity) + + def end_swinging(self): + self.remove_updater(Pendulum.update_by_gravity) + + def update_by_gravity(self, dt): + theta = self.get_theta() + omega = self.get_omega() + nspf = self.n_steps_per_frame + for x in range(nspf): + d_theta = omega * dt / nspf + d_omega = op.add( + -self.damping * omega, + -(self.gravity / self.length) * np.sin(theta), + ) * dt / nspf + theta += d_theta + omega += d_omega + self.set_theta(theta) + self.set_omega(omega) + return self + + +class GravityVector(Vector): + CONFIG = { + "color": YELLOW, + "length_multiple": 1 / 9.8, + # TODO, continually update the length based + # on the pendulum's gravity? + } + + def __init__(self, pendulum, **kwargs): + super().__init__(DOWN, **kwargs) + self.pendulum = pendulum + self.scale(self.length_multiple * pendulum.gravity) + self.attach_to_pendulum(pendulum) + + def attach_to_pendulum(self, pendulum): + self.add_updater(lambda m: m.shift( + pendulum.weight.get_center() - self.get_start(), + )) + + def add_component_lines(self): + self.component_lines = always_redraw(self.create_component_lines) + self.add(self.component_lines) + + def create_component_lines(self): + theta = self.pendulum.get_theta() + x_new = rotate(RIGHT, theta) + base = self.get_start() + tip = self.get_end() + vect = tip - base + corner = base + x_new * np.dot(vect, x_new) + kw = {"dash_length": 0.025} + return VGroup( + DashedLine(base, corner, **kw), + DashedLine(corner, tip, **kw), + ) + + +class ThetaValueDisplay(VGroup): + CONFIG = { + + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class ThetaVsTAxes(Axes): + CONFIG = { + "x_min": 0, + "x_max": 8, + "y_min": -PI / 2, + "y_max": PI / 2, + "y_axis_config": { + "tick_frequency": PI / 8, + "unit_size": 1.5, + }, + "number_line_config": { + "color": "#EEEEEE", + "stroke_width": 2, + "include_tip": False, + }, + "graph_style": { + "stroke_color": GREEN, + "stroke_width": 3, + "fill_opacity": 0, + }, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.add_labels() + + def add_axes(self): + self.axes = Axes(**self.axes_config) + self.add(self.axes) + + def add_labels(self): + x_axis = self.get_x_axis() + y_axis = self.get_y_axis() + + t_label = self.t_label = TexMobject("t") + t_label.next_to(x_axis.get_right(), UP, MED_SMALL_BUFF) + x_axis.label = t_label + x_axis.add(t_label) + theta_label = self.theta_label = TexMobject("\\theta(t)") + theta_label.next_to(y_axis.get_top(), UP, SMALL_BUFF) + y_axis.label = theta_label + y_axis.add(theta_label) + + self.y_axis_label = theta_label + self.x_axis_label = t_label + + x_axis.add_numbers() + y_axis.add(self.get_y_axis_coordinates(y_axis)) + + def get_y_axis_coordinates(self, y_axis): + texs = [ + # "\\pi \\over 4", + # "\\pi \\over 2", + # "3 \\pi \\over 4", + # "\\pi", + "\\pi / 4", + "\\pi / 2", + "3 \\pi / 4", + "\\pi", + ] + values = np.arange(1, 5) * PI / 4 + labels = VGroup() + for pos_tex, pos_value in zip(texs, values): + neg_tex = "-" + pos_tex + neg_value = -1 * pos_value + for tex, value in (pos_tex, pos_value), (neg_tex, neg_value): + if value > self.y_max or value < self.y_min: + continue + symbol = TexMobject(tex) + symbol.scale(0.5) + point = y_axis.number_to_point(value) + symbol.next_to(point, LEFT, MED_SMALL_BUFF) + labels.add(symbol) + return labels + + def get_live_drawn_graph(self, pendulum, + t_max=None, + t_step=1.0 / 60, + **style): + style = merge_dicts_recursively(self.graph_style, style) + if t_max is None: + t_max = self.x_max + + graph = VMobject() + graph.set_style(**style) + + graph.all_coords = [(0, pendulum.get_theta())] + graph.time = 0 + graph.time_of_last_addition = 0 + + def update_graph(graph, dt): + graph.time += dt + if graph.time > t_max: + graph.remove_updater(update_graph) + return + new_coords = (graph.time, pendulum.get_theta()) + if graph.time - graph.time_of_last_addition >= t_step: + graph.all_coords.append(new_coords) + graph.time_of_last_addition = graph.time + points = [ + self.coords_to_point(*coords) + for coords in [*graph.all_coords, new_coords] + ] + graph.set_points_smoothly(points) + + graph.add_updater(update_graph) + return graph + + +# Scenes +class IntroducePendulum(PiCreatureScene, MovingCameraScene): + CONFIG = { + "pendulum_config": { + "length": 3, + "top_point": 4 * RIGHT, + "weight_diameter": 0.35, + "gravity": 20, + }, + "theta_vs_t_axes_config": { + "y_max": PI / 4, + "y_min": -PI / 4, + "y_axis_config": { + "tick_frequency": PI / 16, + "unit_size": 2, + "tip_length": 0.3, + }, + "x_max": 12, + "number_line_config": { + "stroke_width": 2, + } + }, + } + + def setup(self): + MovingCameraScene.setup(self) + PiCreatureScene.setup(self) + + def construct(self): + self.add_pendulum() + # self.label_pi_creatures() + self.label_pendulum() + self.add_graph() + self.label_function() + self.show_graph_period() + self.show_length_and_gravity() + # self.tweak_length_and_gravity() + + def create_pi_creatures(self): + randy = Randolph(color=BLUE_C) + morty = Mortimer(color=MAROON_E) + creatures = VGroup(randy, morty) + creatures.scale(0.5) + creatures.arrange(RIGHT, buff=2.5) + creatures.to_corner(DR) + return creatures + + def add_pendulum(self): + pendulum = self.pendulum = Pendulum(**self.pendulum_config) + pendulum.start_swinging() + frame = self.camera_frame + frame.save_state() + frame.scale(0.5) + frame.move_to(pendulum.dashed_line) + + self.add(pendulum, frame) + + def label_pi_creatures(self): + randy, morty = self.pi_creatures + randy_label = TextMobject("Physics\\\\", "student") + morty_label = TextMobject("Physics\\\\", "teacher") + labels = VGroup(randy_label, morty_label) + labels.scale(0.5) + randy_label.next_to(randy, UP, LARGE_BUFF) + morty_label.next_to(morty, UP, LARGE_BUFF) + + for label, pi in zip(labels, self.pi_creatures): + label.arrow = Arrow( + label.get_bottom(), pi.eyes.get_top() + ) + label.arrow.set_color(WHITE) + label.arrow.set_stroke(width=5) + + morty.labels = VGroup( + morty_label, + morty_label.arrow, + ) + + self.play( + FadeInFromDown(randy_label), + GrowArrow(randy_label.arrow), + randy.change, "hooray", + ) + self.play( + Animation(self.pendulum.fixed_point_tracker), + TransformFromCopy(randy_label[0], morty_label[0]), + FadeIn(morty_label[1]), + GrowArrow(morty_label.arrow), + morty.change, "raise_right_hand", + ) + self.wait(2) + + def label_pendulum(self): + pendulum = self.pendulum + randy, morty = self.pi_creatures + label = pendulum.theta_label + rect = SurroundingRectangle(label, buff=0.5 * SMALL_BUFF) + rect.add_updater(lambda r: r.move_to(label)) + + for pi in randy, morty: + pi.add_updater( + lambda m: m.look_at(pendulum.weight) + ) + + self.play(randy.change, "pondering") + self.play(morty.change, "pondering") + self.wait(3) + randy.clear_updaters() + morty.clear_updaters() + self.play( + ShowCreationThenFadeOut(rect), + ) + self.wait() + + def add_graph(self): + axes = self.axes = ThetaVsTAxes(**self.theta_vs_t_axes_config) + axes.y_axis.label.next_to(axes.y_axis, UP, buff=0) + axes.to_corner(UL) + + self.play( + Restore( + self.camera_frame, + rate_func=squish_rate_func(smooth, 0, 0.9), + ), + DrawBorderThenFill( + axes, + rate_func=squish_rate_func(smooth, 0.5, 1), + lag_ratio=0.9, + ), + Transform( + self.pendulum.theta_label.copy().clear_updaters(), + axes.y_axis.label.copy(), + remover=True, + rate_func=squish_rate_func(smooth, 0, 0.8), + ), + run_time=3, + ) + + self.wait(1.5) + self.graph = axes.get_live_drawn_graph(self.pendulum) + self.add(self.graph) + + def label_function(self): + hm_word = TextMobject("Simple harmonic motion") + hm_word.scale(1.25) + hm_word.to_edge(UP) + + formula = TexMobject( + "=\\theta_0 \\cos(\\sqrt{g / L} t)" + ) + formula.next_to( + self.axes.y_axis_label, RIGHT, SMALL_BUFF + ) + formula.set_stroke(width=0, background=True) + + self.play(FadeInFrom(hm_word, DOWN)) + self.wait() + self.play( + Write(formula), + hm_word.to_corner, UR + ) + self.wait(4) + + def show_graph_period(self): + pendulum = self.pendulum + axes = self.axes + + period = self.period = TAU * np.sqrt( + pendulum.length / pendulum.gravity + ) + amplitude = pendulum.initial_theta + + line = Line( + axes.coords_to_point(0, amplitude), + axes.coords_to_point(period, amplitude), + ) + line.shift(SMALL_BUFF * RIGHT) + brace = Brace(line, UP, buff=SMALL_BUFF) + brace.add_to_back(brace.copy().set_style(BLACK, 10)) + formula = get_period_formula() + formula.next_to(brace, UP, SMALL_BUFF) + + self.period_formula = formula + self.period_brace = brace + + self.play( + GrowFromCenter(brace), + FadeInFromDown(formula), + ) + self.wait(2) + + def show_length_and_gravity(self): + formula = self.period_formula + L = formula.get_part_by_tex("L") + g = formula.get_part_by_tex("g") + + rod = self.pendulum.rod + new_rod = rod.copy() + new_rod.set_stroke(BLUE, 7) + new_rod.add_updater(lambda r: r.put_start_and_end_on( + *rod.get_start_and_end() + )) + + g_vect = GravityVector( + self.pendulum, + length_multiple=0.5 / 9.8, + ) + down_vectors = self.get_down_vectors() + down_vectors.set_color(YELLOW) + down_vectors.set_opacity(0.5) + + self.play( + ShowCreationThenDestructionAround(L), + ShowCreation(new_rod), + ) + self.play(FadeOut(new_rod)) + + self.play( + ShowCreationThenDestructionAround(g), + GrowArrow(g_vect), + ) + self.play(self.get_down_vectors_animation(down_vectors)) + self.wait(6) + + self.gravity_vector = g_vect + + def tweak_length_and_gravity(self): + pendulum = self.pendulum + axes = self.axes + graph = self.graph + brace = self.period_brace + formula = self.period_formula + g_vect = self.gravity_vector + randy, morty = self.pi_creatures + + graph.clear_updaters() + period2 = self.period * np.sqrt(2) + period3 = self.period / np.sqrt(2) + amplitude = pendulum.initial_theta + graph2, graph3 = [ + axes.get_graph( + lambda t: amplitude * np.cos(TAU * t / p), + color=RED, + ) + for p in (period2, period3) + ] + formula.add_updater(lambda m: m.next_to( + brace, UP, SMALL_BUFF + )) + + new_pendulum_config = dict(self.pendulum_config) + new_pendulum_config["length"] *= 2 + new_pendulum_config["top_point"] += 3.5 * UP + # new_pendulum_config["initial_theta"] = pendulum.get_theta() + new_pendulum = Pendulum(**new_pendulum_config) + + down_vectors = self.get_down_vectors() + + self.play(randy.change, "happy") + self.play( + ReplacementTransform(pendulum, new_pendulum), + morty.change, "horrified", + morty.shift, 3 * RIGHT, + morty.labels.shift, 3 * RIGHT, + ) + self.remove(morty, morty.labels) + g_vect.attach_to_pendulum(new_pendulum) + new_pendulum.start_swinging() + self.play( + ReplacementTransform(graph, graph2), + brace.stretch, np.sqrt(2), 0, {"about_edge": LEFT}, + ) + self.add(g_vect) + self.wait(3) + + new_pendulum.gravity *= 4 + g_vect.scale(2) + self.play( + FadeOut(graph2), + self.get_down_vectors_animation(down_vectors) + ) + self.play( + FadeIn(graph3), + brace.stretch, 0.5, 0, {"about_edge": LEFT}, + ) + self.wait(6) + + # + def get_down_vectors(self): + down_vectors = VGroup(*[ + Vector(0.5 * DOWN) + for x in range(10 * 150) + ]) + down_vectors.arrange_in_grid(10, 150, buff=MED_SMALL_BUFF) + down_vectors.set_color_by_gradient(BLUE, RED) + # for vect in down_vectors: + # vect.shift(0.1 * np.random.random(3)) + down_vectors.to_edge(RIGHT) + return down_vectors + + def get_down_vectors_animation(self, down_vectors): + return LaggedStart( + *[ + GrowArrow(v, rate_func=there_and_back) + for v in down_vectors + ], + lag_ratio=0.0005, + run_time=2, + remover=True + ) + + +class MultiplePendulumsOverlayed(Scene): + CONFIG = { + "initial_thetas": [ + 150 * DEGREES, + 90 * DEGREES, + 60 * DEGREES, + 30 * DEGREES, + 10 * DEGREES, + ], + "weight_colors": [ + PINK, RED, GREEN, BLUE, GREY, + ], + "pendulum_config": { + "top_point": ORIGIN, + "length": 3, + }, + } + + def construct(self): + pendulums = VGroup(*[ + Pendulum( + initial_theta=theta, + weight_style={ + "fill_color": wc, + "fill_opacity": 0.5, + }, + **self.pendulum_config, + ) + for theta, wc in zip( + self.initial_thetas, + self.weight_colors, + ) + ]) + for pendulum in pendulums: + pendulum.start_swinging() + pendulum.remove(pendulum.theta_label) + + randy = Randolph(color=BLUE_C) + randy.to_corner(DL) + randy.add_updater(lambda r: r.look_at(pendulums[0].weight)) + + axes = ThetaVsTAxes( + x_max=20, + y_axis_config={ + "unit_size": 0.5, + "tip_length": 0.3, + }, + ) + axes.to_corner(UL) + graphs = VGroup(*[ + axes.get_live_drawn_graph( + pendulum, + stroke_color=pendulum.weight.get_color(), + stroke_width=1, + ) + for pendulum in pendulums + ]) + + self.add(pendulums) + self.add(axes, *graphs) + self.play(randy.change, "sassy") + self.wait(2) + self.play(Blink(randy)) + self.wait(5) + self.play(randy.change, "angry") + self.play(Blink(randy)) + self.wait(10) + + +class LowAnglePendulum(Scene): + CONFIG = { + "pendulum_config": { + "initial_theta": 20 * DEGREES, + "length": 2.0, + "damping": 0, + "top_point": ORIGIN, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "x_axis_config": { + "unit_size": 0.5, + "numbers_to_show": range(2, 25, 2), + "number_scale_val": 0.5, + }, + "x_max": 25, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "axes_corner": UL, + } + + def construct(self): + pendulum = Pendulum(**self.pendulum_config) + axes = ThetaVsTAxes(**self.axes_config) + axes.center() + axes.to_corner(self.axes_corner, buff=LARGE_BUFF) + graph = axes.get_live_drawn_graph(pendulum) + + L = pendulum.length + g = pendulum.gravity + theta0 = pendulum.initial_theta + prediction = axes.get_graph( + lambda t: theta0 * np.cos(t * np.sqrt(g / L)) + ) + dashed_prediction = DashedVMobject(prediction, num_dashes=300) + dashed_prediction.set_stroke(WHITE, 1) + prediction_formula = TexMobject( + "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)" + ) + prediction_formula.scale(0.75) + prediction_formula.next_to( + dashed_prediction, UP, SMALL_BUFF, + ) + + theta0 = prediction_formula.get_part_by_tex("\\theta_0") + theta0_brace = Brace(theta0, UP, buff=SMALL_BUFF) + theta0_brace.stretch(0.5, 1, about_edge=DOWN) + theta0_label = Integer( + pendulum.initial_theta * 180 / PI, + unit="^\\circ" + ) + theta0_label.scale(0.75) + theta0_label.next_to(theta0_brace, UP, SMALL_BUFF) + + group = VGroup(theta0_brace, theta0_label, prediction_formula) + group.shift_onto_screen(buff=MED_SMALL_BUFF) + + self.add(axes, dashed_prediction, pendulum) + self.play( + ShowCreation(dashed_prediction, run_time=2), + FadeInFromDown(prediction_formula), + FadeInFromDown(theta0_brace), + FadeInFromDown(theta0_label), + ) + self.play( + ShowCreationThenFadeAround(theta0_label), + ShowCreationThenFadeAround(pendulum.theta_label), + ) + self.wait() + + pendulum.start_swinging() + self.add(graph) + self.wait(30) + + +class ApproxWordsLowAnglePendulum(Scene): + def construct(self): + period = TexMobject( + "\\text{Period}", "\\approx", + "2\\pi \\sqrt{\\,{L} / {g}}", + **Lg_formula_config + ) + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.scale(2) + checkmark.next_to(period, RIGHT, MED_LARGE_BUFF) + + self.add(period, checkmark) + + +class MediumAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 50 * DEGREES, + "n_steps_per_frame": 1000, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "y_max": PI / 2, + "y_min": -PI / 2, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class MediumHighAnglePendulum(MediumAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 90 * DEGREES, + "n_steps_per_frame": 1000, + }, + } + + +class HighAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 175 * DEGREES, + "n_steps_per_frame": 1000, + "top_point": 1.5 * DOWN, + "length": 2, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.5}, + "y_max": PI, + "y_min": -PI, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class VeryLowAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 10 * DEGREES, + "n_steps_per_frame": 1000, + "top_point": ORIGIN, + "length": 3, + }, + "axes_config": { + "y_axis_config": {"unit_size": 2}, + "y_max": PI / 4, + "y_min": -PI / 4, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class WherePendulumLeads(PiCreatureScene): + def construct(self): + pendulum = Pendulum( + top_point=UP, + length=3, + gravity=20, + ) + pendulum.start_swinging() + + l_title = TextMobject("Linearization") + l_title.scale(1.5) + l_title.to_corner(UL) + c_title = TextMobject("Chaos") + c_title.scale(1.5) + c_title.move_to(l_title) + c_title.move_to( + c_title.get_center() * np.array([-1, 1, 1]) + ) + + get_theta = pendulum.get_theta + spring = always_redraw( + lambda: ParametricFunction( + lambda t: np.array([ + np.cos(TAU * t) + (1.4 + get_theta()) * t, + np.sin(TAU * t) - 0.5, + 0, + ]), + t_min=-0.5, + t_max=7, + color=GREY, + sheen_factor=1, + sheen_direction=UL, + ).scale(0.2).to_edge(LEFT, buff=0) + ) + spring_rect = SurroundingRectangle( + spring, buff=MED_LARGE_BUFF, + stroke_width=0, + fill_color=BLACK, + fill_opacity=0, + ) + + weight = Dot(radius=0.25) + weight.add_updater(lambda m: m.move_to( + spring.points[-1] + )) + weight.set_color(BLUE) + weight.set_sheen(1, UL) + spring_system = VGroup(spring, weight) + + linear_formula = TexMobject( + "\\frac{d \\vec{\\textbf{x}}}{dt}=" + "A\\vec{\\textbf{x}}" + ) + linear_formula.next_to(spring, UP, LARGE_BUFF) + linear_formula.match_x(l_title) + + randy = self.pi_creature + randy.set_height(2) + randy.center() + randy.to_edge(DOWN) + randy.shift(3 * LEFT) + q_marks = TexMobject("???") + q_marks.next_to(randy, UP) + + self.add(pendulum, randy) + self.play( + randy.change, "pondering", pendulum, + FadeInFromDown(q_marks, lag_ratio=0.3) + ) + self.play(randy.look_at, pendulum) + self.wait(5) + self.play( + Animation(VectorizedPoint(pendulum.get_top())), + FadeOutAndShift(q_marks, UP, lag_ratio=0.3), + ) + self.add(spring_system) + self.play( + FadeOut(spring_rect), + FadeInFrom(linear_formula, UP), + FadeInFromDown(l_title), + ) + self.play(FadeInFromDown(c_title)) + self.wait(8) + + +class LongDoublePendulum(ExternallyAnimatedScene): + pass + + +class AnalyzePendulumForce(MovingCameraScene): + CONFIG = { + "pendulum_config": { + "length": 5, + "top_point": 3.5 * UP, + "initial_theta": 60 * DEGREES, + "set_theta_label_height_cap": True, + }, + "g_vect_config": { + "length_multiple": 0.25, + }, + "tan_line_color": BLUE, + "perp_line_color": PINK, + } + + def construct(self): + self.add_pendulum() + self.show_arc_length() + self.add_g_vect() + self.show_constraint() + self.break_g_vect_into_components() + self.show_angle_geometry() + self.show_gsin_formula() + self.show_sign() + self.show_acceleration_formula() + # self.ask_about_what_to_do() + + # self.emphasize_theta() + # self.show_angular_velocity() + # self.show_angular_acceleration() + # self.circle_g_sin_formula() + + def add_pendulum(self): + pendulum = Pendulum(**self.pendulum_config) + theta_tracker = ValueTracker(pendulum.get_theta()) + pendulum.add_updater(lambda p: p.set_theta( + theta_tracker.get_value() + )) + + self.add(pendulum) + self.pendulum = pendulum + self.theta_tracker = theta_tracker + + def show_arc_length(self): + pendulum = self.pendulum + angle = pendulum.get_theta() + height = pendulum.length + top = pendulum.get_fixed_point() + + line = Line(UP, DOWN) + line.set_height(height) + line.move_to(top, UP) + arc = always_redraw(lambda: Arc( + start_angle=-90 * DEGREES, + angle=pendulum.get_theta(), + arc_center=pendulum.get_fixed_point(), + radius=pendulum.length, + stroke_color=GREEN, + )) + + brace = Brace(Line(ORIGIN, 5 * UP), RIGHT) + brace.point = VectorizedPoint(brace.get_right()) + brace.add(brace.point) + brace.set_height(angle) + brace.move_to(ORIGIN, DL) + brace.apply_complex_function(np.exp) + brace.scale(height) + brace.rotate(-90 * DEGREES) + brace.move_to(arc) + brace.shift(MED_SMALL_BUFF * normalize( + arc.point_from_proportion(0.5) - top + )) + x_sym = TexMobject("x") + x_sym.set_color(GREEN) + x_sym.next_to(brace.point, DR, buff=SMALL_BUFF) + + rhs = TexMobject("=", "L", "\\theta") + rhs.set_color_by_tex("\\theta", BLUE) + rhs.next_to(x_sym, RIGHT) + rhs.shift(0.7 * SMALL_BUFF * UP) + line_L = TexMobject("L") + line_L.next_to( + pendulum.rod.get_center(), UR, SMALL_BUFF, + ) + + self.play( + ShowCreation(arc), + Rotate(line, angle, about_point=top), + UpdateFromAlphaFunc( + line, lambda m, a: m.set_stroke( + width=2 * there_and_back(a) + ) + ), + GrowFromPoint( + brace, line.get_bottom(), + path_arc=angle + ), + ) + self.play(FadeInFrom(x_sym, UP)) + self.wait() + + # Show equation + line.set_stroke(BLUE, 5) + self.play( + ShowCreationThenFadeOut(line), + FadeInFromDown(line_L) + ) + self.play( + TransformFromCopy( + line_L, rhs.get_part_by_tex("L") + ), + Write(rhs.get_part_by_tex("=")) + ) + self.play( + TransformFromCopy( + pendulum.theta_label, + rhs.get_parts_by_tex("\\theta"), + ) + ) + self.add(rhs) + + x_eq = VGroup(x_sym, rhs) + + self.play( + FadeOut(brace), + x_eq.rotate, angle / 2, + x_eq.next_to, arc.point_from_proportion(0.5), + UL, {"buff": -MED_SMALL_BUFF} + ) + + self.x_eq = x_eq + self.arc = arc + self.line_L = line_L + + def add_g_vect(self): + pendulum = self.pendulum + + g_vect = self.g_vect = GravityVector( + pendulum, **self.g_vect_config, + ) + g_word = self.g_word = TextMobject("Gravity") + g_word.rotate(-90 * DEGREES) + g_word.scale(0.75) + g_word.add_updater(lambda m: m.next_to( + g_vect, RIGHT, buff=-SMALL_BUFF, + )) + + self.play( + GrowArrow(g_vect), + FadeInFrom(g_word, UP, lag_ratio=0.1), + ) + self.wait() + + def show_constraint(self): + pendulum = self.pendulum + + arcs = VGroup() + for u in [-1, 2, -1]: + d_theta = 40 * DEGREES * u + arc = Arc( + start_angle=pendulum.get_theta() - 90 * DEGREES, + angle=d_theta, + radius=pendulum.length, + arc_center=pendulum.get_fixed_point(), + stroke_width=2, + stroke_color=YELLOW, + stroke_opacity=0.5, + ) + self.play( + self.theta_tracker.increment_value, d_theta, + ShowCreation(arc) + ) + arcs.add(arc) + self.play(FadeOut(arcs)) + + def break_g_vect_into_components(self): + g_vect = self.g_vect + g_vect.component_lines = always_redraw( + g_vect.create_component_lines + ) + tan_line, perp_line = g_vect.component_lines + g_vect.tangent = always_redraw(lambda: Arrow( + tan_line.get_start(), + tan_line.get_end(), + buff=0, + color=self.tan_line_color, + )) + g_vect.perp = always_redraw(lambda: Arrow( + perp_line.get_start(), + perp_line.get_end(), + buff=0, + color=self.perp_line_color, + )) + + 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 + + arc = Arc( + start_angle=90 * DEGREES, + angle=self.pendulum.get_theta(), + radius=0.5, + arc_center=g_vect.get_end(), + ) + q_mark = TexMobject("?") + q_mark.next_to(arc.get_center(), UL, SMALL_BUFF) + theta_label = TexMobject("\\theta") + theta_label.move_to(q_mark) + + self.add(g_vect) + self.play( + ShowCreation(arc), + Write(q_mark) + ) + self.play(ShowCreationThenFadeAround(q_mark)) + self.wait() + self.play(ShowCreationThenFadeAround( + self.pendulum.theta_label + )) + self.play( + TransformFromCopy( + self.pendulum.theta_label, + theta_label, + ), + FadeOut(q_mark) + ) + self.wait() + self.play(WiggleOutThenIn(g_vect.tangent)) + self.play(WiggleOutThenIn( + Line( + *g_vect.get_start_and_end(), + buff=0, + ).add_tip().match_style(g_vect), + remover=True + )) + self.wait() + self.play( + FadeOut(arc), + FadeOut(theta_label), + ) + + def show_gsin_formula(self): + g_vect = self.g_vect + g_word = self.g_word + 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_sign(self): + 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.play( + FadeInFrom(theta_decimal, UP), + FadeOut(self.x_eq), + FadeOut(self.line_L), + ) + self.set_theta(-60 * DEGREES, run_time=4) + self.set_theta(60 * DEGREES, run_time=4) + self.play( + FadeOut(theta_decimal), + FadeIn(self.x_eq), + ) + + def show_acceleration_formula(self): + x_eq = self.x_eq + g_sin_theta = self.g_sin_label + + equation = TexMobject( + "a", "=", + "\\ddot", "x", + "=", + "-", "g", "\\sin\\big(", "\\theta", "\\big)", + ) + equation.to_edge(LEFT) + + second_deriv = equation[2:4] + x_part = equation.get_part_by_tex("x") + x_part.set_color(GREEN) + a_eq = equation[:2] + eq2 = equation.get_parts_by_tex("=")[1] + rhs = equation[5:] + + second_deriv_L_form = TexMobject( + "L", "\\ddot", "\\theta" + ) + second_deriv_L_form.move_to(second_deriv, DOWN) + eq3 = TexMobject("=") + eq3.rotate(90 * DEGREES) + eq3.next_to(second_deriv_L_form, UP) + + g_L_frac = TexMobject( + "-", "{g", "\\over", "L}" + ) + g_L_frac.move_to(rhs[:2], LEFT) + g_L_frac.shift(SMALL_BUFF * UP / 2) + + mu_term = TexMobject( + "-\\mu", "\\dot", "\\theta", + ) + mu_term.next_to(g_L_frac, LEFT) + mu_term.shift(SMALL_BUFF * UP / 2) + + mu_brace = Brace(mu_term, UP) + mu_word = mu_brace.get_text("Air resistance") + + for mob in equation, second_deriv_L_form, mu_term: + mob.set_color_by_tex("\\theta", BLUE) + + self.play( + TransformFromCopy(x_eq[0], x_part), + Write(equation[:3]), + ) + self.wait() + self.play( + Write(eq2), + TransformFromCopy(g_sin_theta, rhs) + ) + self.wait() + # + self.show_acceleration_at_different_angles() + # + self.play( + FadeInFromDown(second_deriv_L_form), + Write(eq3), + second_deriv.next_to, eq3, UP, + a_eq.shift, SMALL_BUFF * LEFT, + eq2.shift, SMALL_BUFF * RIGHT, + rhs.shift, SMALL_BUFF * RIGHT, + ) + self.wait() + self.wait() + self.play( + FadeOut(a_eq), + FadeOut(second_deriv), + FadeOut(eq3), + ReplacementTransform( + second_deriv_L_form.get_part_by_tex("L"), + g_L_frac.get_part_by_tex("L"), + ), + ReplacementTransform( + equation.get_part_by_tex("-"), + g_L_frac.get_part_by_tex("-"), + ), + ReplacementTransform( + equation.get_part_by_tex("g"), + g_L_frac.get_part_by_tex("g"), + ), + Write(g_L_frac.get_part_by_tex("\\over")), + rhs[2:].next_to, g_L_frac, RIGHT, {"buff": SMALL_BUFF}, + ) + self.wait() + self.play( + GrowFromCenter(mu_term), + VGroup(eq2, second_deriv_L_form[1:]).next_to, + mu_term, LEFT, + ) + self.play( + GrowFromCenter(mu_brace), + FadeInFromDown(mu_word), + ) + + def show_acceleration_at_different_angles(self): + to_fade = VGroup( + self.g_cos_label, + self.g_vect.perp, + ) + new_comp_line_sytle = { + "stroke_width": 0.5, + "stroke_opacity": 0.25, + } + + self.play( + FadeOut(self.x_eq), + to_fade.set_opacity, 0.25, + self.g_vect.component_lines.set_style, + new_comp_line_sytle + ) + self.g_vect.component_lines.add_updater( + lambda m: m.set_style(**new_comp_line_sytle) + ) + for mob in to_fade: + mob.add_updater(lambda m: m.set_opacity(0.25)) + + self.set_theta(0) + self.wait(2) + self.set_theta(89.9 * DEGREES, run_time=3) + self.wait(2) + self.set_theta( + 60 * DEGREES, + FadeIn(self.x_eq), + run_time=2, + ) + self.wait() + + 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)) + + pendulum_copy = pendulum.deepcopy() + pendulum_copy.clear_updaters() + pendulum_copy.fade(1) + pendulum_copy.start_swinging() + + def new_updater(p): + p.set_theta(pendulum_copy.get_theta()) + pendulum.add_updater(new_updater) + + self.add(pendulum_copy) + self.wait(5) + pendulum_copy.end_swinging() + self.remove(pendulum_copy) + pendulum.remove_updater(new_updater) + self.update_mobjects(0) + + def show_angular_velocity(self): + pass + + 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): + 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) + + 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): + 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): + 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 SimpleDampenedPendulum(Scene): + def construct(self): + pendulum = Pendulum( + top_point=ORIGIN, + initial_theta=150 * DEGREES, + mu=0.5, + ) + self.add(pendulum) + pendulum.start_swinging() + self.wait(20) + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/diffyq/part1/phase_space.py b/active_projects/diffyq/part1/phase_space.py new file mode 100644 index 00000000..549ac28b --- /dev/null +++ b/active_projects/diffyq/part1/phase_space.py @@ -0,0 +1,2079 @@ +from manimlib.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, + }, + "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), + } + } + + def construct(self): + self.initialize_plane() + self.plane.axes.set_stroke(width=0.5) + self.initialize_vector_field() + + field = self.vector_field + field.set_stroke(width=5) + for vector in field: + vector.set_stroke(width=3) + vector.tip.set_stroke(width=0) + vector.tip.scale(1.5, about_point=vector.get_last_point()) + vector.set_opacity(1) + + title = TextMobject("Differential\\\\", "equations") + title.space_out_submobjects(0.8) + # title.scale(3) + title.set_width(FRAME_WIDTH - 3) + # title.to_edge(UP) + # title[1].to_edge(DOWN) + + subtitle = TextMobject("Studying the unsolvable") + subtitle.set_width(FRAME_WIDTH - 1) + subtitle.set_color(WHITE) + subtitle.to_edge(DOWN, buff=1) + + # title.center() + title.to_edge(UP, buff=1) + title.add(subtitle) + # title.set_stroke(BLACK, 15, background=True) + # title.add_background_rectangle_to_submobjects(opacity=0.5) + title.set_stroke(BLACK, 15, background=True) + subtitle.set_stroke(RED, 2, background=True) + # for part in title: + # part[0].set_fill(opacity=0.25) + # part[0].set_stroke(width=0) + black_parts = VGroup() + for mob in title.family_members_with_points(): + for sp in mob.get_subpaths(): + new_mob = VMobject() + new_mob.set_points(sp) + new_mob.set_fill(BLACK, 0.25) + new_mob.set_stroke(width=0) + black_parts.add(new_mob) + + for vect in field: + for mob in title.family_members_with_points(): + for p in [vect.get_start(), vect.get_end()]: + x, y = p[:2] + x0, y0 = mob.get_corner(DL)[:2] + x1, y1 = mob.get_corner(UR)[:2] + if x0 < x < x1 and y0 < y < y1: + vect.set_opacity(0.25) + vect.tip.set_stroke(width=0) + + self.add(self.plane) + self.add(field) + self.add(black_parts) + self.add(title) diff --git a/active_projects/diffyq/part1/pi_scenes.py b/active_projects/diffyq/part1/pi_scenes.py new file mode 100644 index 00000000..ab0d60fa --- /dev/null +++ b/active_projects/diffyq/part1/pi_scenes.py @@ -0,0 +1,515 @@ +from manimlib.imports import * +from active_projects.ode.part1.shared_constructs import * + + +class SomeOfYouWatching(TeacherStudentsScene): + CONFIG = { + "camera_config": { + "background_color": DARKER_GREY, + } + } + + def construct(self): + screen = self.screen + screen.scale(1.25, about_edge=UL) + screen.set_fill(BLACK, 1) + self.add(screen) + + self.teacher.change("raise_right_hand") + for student in self.students: + student.change("pondering", screen) + + self.student_says( + "Well...yeah", + target_mode="tease" + ) + self.wait(3) + + +class FormulasAreLies(PiCreatureScene): + def construct(self): + you = self.pi_creature + t2c = { + "{L}": BLUE, + "{g}": YELLOW, + "\\theta_0": WHITE, + "\\sqrt{\\,": WHITE, + } + kwargs = {"tex_to_color_map": t2c} + period_eq = TexMobject( + "\\text{Period} = 2\\pi \\sqrt{\\,{L} / {g}}", + **kwargs + ) + theta_eq = TexMobject( + "\\theta(t) = \\theta_0 \\cos\\left(" + "\\sqrt{\\,{L} / {g}} \\cdot t" + "\\right)", + **kwargs + ) + equations = VGroup(theta_eq, period_eq) + equations.arrange(DOWN, buff=LARGE_BUFF) + + for eq in period_eq, theta_eq: + i = eq.index_of_part_by_tex("\\sqrt") + eq.sqrt_part = eq[i:i + 4] + + theta0 = theta_eq.get_part_by_tex("\\theta_0") + theta0_words = TextMobject("Starting angle") + theta0_words.next_to(theta0, UL) + theta0_words.shift(UP + 0.5 * RIGHT) + arrow = Arrow( + theta0_words.get_bottom(), + theta0, + color=WHITE, + tip_length=0.25, + ) + + bubble = SpeechBubble() + bubble.pin_to(you) + bubble.write("Lies!") + bubble.content.scale(2) + bubble.resize_to_content() + + self.add(period_eq) + you.change("pondering", period_eq) + self.wait() + theta_eq.remove(*theta_eq.sqrt_part) + self.play( + TransformFromCopy( + period_eq.sqrt_part, + theta_eq.sqrt_part, + ), + FadeIn(theta_eq) + ) + theta_eq.add(*theta_eq.sqrt_part) + self.play( + FadeInFrom(theta0_words, LEFT), + GrowArrow(arrow), + ) + self.wait() + self.play(you.change, "confused") + self.wait() + self.play( + you.change, "angry", + ShowCreation(bubble), + FadeInFromPoint(bubble.content, you.mouth), + equations.to_edge, LEFT, + FadeOut(arrow), + FadeOut(theta0_words), + ) + self.wait() + + def create_pi_creature(self): + return You().flip().to_corner(DR) + + +# class TourOfDifferentialEquations(Scene): +# def construct(self): +# pass + + +class SoWhatIsThetaThen(TeacherStudentsScene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + self.add(ode) + + self.student_says( + "Okay, but then\\\\" + "what \\emph{is} $\\theta(t)$?" + ) + self.wait() + self.play(self.teacher.change, "happy") + self.wait(2) + self.teacher_says( + "First, you must appreciate\\\\" + "a deep truth...", + added_anims=[self.get_student_changes( + *3 * ["confused"] + )] + ) + self.wait(4) + + +class ProveTeacherWrong(TeacherStudentsScene): + def construct(self): + tex_config = { + "tex_to_color_map": { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, + } + } + 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, + # ) + ode = get_ode() + 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 PhysicistPhaseSpace(PiCreatureScene): + def construct(self): + physy = self.pi_creature + name = TextMobject("Physicist") + name.scale(1.5) + name.to_corner(DL, buff=MED_SMALL_BUFF) + physy.next_to(name, UP, SMALL_BUFF) + VGroup(name, physy).shift_onto_screen() + + axes = Axes( + x_min=-1, + x_max=10, + y_min=-1, + y_max=7, + ) + axes.set_height(6) + axes.next_to(physy, RIGHT) + axes.to_edge(UP) + axes.set_stroke(width=1) + x_label = TextMobject("Position") + x_label.next_to(axes.x_axis.get_right(), UP) + y_label = TextMobject("Momentum") + y_label.next_to(axes.y_axis.get_top(), RIGHT) + + title = TextMobject("Phase space") + title.scale(1.5) + title.set_color(YELLOW) + title.move_to(axes) + + self.add(name, physy) + + self.play( + physy.change, "angry", + Write(axes), + FadeInFromDown(title) + ) + self.wait(2) + self.play( + GrowFromPoint(x_label, physy.get_corner(UR)), + physy.change, "raise_right_hand", + axes.x_axis.get_right() + ) + self.play( + GrowFromPoint(y_label, physy.get_corner(UR)), + physy.look_at, axes.y_axis.get_top(), + ) + self.wait(3) + + def create_pi_creature(self): + return PiCreature(color=GREY).to_corner(DL) + + +class AskAboutActuallySolving(TeacherStudentsScene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + self.add(ode) + morty = self.teacher + + self.student_says( + "Yeah yeah, but how do\\\\" + "you actually \\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] + self.add(ode) + + 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.change_student_modes("tired", "sad", "concerned_musician") + self.wait(4) + self.look_at(solution) + self.wait(5) + self.play( + FadeOutAndShift(solution, 2 * LEFT), + Restore(ode), + self.get_student_changes( + "sick", "angry", "tired", + ) + ) + self.wait(3) + + mystery = TexMobject( + "\\theta(t) = ???", + tex_to_color_map={"\\theta": BLUE}, + ) + mystery.scale(2) + mystery.to_edge(UP) + mystery.set_stroke(width=0, background=True) + mystery_boundary = AnimatedBoundary( + mystery, stroke_width=1 + ) + + self.play( + FadeInFromDown(mystery), + self.teacher.change, "pondering" + ) + self.add(mystery_boundary, mystery) + self.change_all_student_modes("sad") + self.look_at(mystery) + self.wait(5) + + # Define + self.student_says( + "Let $\\text{P}(\\mu, g, L; t)$ be a\\\\" + "function satisfying this ODE.", + student_index=0, + target_mode="speaking", + added_anims=[ + FadeOut(mystery), + FadeOut(mystery_boundary), + ode.to_corner, UR + ] + ) + self.change_student_modes( + "hooray", "sassy", "sassy", + look_at_arg=students[0].eyes.get_corner(UR), + ) + self.wait(2) + + +class ItGetsWorse(TeacherStudentsScene): + def construct(self): + self.teacher_says("It gets\\\\worse") + self.change_student_modes( + "hesitant", "pleading", "erm" + ) + self.wait(5) diff --git a/active_projects/diffyq/part1/shared_constructs.py b/active_projects/diffyq/part1/shared_constructs.py new file mode 100644 index 00000000..1dcfddf6 --- /dev/null +++ b/active_projects/diffyq/part1/shared_constructs.py @@ -0,0 +1,118 @@ +from manimlib.imports import * + + +Lg_formula_config = { + "tex_to_color_map": { + "\\theta_0": WHITE, + "{L}": BLUE, + "{g}": YELLOW, + }, +} + + +class You(PiCreature): + CONFIG = { + "color": BLUE_C, + } + + +def get_ode(): + tex_config = { + "tex_to_color_map": { + "{\\theta}": BLUE, + "{\\dot\\theta}": RED, + "{\\ddot\\theta}": YELLOW, + "{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 get_period_formula(): + return TexMobject( + "2\\pi", "\\sqrt{\\,", "L", "/", "g", "}", + tex_to_color_map={ + "L": BLUE, + "g": YELLOW, + } + ) + + +def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): + theta, omega = point[:2] + return np.array([ + 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) + + +def get_heart_var(index): + heart = SuitSymbol("hearts") + if index == 1: + heart.set_color(BLUE_C) + elif index == 2: + heart.set_color(GREEN) + heart.set_height(0.7) + index = Integer(index) + index.move_to(heart.get_corner(DR)) + heart.add(index) + return heart + + +def get_heart_var_deriv(index): + heart = get_heart_var(index) + filler_tex = "T" + deriv = TexMobject("{d", filler_tex, "\\over", "dt}") + deriv.scale(2) + filler = deriv.get_part_by_tex(filler_tex) + heart.match_height(filler) + heart.move_to(filler) + heart.scale(1.5, about_edge=UL) + deriv.remove(filler) + deriv.add(heart) + deriv.heart = heart + return deriv + + +def get_love_equation1(): + equation = VGroup( + get_heart_var_deriv(1), + TexMobject("=").scale(2), + TexMobject("a").scale(2), + get_heart_var(2) + ) + equation.arrange(RIGHT) + equation[-1].shift(SMALL_BUFF * DL) + return equation + + +def get_love_equation2(): + equation = VGroup( + get_heart_var_deriv(2), + TexMobject("=").scale(2), + TexMobject("-b").scale(2), + get_heart_var(1), + ) + equation.arrange(RIGHT) + equation[-1].shift(SMALL_BUFF * DL) + return equation diff --git a/active_projects/diffyq/part1/staging.py b/active_projects/diffyq/part1/staging.py new file mode 100644 index 00000000..823bd1fe --- /dev/null +++ b/active_projects/diffyq/part1/staging.py @@ -0,0 +1,3028 @@ +from manimlib.imports import * +from active_projects.ode.part1.shared_constructs import * +from active_projects.ode.part1.pendulum import Pendulum +from active_projects.ode.part1.pendulum import ThetaVsTAxes +from active_projects.ode.part1.phase_space import IntroduceVectorField +from old_projects.div_curl import PhaseSpaceOfPopulationModel +from old_projects.div_curl import ShowTwoPopulations + + +# Scenes + + +class VectorFieldTest(Scene): + def construct(self): + plane = NumberPlane( + # axis_config={"unit_size": 2} + ) + mu_tracker = ValueTracker(1) + field = VectorField( + lambda p: pendulum_vector_field_func( + plane.point_to_coords(p), + mu=mu_tracker.get_value() + ), + delta_x=0.5, + delta_y=0.5, + max_magnitude=6, + opacity=0.5, + # length_func=lambda norm: norm, + ) + field.set_opacity(1) + + self.add(plane, field) + return + + stream_lines = StreamLines( + field.func, + delta_x=0.5, + delta_y=0.5, + ) + animated_stream_lines = AnimatedStreamLines( + stream_lines, + line_anim_class=ShowPassingFlashWithThinningStrokeWidth, + ) + + self.add(plane, field, animated_stream_lines) + self.wait(10) + + +class ShowRect(Scene): + CONFIG = { + "height": 1, + "width": 3, + } + + def construct(self): + rect = Rectangle( + height=self.height, + width=self.width, + ) + rect.set_stroke(YELLOW) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + + +class ShowSquare(ShowRect): + CONFIG = { + "height": 3, + "width": 3, + } + + +class WhenChangeIsEasier(Scene): + def construct(self): + pass + + +class AirResistanceBrace(Scene): + def construct(self): + brace = Brace(Line(ORIGIN, RIGHT), DOWN) + word = TextMobject("Air resistance") + word.next_to(brace, DOWN) + self.play(GrowFromCenter(brace), FadeInFrom(word, UP)) + self.wait() + + +class PeriodFormula(Scene): + def construct(self): + formula = get_period_formula() + formula.scale(2) + q_mark = TexMobject("?") + q_mark.scale(3) + q_mark.next_to(formula, RIGHT) + self.add(formula, q_mark) + + +class TourOfDifferentialEquations(MovingCameraScene): + CONFIG = { + "screen_rect_style": { + "stroke_width": 2, + "stroke_color": WHITE, + "fill_opacity": 1, + "fill_color": BLACK, + }, + "camera_config": {"background_color": DARKER_GREY}, + "zoomed_thumbnail_index": 0, + } + + def construct(self): + self.add_title() + self.show_thumbnails() + self.zoom_in_to_one_thumbnail() + # 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): + 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) + + line = self.line = CubicBezier([ + [-5, 3, 0], + [3, 3, 0], + [-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_phase_space_preview(thumbnails[0]) + 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( + line, + rate_func=lambda t: np.clip(t * (n + 1) / n, 0, 1) + ), + LaggedStart(*[ + GrowFromCenter( + thumbnail, + rate_func=squish_rate_func( + smooth, + 0, 0.7, + ) + ) + for thumbnail in thumbnails + ], lag_ratio=1), + run_time=5 + ) + self.play(Write(dots)) + self.wait() + + self.thumbnails = thumbnails + + def zoom_in_to_one_thumbnail(self): + self.play( + self.camera_frame.replace, + self.thumbnails[self.zoomed_thumbnail_index], + run_time=3, + ) + self.wait() + + def show_words(self): + words = VGroup( + TextMobject("Generalize"), + TextMobject("Put in context"), + TextMobject("Modify"), + ) + # words.arrange(DOWN, aligned_edge=LEFT, buff=LARGE_BUFF) + words.scale(1.5) + words.to_corner(UR) + words.add_to_back(VectorizedPoint(words.get_center())) + words.add(VectorizedPoint(words.get_center())) + + diffEq = TextMobject("Differential\\\\equations") + diffEq.scale(1.5) + diffEq.to_corner(DL, buff=LARGE_BUFF) + + for word1, word2 in zip(words, words[1:]): + self.play( + FadeInFromDown(word2), + FadeOutAndShift(word1, UP), + ) + self.wait() + self.play( + ReplacementTransform( + VGroup(self.thumbnails).copy().fade(1), + diffEq, + lag_ratio=0.01, + ) + ) + self.wait() + + # + def add_phase_space_preview(self, thumbnail): + image = ImageMobject("LovePhaseSpace") + image.replace(thumbnail) + thumbnail.add(image) + + def add_heat_preview(self, thumbnail): + image = ImageMobject("HeatSurfaceExample") + image.replace(thumbnail) + thumbnail.add(image) + + 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, + ) + 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) + + 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 + ) + + def add_laplace_symbol(self, thumbnail): + mob = TexMobject( + "\\mathcal{L}\\left\\{f(t)\\right\\}" + ) + mob.set_width(0.8 * thumbnail.get_width()) + mob.move_to(thumbnail) + thumbnail.add(mob) + + +class HeatEquationPreview(ExternallyAnimatedScene): + pass + + +class ShowHorizontalDashedLine(Scene): + def construct(self): + line = DashedLine(LEFT_SIDE, RIGHT_SIDE) + self.play(ShowCreation(line)) + self.wait() + + +class RabbitFoxPopulations(ShowTwoPopulations): + pass + + +class RabbitFoxEquation(PhaseSpaceOfPopulationModel): + def construct(self): + equations = self.get_equations() + self.add(equations) + + +class ShowGravityAcceleration(Scene): + CONFIG = { + "flash": True, + "add_ball_copies": True, + } + + def construct(self): + self.add_gravity_field() + self.add_title() + self.pulse_gravity_down() + self.show_g_value() + self.show_trajectory() + self.combine_v_vects() + self.show_g_symbol() + + def add_gravity_field(self): + gravity_field = self.gravity_field = VectorField( + lambda p: DOWN, + # delta_x=2, + # delta_y=2, + ) + gravity_field.set_opacity(0.5) + gravity_field.sort_submobjects( + lambda p: -p[1], + ) + self.add(gravity_field) + + def add_title(self): + title = self.title = TextMobject("Gravitational acceleration") + title.scale(1.5) + title.to_edge(UP) + title.add_background_rectangle( + buff=0.05, + opacity=1, + ) + self.play(FadeInFromDown(title)) + + def pulse_gravity_down(self): + field = self.gravity_field + self.play(LaggedStart(*[ + ApplyFunction( + lambda v: v.set_opacity(1).scale(1.2), + vector, + rate_func=there_and_back, + ) + for vector in field + ]), run_time=2, lag_ratio=0.001) + self.add(self.title) + + def show_g_value(self): + title = self.title + g_eq = self.g_eq = TexMobject( + "-9.8", "{\\text{m/s}", "\\over", "\\text{s}}", + **Lg_formula_config + ) + g_eq.add_background_rectangle_to_submobjects() + g_eq.scale(2) + g_eq.center() + num, ms, per, s = g_eq + + self.add(num) + self.wait(0.75) + self.play( + FadeInFrom(ms, 0.25 * DOWN, run_time=0.5) + ) + self.wait(0.25) + self.play(LaggedStart( + GrowFromPoint(per, per.get_left()), + FadeInFrom(s, 0.5 * UP), + lag_ratio=0.7, + run_time=0.75 + )) + self.wait() + self.play( + g_eq.scale, 0.5, + g_eq.next_to, title, DOWN, + ) + + def show_trajectory(self): + total_time = 6 + + ball = self.get_ball() + + p0 = 3 * DOWN + 5 * LEFT + v0 = 2.8 * UP + 1.5 * RIGHT + g = 0.9 * DOWN + graph = ParametricFunction( + lambda t: p0 + v0 * t + 0.5 * g * t**2, + t_min=0, + t_max=total_time, + ) + # graph.center().to_edge(DOWN) + dashed_graph = DashedVMobject(graph, num_dashes=60) + dashed_graph.set_stroke(WHITE, 1) + + ball.move_to(graph.get_start()) + randy.add_updater( + lambda m, dt: m.rotate(dt).move_to(ball) + ) + times = np.arange(0, total_time + 1) + + velocity_graph = ParametricFunction( + lambda t: v0 + g * t, + t_min=0, t_max=total_time, + ) + v_point = VectorizedPoint() + v_point.move_to(velocity_graph.get_start()) + + def get_v_vect(): + result = Vector( + v_point.get_location(), + color=RED, + tip_length=0.2, + ) + result.scale(0.5, about_point=result.get_start()) + result.shift(ball.get_center()) + result.set_stroke(width=2, family=False) + return result + v_vect = always_redraw(get_v_vect) + self.add(v_vect) + + flash_rect = FullScreenRectangle( + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.2, + ) + flash = FadeOut( + flash_rect, + rate_func=squish_rate_func(smooth, 0, 0.1) + ) + + time_label = TextMobject("Time = ") + time_label.shift(MED_SMALL_BUFF * LEFT) + time_tracker = ValueTracker(0) + time = DecimalNumber(0) + time.next_to(time_label, RIGHT) + time.add_updater(lambda d, dt: d.set_value( + time_tracker.get_value() + )) + time_group = VGroup(time_label, time) + time_group.center().to_edge(DOWN) + self.add(time_group) + + ball_copies = VGroup() + v_vect_copies = VGroup() + self.add(dashed_graph, ball) + for t1, t2 in zip(times, times[1:]): + v_vect_copy = v_vect.copy() + v_vect_copies.add(v_vect_copy) + ball_copy = ball.copy() + ball_copy.clear_updaters() + ball_copies.add(ball_copy) + + if self.add_ball_copies: + self.add(v_vect_copy) + self.add(ball_copy, ball) + + dashed_graph.save_state() + kw = { + "rate_func": lambda alpha: interpolate( + t1 / total_time, + t2 / total_time, + alpha + ) + } + anims = [ + ShowCreation(dashed_graph, **kw), + MoveAlongPath(ball, graph, **kw), + MoveAlongPath(v_point, velocity_graph, **kw), + ApplyMethod( + time_tracker.increment_value, 1, + rate_func=linear + ), + ] + if self.flash: + anims.append(flash) + self.play(*anims, run_time=1) + dashed_graph.restore() + randy.clear_updaters() + self.play(FadeOut(time_group)) + self.wait() + + self.v_vects = v_vect_copies + + def combine_v_vects(self): + v_vects = self.v_vects.copy() + v_vects.generate_target() + new_center = 2 * DOWN + 2 * LEFT + for vect in v_vects.target: + vect.scale(1.5) + vect.set_stroke(width=2) + vect.shift(new_center - vect.get_start()) + + self.play(MoveToTarget(v_vects)) + + delta_vects = VGroup(*[ + Arrow( + v1.get_end(), + v2.get_end(), + buff=0.01, + color=YELLOW, + ).set_opacity(0.5) + for v1, v2 in zip(v_vects, v_vects[1:]) + ]) + brace = Brace(Line(ORIGIN, UP), RIGHT) + braces = VGroup(*[ + brace.copy().match_height(arrow).next_to( + arrow, RIGHT, buff=0.2 * SMALL_BUFF + ) + for arrow in delta_vects + ]) + amounts = VGroup(*[ + TextMobject("9.8 m/s").scale(0.5).next_to( + brace, RIGHT, SMALL_BUFF + ) + for brace in braces + ]) + + self.play( + FadeOut(self.gravity_field), + FadeIn(delta_vects, lag_ratio=0.1), + ) + self.play( + LaggedStartMap(GrowFromCenter, braces), + LaggedStartMap(FadeInFrom, amounts, lambda m: (m, LEFT)), + ) + self.wait() + + def show_g_symbol(self): + g = TexMobject("g") + brace = Brace(self.g_eq[0][2:], UP, buff=SMALL_BUFF) + g.scale(1.5) + g.next_to(brace, UP) + g.set_color(YELLOW) + self.play( + FadeOut(self.title), + GrowFromCenter(brace), + FadeInFrom(g, UP), + ) + self.wait() + + # + def get_ball(self): + ball = Circle( + stroke_width=1, + stroke_color=WHITE, + fill_color=GREY, + fill_opacity=1, + sheen_factor=1, + sheen_direction=UL, + radius=0.25, + ) + randy = Randolph(mode="pondering") + randy.eyes.set_stroke(BLACK, 0.5) + randy.match_height(ball) + randy.scale(0.75) + randy.move_to(ball) + ball.add(randy) + return ball + + +class ShowSimpleTrajectory(ShowGravityAcceleration): + CONFIG = { + "flash": False, + } + + def construct(self): + self.show_trajectory() + + +class SimpleProjectileEquation(ShowGravityAcceleration): + CONFIG = { + "y0": 0, + "g": 9.8, + "axes_config": { + "x_min": 0, + "x_max": 6, + "x_axis_config": { + "unit_size": 1.5, + "tip_width": 0.15, + }, + "y_min": -30, + "y_max": 35, + "y_axis_config": { + "unit_size": 0.1, + "numbers_with_elongated_ticks": range( + -30, 35, 10 + ), + "tick_size": 0.05, + "numbers_to_show": range(-30, 31, 10), + "tip_width": 0.15, + }, + "center_point": 2 * LEFT, + } + } + + def construct(self): + self.add_axes() + self.setup_trajectory() + + self.show_trajectory() + self.show_equation() + self.solve_for_velocity() + self.solve_for_position() + + def add_axes(self): + axes = self.axes = Axes(**self.axes_config) + axes.set_stroke(width=2) + axes.add_coordinates() + + t_label = TexMobject("t") + t_label.next_to(axes.x_axis.get_right(), UL) + axes.add(t_label) + + self.add(axes) + + def setup_trajectory(self): + axes = self.axes + total_time = self.total_time = 5 + + ball = self.get_ball() + offset_vector = 3 * LEFT + + g = self.g + y0 = self.y0 + v0 = 0.5 * g * total_time + + t_tracker = ValueTracker(0) + get_t = t_tracker.get_value + + # Position + def y_func(t): + return -0.5 * g * t**2 + v0 * t + y0 + + graph_template = axes.get_graph(y_func, x_max=total_time) + graph_template.set_stroke(width=2) + traj_template = graph_template.copy() + traj_template.stretch(0, 0) + traj_template.move_to( + axes.coords_to_point(0, 0), DOWN + ) + traj_template.shift(offset_vector) + traj_template.set_stroke(width=0.5) + + graph = VMobject() + graph.set_stroke(BLUE, 2) + traj = VMobject() + traj.set_stroke(WHITE, 0.5) + graph.add_updater(lambda g: g.pointwise_become_partial( + graph_template, 0, get_t() / total_time + )) + traj.add_updater(lambda t: t.pointwise_become_partial( + traj_template, 0, get_t() / total_time + )) + + def get_ball_point(): + return axes.coords_to_point( + 0, y_func(get_t()) + ) + offset_vector + + f_always(ball.move_to, get_ball_point) + + h_line = always_redraw(lambda: DashedLine( + get_ball_point(), + axes.input_to_graph_point(get_t(), graph_template), + stroke_width=1, + )) + + y_label = TexMobject("y", "(t)") + y_label.set_color_by_tex("y", BLUE) + y_label.add_updater( + lambda m: m.next_to( + graph.get_last_point(), + UR, SMALL_BUFF, + ) + ) + + # Velocity + def v_func(t): + return -g * t + v0 + + def get_v_vect(): + return Vector( + axes.y_axis.unit_size * v_func(get_t()) * UP, + color=RED, + ) + v_vect = always_redraw( + lambda: get_v_vect().shift(get_ball_point()) + ) + v_brace = always_redraw(lambda: Brace(v_vect, LEFT)) + dy_dt_label = TexMobject( + "{d", "y", "\\over dt}", "(t)", + ) + dy_dt_label.scale(0.8) + dy_dt_label.set_color_by_tex("y", BLUE) + y_dot_label = TexMobject("\\dot y", "(t)") + y_dot_label.set_color_by_tex("\\dot y", RED) + for label in dy_dt_label, y_dot_label: + label.add_updater(lambda m: m.next_to( + v_brace, LEFT, SMALL_BUFF, + )) + + graphed_v_vect = always_redraw( + lambda: get_v_vect().shift( + axes.coords_to_point(get_t(), 0) + ) + ) + v_graph_template = axes.get_graph( + v_func, x_max=total_time, + ) + v_graph = VMobject() + v_graph.set_stroke(RED, 2) + v_graph.add_updater(lambda m: m.pointwise_become_partial( + v_graph_template, + 0, get_t() / total_time, + )) + + # Acceleration + def get_a_vect(): + return Vector( + axes.y_axis.unit_size * g * DOWN + ) + + a_vect = get_a_vect() + a_vect.add_updater(lambda a: a.move_to( + get_ball_point(), UP, + )) + a_brace = Brace(a_vect, RIGHT) + always(a_brace.next_to, a_vect, RIGHT, SMALL_BUFF) + d2y_dt2_label = TexMobject( + "d^2", "{y}", "\\over dt}", "(t)" + ) + d2y_dt2_label.scale(0.8) + d2y_dt2_label.set_color_by_tex( + "y", BLUE, + ) + y_ddot_label = TexMobject("\\ddot y", "(t)") + y_ddot_label.set_color_by_tex("\\ddot y", YELLOW) + for label in d2y_dt2_label, y_ddot_label: + label.add_updater(lambda m: m.next_to( + a_brace, RIGHT, SMALL_BUFF + )) + a_graph = axes.get_graph( + lambda t: -g, x_max=total_time, + ) + a_graph.set_stroke(YELLOW, 2) + + graphed_a_vect = get_a_vect() + graphed_a_vect.add_updater(lambda a: a.move_to( + axes.coords_to_point(get_t(), 0), UP, + )) + + self.set_variables_as_attrs( + t_tracker, + graph, + y_label, + traj, + h_line, + v_vect, + v_brace, + dy_dt_label, + y_dot_label, + ball, + graphed_v_vect, + v_graph, + a_vect, + a_brace, + d2y_dt2_label, + y_ddot_label, + a_graph, + graphed_a_vect, + ) + + def show_trajectory(self): + self.add( + self.h_line, + self.traj, + self.ball, + self.graph, + self.y_label, + ) + self.play_trajectory() + self.wait() + + self.add( + self.v_vect, + self.v_brace, + self.dy_dt_label, + self.ball, + self.graphed_v_vect, + self.v_graph, + ) + self.play_trajectory() + self.wait() + + self.add( + self.a_vect, + self.ball, + self.a_brace, + self.d2y_dt2_label, + self.a_graph, + self.graphed_a_vect, + ) + self.play_trajectory() + self.wait() + + self.play( + ReplacementTransform( + self.dy_dt_label, + self.y_dot_label, + ), + ShowCreationThenFadeAround( + self.y_dot_label, + ), + ) + self.play( + ReplacementTransform( + self.d2y_dt2_label, + self.y_ddot_label, + ), + ShowCreationThenFadeAround( + self.y_ddot_label, + ), + ) + + def show_equation(self): + y_ddot = self.y_ddot_label + new_y_ddot = y_ddot.deepcopy() + new_y_ddot.clear_updaters() + + equation = VGroup( + new_y_ddot, + *TexMobject( + "=", "-g", + tex_to_color_map={"-g": YELLOW}, + ), + ) + new_y_ddot.next_to(equation[1], LEFT, SMALL_BUFF) + equation.move_to(self.axes) + equation.to_edge(UP) + + self.play( + TransformFromCopy(y_ddot, new_y_ddot), + Write(equation[1:]), + FadeOut(self.graph), + FadeOut(self.y_label), + FadeOut(self.h_line), + FadeOut(self.v_graph), + FadeOut(self.graphed_v_vect), + FadeOut(self.graphed_a_vect), + ) + + self.equation = equation + + def solve_for_velocity(self): + axes = self.axes + equation = self.equation + v_graph = self.v_graph.deepcopy() + v_graph.clear_updaters() + v_start_point = v_graph.get_start() + origin = axes.coords_to_point(0, 0) + offset = v_start_point - origin + v_graph.shift(-offset) + + tex_question, answer1, answer2 = derivs = [ + TexMobject( + "{d", "(", *term, ")", "\\over", "dt}", "(t)", + "=", "-g", + tex_to_color_map={ + "-g": YELLOW, + "v_0": RED, + "?": RED, + } + ) + for term in [ + ("?", "?", "?", "?"), + ("-g", "t"), + ("-g", "t", "+", "v_0",), + ] + ] + for x in range(2): + answer1.submobjects.insert( + 4, VectorizedPoint(answer1[4].get_left()) + ) + for deriv in derivs: + deriv.next_to(equation, DOWN, MED_LARGE_BUFF) + + question = TextMobject( + "What function has slope $-g$?", + tex_to_color_map={"$-g$": YELLOW}, + ) + question.next_to(tex_question, DOWN) + question.set_stroke(BLACK, 5, background=True) + question.add_background_rectangle() + + v0_dot = Dot(v_start_point, color=PINK) + v0_label = TexMobject("v_0") + v0_label.set_color(RED) + v0_label.next_to(v0_dot, UR, buff=0) + + y_dot_equation = TexMobject( + "{\\dot y}", "(t)", "=", + "-g", "t", "+", "v_0", + tex_to_color_map={ + "{\\dot y}": RED, + "-g": YELLOW, + "v_0": RED, + } + ) + y_dot_equation.to_corner(UR) + + self.play( + FadeInFrom(tex_question, DOWN), + FadeInFrom(question, UP) + ) + self.wait() + self.add(v_graph, question) + self.play( + ReplacementTransform(tex_question, answer1), + ShowCreation(v_graph), + ) + self.wait() + self.play( + ReplacementTransform(answer1, answer2), + v_graph.shift, offset, + ) + self.play( + FadeInFromLarge(v0_dot), + FadeInFromDown(v0_label), + ) + self.wait() + self.play( + TransformFromCopy( + answer2[2:6], y_dot_equation[3:], + ), + Write(y_dot_equation[:3]), + equation.shift, LEFT, + ) + self.play( + FadeOut(question), + FadeOut(answer2), + ) + + self.remove(v_graph) + self.add(self.v_graph) + self.y_dot_equation = y_dot_equation + + def solve_for_position(self): + # Largely copied from above...not great + equation = self.equation + y_dot_equation = self.y_dot_equation + graph = self.graph + + all_terms = [ + ("?", "?", "?", "?"), + ("-", "(1/2)", "g", "t^2", "+", "v_0", "t"), + ("-", "(1/2)", "g", "t^2", "+", "v_0", "t", "+", "y_0"), + ] + tex_question, answer1, answer2 = derivs = [ + TexMobject( + "{d", "(", *term, ")", "\\over", "dt}", "(t)", + "=", + "-g", "t", "+", "v_0", + tex_to_color_map={ + "g": YELLOW, + "v_0": RED, + "?": BLUE, + "y_0": BLUE, + } + ) + for term in all_terms + ] + answer1.scale(0.8) + answer2.scale(0.8) + for deriv, terms in zip(derivs, all_terms): + for x in range(len(all_terms[-1]) - len(terms)): + n = 2 + len(terms) + deriv.submobjects.insert( + n, VectorizedPoint(deriv[n].get_left()) + ) + deriv.next_to( + VGroup(equation, y_dot_equation), + DOWN, MED_LARGE_BUFF + SMALL_BUFF + ) + deriv.shift_onto_screen() + deriv.add_background_rectangle_to_submobjects() + + y_equation = TexMobject( + "y", "(t)", "=", + "-", "(1/2)", "g", "t^2", + "+", "v_0", "t", + "+", "y_0", + tex_to_color_map={ + "y": BLUE, + "g": YELLOW, + "v_0": RED, + } + ) + y_equation.next_to( + VGroup(equation, y_dot_equation), + DOWN, MED_LARGE_BUFF, + ) + + self.play( + FadeInFrom(tex_question, DOWN), + ) + self.wait() + self.add(graph, tex_question) + self.play( + ReplacementTransform(tex_question, answer1), + ShowCreation(graph), + ) + self.add(graph, answer1) + self.wait() + self.play(ReplacementTransform(answer1, answer2)) + self.add(graph, answer2) + g_updaters = graph.updaters + graph.clear_updaters() + self.play( + graph.shift, 2 * DOWN, + rate_func=there_and_back, + run_time=2, + ) + graph.add_updater(g_updaters[0]) + self.wait() + br = BackgroundRectangle(y_equation) + self.play( + FadeIn(br), + ReplacementTransform( + answer2[2:11], + y_equation[3:] + ), + FadeIn(y_equation[:3]), + FadeOut(answer2[:2]), + FadeOut(answer2[11:]), + ) + self.play(ShowCreationThenFadeAround(y_equation)) + self.play_trajectory() + + # + def play_trajectory(self, *added_anims, **kwargs): + self.t_tracker.set_value(0) + self.play( + ApplyMethod( + self.t_tracker.set_value, 5, + rate_func=linear, + run_time=self.total_time, + ), + *added_anims, + ) + self.wait() + + +class SimpleProjectileEquationVGraphFreedom(SimpleProjectileEquation): + def construct(self): + self.add_axes() + self.setup_trajectory() + self.clear() + v_graph = self.v_graph + self.t_tracker.set_value(5) + v_graph.update() + v_graph.clear_updaters() + self.add(v_graph) + self.play(v_graph.shift, 5 * DOWN, run_time=2) + self.play(v_graph.shift, 5 * UP, run_time=2) + + +class UniversalGravityLawSymbols(Scene): + def construct(self): + x1_tex = "\\vec{\\textbf{x}}_1" + x2_tex = "\\vec{\\textbf{x}}_2" + a1_tex = "\\vec{\\textbf{a}}_1" + new_brown = interpolate_color(LIGHT_GREY, LIGHT_BROWN, 0.5) + law = TexMobject( + "F_1", "=", "m_1", a1_tex, "=", + "G", "m_1", "m_2", + "\\left({", x2_tex, "-", x1_tex, "\\over", + "||", x2_tex, "-", x1_tex, "||", "}\\right)", + "\\left({", "1", "\\over", + "||", x2_tex, "-", x1_tex, "||^2", "}\\right)", + tex_to_color_map={ + x1_tex: BLUE_C, + "m_1": BLUE_C, + x2_tex: new_brown, + "m_2": new_brown, + a1_tex: YELLOW, + } + ) + law.to_edge(UP) + + force = law[:4] + constants = law[4:8] + unit_vect = law[8:19] + inverse_square = law[19:] + parts = VGroup( + force, unit_vect, inverse_square + ) + + words = VGroup( + TextMobject("Force on\\\\mass 1"), + TextMobject("Unit vector\\\\towards mass 2"), + TextMobject("Inverse square\\\\law"), + ) + + self.add(law) + + braces = VGroup() + rects = VGroup() + for part, word in zip(parts, words): + brace = Brace(part, DOWN) + word.scale(0.8) + word.next_to(brace, DOWN) + rect = SurroundingRectangle(part) + rect.set_stroke(YELLOW, 1) + braces.add(brace) + rects.add(rect) + + self.play( + ShowCreationThenFadeOut(rects[0]), + GrowFromCenter(braces[0]), + FadeInFrom(words[0], UP) + ) + self.wait() + self.play( + ShowCreationThenFadeOut(rects[1]), + GrowFromCenter(braces[1]), + FadeInFrom(words[1], UP) + ) + self.wait() + self.play( + ShowCreationThenFadeOut(rects[2]), + TransformFromCopy(*braces[1:3]), + FadeInFrom(words[2], UP), + ) + self.wait() + + # Position derivative + v1_tex = "\\vec{\\textbf{v}}_1" + kw = { + "tex_to_color_map": { + x1_tex: BLUE_C, + v1_tex: RED, + } + } + x_deriv = TexMobject( + "{d", x1_tex, "\\over", "dt}", "=", v1_tex, **kw + ) + x_deriv.to_corner(UL) + v_deriv = TexMobject( + "{d", v1_tex, "\\over", "dt}", "=", **kw + ) + + # Make way + law.generate_target() + lt = law.target + lt.to_edge(RIGHT) + lt[6].fade(1) + lt[:6].align_to(lt[6], RIGHT) + lt[:3].fade(1) + v_deriv.next_to(lt[3], LEFT) + + self.play( + FadeInFromDown(x_deriv), + MoveToTarget(law), + braces[1:].align_to, lt, RIGHT, + MaintainPositionRelativeTo(words[1:], braces[1:]), + FadeOut(words[0]), + FadeOut(braces[0]), + ) + self.play(ShowCreationThenFadeAround(x_deriv)) + + self.play( + TransformFromCopy( + x_deriv.get_part_by_tex(v1_tex), + v_deriv.get_part_by_tex(v1_tex), + ), + Write(VGroup(*filter( + lambda m: m is not v_deriv.get_part_by_tex(v1_tex), + v_deriv, + ))) + ) + + x_parts = law.get_parts_by_tex(x1_tex) + self.play( + TransformFromCopy( + x_deriv.get_parts_by_tex(x1_tex), + x_parts.copy(), + remover=True, + path_arc=30 * DEGREES, + ) + ) + self.play( + LaggedStartMap( + ShowCreationThenFadeAround, + x_parts + ) + ) + self.wait() + + +class ExampleTypicalODE(TeacherStudentsScene): + def construct(self): + examples = VGroup( + TexMobject( + "{\\dot x}(t) = k{x}(t)", + tex_to_color_map={ + "{\\dot x}": BLUE, + "{x}": BLUE, + }, + ), + get_ode(), + TexMobject( + "{\\partial T", "\\over", "\\partial t} = ", + "{\\partial^2 T", "\\over", "\\partial x^2}", "+", + "{\\partial^2 T", "\\over", "\\partial y^2}", "+", + "{\\partial^2 T", "\\over", "\\partial z^2}", + tex_to_color_map={ + "T": RED, + } + ), + ) + examples[1].get_parts_by_tex("theta").set_color(GREEN) + examples.arrange(DOWN, buff=MED_LARGE_BUFF) + examples.to_edge(UP) + + self.play( + FadeInFrom(examples[0], UP), + self.teacher.change, "raise_right_hand", + ) + self.play( + FadeInFrom(examples[1], UP), + self.get_student_changes( + *3 * ["pondering"], + look_at_arg=examples, + ), + ) + self.play( + FadeInFrom(examples[2], UP) + ) + self.wait(5) + + +class ShowDerivativeVideo(Scene): + def construct(self): + title = TextMobject("Essence of", "Calculus") + title.scale(1.5) + title.to_edge(UP) + + title2 = TextMobject("Essence of", "Linear Algebra") + title2.scale(1.5) + title2.move_to(title, DOWN) + + rect = ScreenRectangle(height=6) + rect = rect.copy() + rect.set_style( + 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(5) + self.play(ReplacementTransform(title, title2)) + 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, buff=MED_SMALL_BUFF) + + 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}": RED, + "{\\ddot\\theta}": YELLOW, + }, + "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=RED, + 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(YELLOW, 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(RED), + TextMobject("= ``Curvature''").set_color(YELLOW), + ) + 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=YELLOW), + ) + self.wait() + + self.second_order_word = so + + def show_higher_order_examples(self): + main_example = self.get_main_example() + 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 get_main_example(self): + return VGroup( + self.second_order_word, + self.ode_initials, + self.equation + ) + + def show_changing_curvature_group(self): + t_tracker = self.t_tracker + curvature_group = self.curvature_group + labels = self.curvature_group_labels + graph = VMobject() + graph.pointwise_become_partial( + self.fake_graph, + t_tracker.get_value() / self.axes.x_max, + 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() + + +# Largely a copy of DefineODE, which is not great. +# But what can you do? +class SecondOrderEquationExample(DefineODE): + def construct(self): + self.add_graph() + self.write_differential_equation() + self.show_value_slope_curvature() + self.show_higher_order_examples() + self.show_changing_curvature_group() + + def add_graph(self): + axes = self.axes = Axes( + x_min=0, + x_max=10.5, + y_min=-2.5, + y_max=2.5, + ) + axes.center() + axes.to_edge(DOWN) + x_t = TexMobject("x", "(t)") + x_t.set_color_by_tex("x", BLUE) + t = TexMobject("t") + t.next_to(axes.x_axis.get_right(), UP) + x_t.next_to(axes.y_axis.get_top(), UP) + + axes.add(t, x_t) + axes.add_coordinates() + + self.add(axes) + + def write_differential_equation(self): + de_word = TextMobject("Differential", "Equation") + de_word.scale(1.25) + de_word.to_edge(UP, buff=MED_SMALL_BUFF) + so_word = TextMobject("Second Order") + so_word.scale(1.25) + de_word.generate_target() + group = VGroup(so_word, de_word.target) + group.arrange(RIGHT) + group.to_edge(UP, buff=MED_SMALL_BUFF) + so_word.align_to(de_word.target[0], DOWN) + so_line = Line(LEFT, RIGHT, color=YELLOW) + so_line.match_width(so_word) + so_line.next_to(so_word, DOWN, buff=SMALL_BUFF) + + equation = TexMobject( + "{\\ddot x}(t)", "=", + "-\\mu", "{\\dot x}(t)", + "-", "\\omega", "{x}(t)", + tex_to_color_map={ + "{x}": BLUE, + "{\\dot x}": RED, + "{\\ddot x}": YELLOW, + } + ) + equation.next_to(de_word, DOWN) + + dd_x_part = equation[:2] + dd_x_rect = SurroundingRectangle(dd_x_part) + + self.add(de_word, equation) + self.play( + MoveToTarget(de_word), + FadeInFrom(so_word, RIGHT), + GrowFromCenter(so_line), + ) + self.play(ReplacementTransform(so_line, dd_x_rect)) + self.play(FadeOut(dd_x_rect)) + + self.equation = equation + self.title = VGroup(*so_word, *de_word) + + def show_value_slope_curvature(self): + axes = self.axes + graph = axes.get_graph( + lambda t: -2.5 * np.cos(2 * t) * np.exp(-0.2 * t) + ) + + tex_config = { + "tex_to_color_map": { + "{x}": BLUE, + "{\\dot x}": RED, + "{\\ddot x}": YELLOW, + }, + "height": 0.5, + } + x, d_x, dd_x = [ + TexMobject( + "{" + s + "x}(t)", + **tex_config + ) + for s in ("", "\\dot ", "\\ddot ") + ] + + t_tracker = ValueTracker(1.25) + get_t = t_tracker.get_value + + def get_point(t): + return graph.point_from_proportion(t / axes.x_max) + + def get_dot(): + return Dot(get_point(get_t())).scale(0.5) + + def get_v_line(): + point = get_point(get_t()) + x_point = axes.x_axis.number_to_point( + axes.x_axis.point_to_number(point) + ) + return DashedLine( + x_point, point, + dash_length=0.025, + stroke_color=BLUE, + stroke_width=2, + ) + + def get_tangent_line(curve, alpha): + line = Line( + ORIGIN, 1.5 * RIGHT, + color=RED, + 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(YELLOW, 1) + return curve + + v_line = always_redraw(get_v_line) + dot = always_redraw(get_dot) + slope_line = always_redraw(get_slope_line) + curve = always_redraw(get_curve) + + x.next_to(v_line, RIGHT, SMALL_BUFF) + d_x.next_to(slope_line.get_end(), UP, SMALL_BUFF) + dd_x.next_to(curve.get_end(), RIGHT, SMALL_BUFF) + xs = VGroup(x, d_x, dd_x) + + words = VGroup( + TextMobject("= Height").set_color(BLUE), + TextMobject("= Slope").set_color(RED), + TextMobject("= ``Curvature''").set_color(YELLOW), + ) + words.scale(0.75) + for word, sym in zip(words, xs): + word.next_to(sym, RIGHT, buff=2 * SMALL_BUFF) + sym.word = word + + self.play( + ShowCreation(v_line), + FadeInFromPoint(dot, v_line.get_start()), + FadeInFrom(x, DOWN), + FadeInFrom(x.word, DOWN), + ) + self.add(slope_line, dot) + self.play( + ShowCreation(slope_line), + FadeInFrom(d_x, LEFT), + FadeInFrom(d_x.word, LEFT), + ) + + a_tracker = ValueTracker(0) + curve_copy = curve.copy() + changing_slope = always_redraw( + lambda: get_tangent_line( + curve_copy, + a_tracker.get_value(), + ).set_stroke( + opacity=there_and_back(a_tracker.get_value()) + ) + ) + self.add(curve, dot) + self.play( + ShowCreation(curve), + FadeInFrom(dd_x, LEFT), + FadeInFrom(dd_x.word, LEFT), + ) + self.add(changing_slope) + self.play( + a_tracker.set_value, 1, + run_time=3, + ) + self.remove(changing_slope, a_tracker) + + self.t_tracker = t_tracker + self.curvature_group = VGroup( + v_line, slope_line, curve, dot + ) + self.curvature_group_labels = VGroup(xs, words) + self.fake_graph = graph + + def get_main_example(self): + return VGroup( + self.equation, + self.title, + ) + +# class VisualizeHeightSlopeCurvature(DefineODE): +# CONFIG = { +# "pendulum_config": { +# "length": 2, +# "top_point": 5 * RIGHT + 2 * UP, +# "initial_theta": 175 * DEGREES, +# "mu": 0.3, +# }, +# } + +# def construct(self): +# self.add_graph() +# self.show_value_slope_curvature() +# self.show_changing_curvature_group() + + +class ODEvsPDEinFrames(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + frames.shift(0.5 * DOWN) + + animated_frames = VGroup(*[ + AnimatedBoundary( + frame, + cycle_rate=0.2, + max_stroke_width=1, + ) + for frame in frames + ]) + + titles = VGroup( + # TextMobject("ODEs"), + # TextMobject("PDEs"), + TextMobject("Ordinary", "Differential", "Equations"), + TextMobject("Partial", "Differential", "Equations"), + ) + for title, frame in zip(titles, frames): + title.arrange( + DOWN, + buff=MED_SMALL_BUFF, + aligned_edge=LEFT + ) + title.next_to(frame, UP, MED_LARGE_BUFF) + title.initials = VGroup(*[ + part[0] for part in title + ]) + titles[0][1].shift(0.05 * UP) + + # ODE content + ode = get_ode() + ode.set_width(frames[0].get_width() - MED_LARGE_BUFF) + ode.next_to(frames[0].get_top(), DOWN) + ts = ode.get_parts_by_tex("{t}") + one_input = TextMobject("One input") + one_input.next_to(frames[0].get_bottom(), UP) + o_arrows = VGroup(*[ + Arrow( + one_input.get_top(), + t.get_bottom(), + buff=0.2, + color=WHITE, + max_tip_length_to_length_ratio=0.075, + path_arc=pa + ) + for t, pa in zip(ts, [-0.7, 0, 0.7]) + ]) + o_arrows.set_stroke(width=3) + frames[0].add(ode, one_input, o_arrows) + + # PDE content + pde = TexMobject( + """ + \\frac{\\partial T}{\\partial t} + {(x, y, t)} = + \\frac{\\partial^2 T}{\\partial x^2} + {(x, y, t)} + + \\frac{\\partial^2 T}{\\partial y^2} + {(x, y, t)} + """, + tex_to_color_map={"{(x, y, t)}": WHITE} + ) + pde.set_width(frames[1].get_width() - MED_LARGE_BUFF) + pde.next_to(frames[1].get_top(), DOWN) + inputs = pde.get_parts_by_tex("{(x, y, t)}") + multi_input = TextMobject("Multiple inputs") + multi_input.next_to(frames[1].get_bottom(), UP) + p_arrows = VGroup(*[ + Arrow( + multi_input.get_top(), + mob.get_bottom(), + buff=0.2, + color=WHITE, + max_tip_length_to_length_ratio=0.075, + path_arc=pa + ) + for mob, pa in zip(inputs, [-0.7, 0, 0.7]) + ]) + p_arrows.set_stroke(width=3) + frames[1].add(pde, multi_input, p_arrows) + + self.add( + frames[0], + animated_frames[0], + titles[0] + ) + self.play( + Write(one_input), + ShowCreation(o_arrows, lag_ratio=0.5) + ) + self.wait(2) + self.play(titles[0].initials.set_color, BLUE) + self.wait(7) + + # Transition + self.play( + TransformFromCopy(*titles), + TransformFromCopy(*frames), + ) + self.play(VFadeIn(animated_frames[1])) + self.wait() + self.play(titles[1].initials.set_color, YELLOW) + self.wait(30) + + +class ReferencePiCollisionStateSpaces(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + title = TextMobject("The block collision puzzle") + title.scale(1.5) + title.to_edge(UP) + self.add(title) + + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + boundary = AnimatedBoundary(frames) + self.add(frames, boundary) + self.wait(15) + + +class XComponentArrows(Scene): + def construct(self): + field = VectorField( + lambda p: np.array([p[1], 0, 0]) + ) + field.set_opacity(0.75) + for u in (1, -1): + field.sort(lambda p: u * p[0]) + self.play(LaggedStartMap( + GrowArrow, field, + lag_ratio=0.1, + run_time=1 + )) + self.play(FadeOut(field)) + + +class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): + 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) + + theta_dots = VGroup(*filter( + lambda m: ( + isinstance(m, SingleStringTexMobject) and + "{\\dot\\theta}" == m.get_tex_string() + ), + system1.get_family(), + )) + + self.add(ode) + self.play(FadeInFrom(so_word, 0.5 * DOWN)) + self.wait() + + 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(LaggedStartMap( + ShowCreationThenFadeAround, + theta_dots, + surrounding_rectangle_config={ + "color": PINK, + } + )) + + 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) + + # Orient + self.set_camera_orientation( + phi=70 * DEGREES, + theta=-110 * DEGREES, + ) + self.begin_ambient_camera_rotation() + + def add_bodies(self): + masses = self.masses + colors = self.colors + + bodies = self.bodies = VGroup() + velocity_vectors = VGroup() + + centers = self.get_initial_positions() + + 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.radius = 0.08 * np.sqrt(mass) + body.set_width(2 * body.radius) + + body.point = center + body.move_to(center) + + body.velocity = self.get_initial_velocity( + center, centers, mass + ) + + vect = self.get_velocity_vector_mob(body) + + bodies.add(body) + velocity_vectors.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 get_initial_positions(self): + return [ + np.dot( + 4 * (np.random.random(3) - 0.5), + [RIGHT, UP, OUT] + ) + for x in range(len(self.masses)) + ] + + def get_initial_velocity(self, center, centers, mass): + to_others = [ + center - center2 + for center2 in centers + ] + velocity = 0.2 * mass * normalize(np.cross(*filter( + lambda diff: get_norm(diff) > 0, + to_others + ))) + return velocity + + def add_trajectories(self): + def update_trajectory(traj, dt): + new_point = traj.body.point + 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): + bodies = self.bodies + bodies.add_updater(self.update_bodies) + # Break it up to see partial files as + # it's rendered + self.add(bodies) + for x in range(int(self.play_time)): + self.wait() + + # + 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_bodies(self, bodies, dt): + G = self.G + + num_mid_steps = 1000 + for x in range(num_mid_steps): + for body in bodies: + acceleration = np.zeros(3) + for body2 in bodies: + if body2 is body: + continue + diff = body2.point - body.point + m2 = body2.mass + R = get_norm(diff) + acceleration += G * m2 * diff / (R**3) + body.point += body.velocity * dt / num_mid_steps + body.velocity += acceleration * dt / num_mid_steps + for body in bodies: + body.move_to(body.point) + return bodies + + +class AltThreeBodiesInSpace(ThreeBodiesInSpace): + CONFIG = { + "random_seed": 6, + "masses": [1, 2, 6], + } + + +class TwoBodiesInSpace(ThreeBodiesInSpace): + CONFIG = { + "colors": [GREY, BLUE], + "masses": [6, 36], + "play_time": 60, + } + + def construct(self): + self.add_axes() + self.add_bodies() + self.add_trajectories() + self.add_velocity_vectors() + self.add_force_vectors() + self.let_play() + + def add_bodies(self): + super().add_bodies() + for body in self.bodies: + body.point = 3 * normalize(body.get_center()) + # body.point += 2 * IN + # body.velocity += (4 / 60) * OUT + body.move_to(body.point) + + def get_initial_positions(self): + return [ + np.dot( + 6 * (np.random.random(3) - 0.5), + [RIGHT, UP, ORIGIN] + ) + for x in range(len(self.masses)) + ] + + def get_initial_velocity(self, center, centers, mass): + return 0.75 * normalize(np.cross(center, OUT)) + + def add_velocity_vectors(self): + vectors = VGroup(*[ + self.get_velocity_vector(body) + for body in self.bodies + ]) + self.velocity_vectors = vectors + self.add(vectors) + + def get_velocity_vector(self, body): + def create_vector(b): + v = Vector( + b.velocity, + color=RED, + max_stroke_width_to_length_ratio=3, + ) + v.set_stroke(width=3) + v.shift( + b.point + b.radius * normalize(b.velocity) - + v.get_start(), + ) + v.set_shade_in_3d(True) + return v + return always_redraw(lambda: create_vector(body)) + + def add_force_vectors(self): + vectors = VGroup(*[ + self.get_force_vector(b1, b2) + for (b1, b2) in (self.bodies, self.bodies[::-1]) + ]) + self.force_vectors = vectors + self.add(vectors) + + def get_force_vector(self, body1, body2): + def create_vector(b1, b2): + r = b2.point - b1.point + F = r / (get_norm(r)**3) + v = Vector( + 4 * F, + color=YELLOW, + max_stroke_width_to_length_ratio=3, + ) + v.set_stroke(width=3) + v.shift( + b1.point + b1.radius * normalize(F) - + v.get_start(), + ) + v.set_shade_in_3d(True) + return v + return always_redraw(lambda: create_vector(body1, body2)) + + +class TwoBodiesWithZPart(TwoBodiesInSpace): + def add_bodies(self): + super().add_bodies() + for body in self.bodies: + body.point += 3 * IN + body.velocity += (6 / 60) * OUT + + +class LoveExample(PiCreatureScene): + def construct(self): + self.show_hearts() + self.add_love_trackers() + self.break_down_your_rule() + self.break_down_their_rule() + + def create_pi_creatures(self): + you = You() + you.shift(FRAME_WIDTH * LEFT / 4) + you.to_edge(DOWN) + + tau = TauCreature(color=GREEN) + tau.flip() + tau.shift(FRAME_WIDTH * RIGHT / 4) + tau.to_edge(DOWN) + + self.you = you + self.tau = tau + return (you, tau) + + def show_hearts(self): + you, tau = self.you, self.tau + hearts = VGroup() + n_hearts = 20 + for x in range(n_hearts): + heart = SuitSymbol("hearts") + heart.scale(0.5 + 2 * np.random.random()) + heart.shift(np.random.random() * 4 * RIGHT) + heart.shift(np.random.random() * 4 * UP) + hearts.add(heart) + hearts.move_to(2 * DOWN) + hearts.add_updater(lambda m, dt: m.shift(2 * dt * UP)) + + self.add(hearts) + self.play( + LaggedStartMap( + UpdateFromAlphaFunc, hearts, + lambda heart: ( + heart, + lambda h, a: h.set_opacity( + there_and_back(a) + ).shift(0.02 * UP) + ), + lag_ratio=0.01, + run_time=3, + suspend_mobject_updating=False, + ), + ApplyMethod( + you.change, 'hooray', tau.eyes, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ), + ApplyMethod( + tau.change, 'hooray', you.eyes, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ), + ) + self.remove(hearts) + self.wait() + + def add_love_trackers(self): + self.init_ps_point() + self.add_love_decimals() + self.add_love_number_lines() + self.tie_creature_state_to_ps_point() + + self.play(Rotating( + self.ps_point, + radians=-7 * TAU / 8, + about_point=ORIGIN, + run_time=10, + rate_func=linear, + )) + self.wait() + + def break_down_your_rule(self): + label1 = self.love_1_label + label2 = self.love_2_label + ps_point = self.ps_point + + up_arrow = Vector(UP, color=GREEN) + down_arrow = Vector(DOWN, color=RED) + for arrow in (up_arrow, down_arrow): + arrow.next_to(label1, RIGHT) + + self.play(GrowArrow(up_arrow)) + self.play( + self.tau.love_eyes.scale, 1.25, + self.tau.love_eyes.set_color, BLUE_C, + rate_func=there_and_back, + ) + self.play( + ps_point.shift, 6 * RIGHT, + run_time=2, + ) + self.wait() + ps_point.shift(13 * DOWN) + self.play( + FadeOut(up_arrow), + GrowArrow(down_arrow), + ) + self.play( + ps_point.shift, 11 * LEFT, + run_time=3, + ) + self.wait() + + # Derivative + equation = get_love_equation1() + equation.shift(0.5 * UP) + deriv, equals, a, h2 = equation + + self.play( + Write(deriv[:-1]), + Write(equals), + Write(a), + TransformFromCopy(label1[0], deriv.heart), + TransformFromCopy(label2[0], h2), + ) + self.wait() + self.play( + equation.scale, 0.5, + equation.to_corner, UL, + FadeOut(down_arrow) + ) + + def break_down_their_rule(self): + label1 = self.love_1_label + label2 = self.love_2_label + ps_point = self.ps_point + + up_arrow = Vector(UP, color=GREEN) + down_arrow = Vector(DOWN, color=RED) + for arrow in (up_arrow, down_arrow): + arrow.next_to(label2, RIGHT) + + # Derivative + equation = get_love_equation2() + equation.shift(0.5 * UP) + deriv, equals, mb, h1 = equation + + self.play( + Write(deriv[:-1]), + Write(equals), + Write(mb), + TransformFromCopy(label1[0], h1), + TransformFromCopy(label2[0], deriv.heart), + ) + + self.play(GrowArrow(up_arrow)) + self.play( + ps_point.shift, 13 * UP, + run_time=3, + ) + self.wait() + self.play( + ps_point.shift, 11 * RIGHT, + ) + self.play( + FadeOut(up_arrow), + GrowArrow(down_arrow), + ) + self.play( + ps_point.shift, 13 * DOWN, + run_time=3, + ) + self.wait() + + # + def init_ps_point(self): + self.ps_point = VectorizedPoint(np.array([5.0, 5.0, 0])) + + def get_love1(self): + return self.ps_point.get_location()[0] + + def get_love2(self): + return self.ps_point.get_location()[1] + + def set_loves(self, love1=None, love2=None): + if love1 is not None: + self.ps_point.set_x(love1) + if love2 is not None: + self.ps_point.set_x(love2) + + def add_love_decimals(self): + self.love_1_label = self.add_love_decimal( + 1, self.get_love1, self.you.get_color(), -3, + ) + self.love_2_label = self.add_love_decimal( + 2, self.get_love2, self.tau.get_color(), 3, + ) + + def add_love_decimal(self, index, value_func, color, x_coord): + d = DecimalNumber(include_sign=True) + d.add_updater(lambda d: d.set_value(value_func())) + + label = get_heart_var(index) + label.move_to(x_coord * RIGHT) + label.to_edge(UP) + eq = TexMobject("=") + eq.next_to(label, RIGHT, SMALL_BUFF) + eq.shift(SMALL_BUFF * UP) + d.next_to(eq, RIGHT, SMALL_BUFF) + + self.add(label, eq, d) + return VGroup(label, eq, d) + + def add_love_number_lines(self): + nl1 = NumberLine( + x_min=-8, + x_max=8, + unit_size=0.25, + tick_frequency=2, + number_scale_val=0.25, + ) + nl1.set_stroke(width=1) + nl1.next_to(self.love_1_label, DOWN) + nl1.add_numbers(*range(-6, 8, 2)) + + nl2 = nl1.copy() + nl2.next_to(self.love_2_label, DOWN) + + dot1 = Dot(color=self.you.get_color()) + dot1.add_updater(lambda d: d.move_to( + nl1.number_to_point(self.get_love1()) + )) + dot2 = Dot(color=self.tau.get_color()) + dot2.add_updater(lambda d: d.move_to( + nl2.number_to_point(self.get_love2()) + )) + + self.add(nl1, nl2, dot1, dot2) + + def get_love_eyes(self, eyes): + hearts = VGroup() + for eye in eyes: + heart = SuitSymbol("hearts") + heart.match_width(eye) + heart.move_to(eye) + heart.scale(1.25) + heart.set_stroke(BLACK, 1) + hearts.add(heart) + hearts.add_updater( + lambda m: m.move_to(eyes) + ) + return hearts + + def tie_creature_state_to_ps_point(self): + # Quite a mess, but I'm coding in a rush here... + you = self.you + you_copy = you.copy() + tau = self.tau + tau_copy = tau.copy() + + you.love_eyes = self.get_love_eyes(you.eyes) + tau.love_eyes = self.get_love_eyes(tau.eyes) + + self.add(you.love_eyes) + self.add(tau.love_eyes) + + you_height = you.get_height() + tau_height = tau.get_height() + + you_bottom = you.get_bottom() + tau_bottom = tau.get_bottom() + + def update_you(y): + love = self.get_love1() + + cutoff_values = [ + -5, -3, -1, 1, 3, 5 + ] + modes = [ + "angry", "sassy", "hesitant", + "plain", + "happy", "hooray", "surprised", + ] + + if love < cutoff_values[0]: + y.change(modes[0]) + elif love >= cutoff_values[-1]: + y.change(modes[-1]) + else: + i = 0 + while cutoff_values[i] < love: + i += 1 + m1 = modes[i - 1] + m2 = modes[i] + y.change(m1) + you_copy.change(m2) + for mob in y, you_copy: + mob.set_height(you_height) + mob.move_to(you_bottom, DOWN) + + alpha = inverse_interpolate( + cutoff_values[i - 1], + cutoff_values[i], + love, + ) + s_alpha = squish_rate_func(smooth, 0.25, 1)(alpha) + if s_alpha > 0: + y.align_data(you_copy) + f1 = y.family_members_with_points() + f2 = you_copy.family_members_with_points() + for sm1, sm2 in zip(f1, f2): + sm1.interpolate(sm1, sm2, s_alpha) + y.look_at(tau.eyes) + if love < -4: + y.look_at(LEFT_SIDE) + # y.move_to( + # you_bottom + 0.025 * love * RIGHT, DOWN, + # ) + + l_alpha = np.clip( + inverse_interpolate(5, 5.5, love), + 0, 1 + ) + y.eyes.set_opacity(1 - l_alpha) + y.love_eyes.set_opacity(l_alpha) + + return y + + def update_tau(t): + love = self.get_love2() + + cutoff_values = [ + -5, -1.7, 1.7, 5 + ] + modes = [ + "angry", "confused", "plain", + "hooray", "hooray" + ] + + if love < cutoff_values[0]: + t.change(modes[0]) + elif love >= cutoff_values[-1]: + t.change(modes[-1]) + else: + i = 0 + while cutoff_values[i] < love: + i += 1 + m1 = modes[i - 1] + m2 = modes[i] + t.change(m1) + tau_copy.change(m2) + for mob in t, tau_copy: + mob.set_height(tau_height) + mob.move_to(tau_bottom, DOWN) + + alpha = inverse_interpolate( + cutoff_values[i - 1], + cutoff_values[i], + love, + ) + s_alpha = squish_rate_func(smooth, 0.25, 1)(alpha) + if s_alpha > 0: + t.align_data(tau_copy) + f1 = t.family_members_with_points() + f2 = tau_copy.family_members_with_points() + for sm1, sm2 in zip(f1, f2): + sm1.interpolate(sm1, sm2, s_alpha) + # t.move_to( + # tau_bottom + 0.025 * love * LEFT, DOWN, + # ) + t.look_at(you.eyes) + if love < -4: + t.look_at(RIGHT_SIDE) + + l_alpha = np.clip( + inverse_interpolate(5, 5.5, love), + 0, 1 + ) + t.eyes.set_opacity(1 - l_alpha) + t.love_eyes.set_opacity(l_alpha) + + you.add_updater(update_you) + tau.add_updater(update_tau) + + self.pi_creatures = VGroup() + + +class ComparePhysicsToLove(Scene): + def construct(self): + ode = get_ode() + ode.to_edge(UP) + thetas = ode.get_parts_by_tex("theta") + + love = VGroup( + get_love_equation1(), + get_love_equation2(), + ) + love.scale(0.5) + love.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + love.move_to(DOWN) + hearts = VGroup(*filter( + lambda sm: isinstance(sm, SuitSymbol), + love.get_family() + )) + + arrow = DoubleArrow(love.get_top(), ode.get_bottom()) + + self.play(FadeInFrom(ode, DOWN)) + self.play(FadeInFrom(love, UP)) + self.wait() + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + thetas, + )) + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + hearts, + )) + self.wait() + self.play(ShowCreation(arrow)) + self.wait() + + +class FramesComparingPhysicsToLove(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + ode = get_ode() + ode.to_edge(UP) + + love = VGroup( + get_love_equation1(), + get_love_equation2(), + ) + love.scale(0.5) + love.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + + frames = VGroup(*[ + ScreenRectangle( + height=3.5, + fill_color=BLACK, + fill_opacity=1, + stroke_width=0, + ) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + frames.shift(DOWN) + + animated_frames = AnimatedBoundary(frames) + + ode.next_to(frames[0], UP) + love.next_to(frames[1], UP) + + self.add(frames, animated_frames) + self.add(ode, love) + + self.wait(15) + + +class PassageOfTime(Scene): + def construct(self): + clock = Clock() + clock[0].set_color(BLUE) + clock.set_stroke(width=1) + clock.scale(0.8) + clock.to_corner(UL) + passage = ClockPassesTime( + clock, + hours_passed=48, + ) + self.play(passage, run_time=10) + + +class WriteODESolvingCode(ExternallyAnimatedScene): + 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/diffyq/part1/wordy_scenes.py b/active_projects/diffyq/part1/wordy_scenes.py new file mode 100644 index 00000000..91dc0ff1 --- /dev/null +++ b/active_projects/diffyq/part1/wordy_scenes.py @@ -0,0 +1,855 @@ +from manimlib.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): + quote = self.get_quote() + movers = VGroup(*quote[:-1].family_members_with_points()) + for mover in movers: + mover.save_state() + disc = Circle(radius=0.05) + disc.set_stroke(width=0) + disc.set_fill(BLACK, 0) + disc.move_to(mover) + mover.become(disc) + self.play( + FadeInFrom(quote.author_part, LEFT), + LaggedStartMap( + # FadeInFromLarge, + # quote[:-1].family_members_with_points(), + Restore, movers, + lag_ratio=0.005, + run_time=2, + ) + # FadeInFromDown(quote[:-1]), + # lag_ratio=0.01, + ) + self.wait() + self.play( + Write(quote.law_part.copy().set_color(YELLOW)), + run_time=1, + ) + self.wait() + self.play( + Write(quote.language_part.copy().set_color(BLUE)), + run_time=1.5, + ) + self.wait(2) + + def get_quote(self): + law_words = "laws of physics" + language_words = "language of differential equations" + author = "-Steven Strogatz" + 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] + ) + quote.law_part = quote.get_part_by_tex(law_words) + quote.language_part = quote.get_part_by_tex(language_words) + quote.author_part = quote.get_part_by_tex(author) + quote.set_width(12) + quote.to_edge(UP) + quote[-2].shift(SMALL_BUFF * LEFT) + quote.author_part.shift(RIGHT + 0.5 * DOWN) + quote.author_part.scale(1.2, about_edge=UL) + + return quote + + +class WriteInRadians(Scene): + def construct(self): + words = TextMobject("In radians") + words.set_color(YELLOW) + square = SurroundingRectangle(TexMobject("\\theta")) + square.next_to(words, UP) + self.play(ShowCreation(square)) + self.play(Write(words), FadeOut(square)) + self.wait() + + +class XEqLThetaToCorner(Scene): + def construct(self): + equation = TexMobject( + "x = L\\theta", + tex_to_color_map={ + "x": GREEN, + "\\theta": BLUE, + } + ) + equation.move_to(DOWN + 3 * RIGHT) + self.add(equation) + self.play(equation.to_corner, DL, {"buff": LARGE_BUFF}) + self.wait() + + +class ComingUp(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + + def construct(self): + frame = ScreenRectangle( + stroke_width=0, + fill_color=BLACK, + fill_opacity=1, + height=6 + ) + title = TextMobject("Coming up") + title.scale(1.5) + title.to_edge(UP) + frame.next_to(title, DOWN) + animated_frame = AnimatedBoundary(frame) + self.add(frame, title, animated_frame) + self.wait(10) + + +class InputLabel(Scene): + def construct(self): + label = TextMobject("Input") + label.scale(1.25) + arrow = Vector(UP) + arrow.next_to(label, UP) + self.play( + FadeInFrom(label, UP), + GrowArrow(arrow) + ) + self.wait() + + +class ReallyHardToSolve(Scene): + def construct(self): + words = TextMobject( + "They're", "really\\\\", + "freaking", "hard\\\\", + "to", "solve\\\\", + ) + words.set_height(6) + + self.wait() + for word in words: + wait_time = 0.05 * len(word) + self.add(word) + self.wait(wait_time) + self.wait() + + +class ReasonForSolution(Scene): + def construct(self): + # Words + eq_word = TextMobject("Differential\\\\Equation") + s_word = TextMobject("Solution") + u_word = TextMobject("Understanding") + c_word = TextMobject("Computation") + cu_group = VGroup(u_word, c_word) + cu_group.arrange(DOWN, buff=2) + group = VGroup(eq_word, s_word, cu_group) + group.arrange(RIGHT, buff=2) + # words = VGroup(eq_word, s_word, u_word, c_word) + + # Arrows + arrows = VGroup( + Arrow(eq_word.get_right(), s_word.get_left()), + Arrow(s_word.get_right(), u_word.get_left()), + Arrow(s_word.get_right(), c_word.get_left()), + ) + arrows.set_color(LIGHT_GREY) + new_arrows = VGroup( + Arrow( + eq_word.get_corner(UR), + u_word.get_left(), + path_arc=-60 * DEGREES, + ), + Arrow( + eq_word.get_corner(DR), + c_word.get_left(), + path_arc=60 * DEGREES, + ), + ) + new_arrows.set_color(BLUE) + + # Define first examples + t2c = { + "{x}": BLUE, + "{\\dot x}": RED, + } + equation = TexMobject( + "{\\dot x}(t) = k {x}(t)", + tex_to_color_map=t2c, + ) + equation.next_to(eq_word, DOWN) + solution = TexMobject( + "{x}(t) = x_0 e^{kt}", + tex_to_color_map=t2c, + ) + solution.next_to(s_word, DOWN, MED_LARGE_BUFF) + equation.align_to(solution, DOWN) + + axes = Axes( + x_min=-1, + x_max=5.5, + y_min=-1, + y_max=4.5, + y_axis_config={"unit_size": 0.5} + ) + axes.set_stroke(width=2) + graph_line = axes.get_graph( + lambda x: np.exp(0.4 * x) + ) + graph_line.set_stroke(width=2) + graph = VGroup(axes, graph_line) + graph.scale(0.5) + graph.next_to(u_word, UP) + + computation = TexMobject( + # "\\displaystyle " + "e^x = \\sum_{n=0}^\\infty " + "\\frac{x^n}{n!}" + ) + computation.next_to(c_word, DOWN) + + first_examples = VGroup( + equation, solution, graph, computation + ) + + # Second example + ode = get_ode() + ode.scale(0.75) + second_examples = VGroup( + ode, + TexMobject("???").set_color(LIGHT_GREY), + ScreenRectangle( + height=2, + stroke_width=1, + ), + ) + for fe, se in zip(first_examples, second_examples): + se.move_to(fe, DOWN) + + ode.shift(2 * SMALL_BUFF * DOWN) + ode.add_to_back(BackgroundRectangle(ode[-4:])) + + self.add(eq_word) + self.add(equation) + self.play( + FadeInFrom(s_word, LEFT), + GrowArrow(arrows[0]), + TransformFromCopy(equation, solution) + ) + self.wait() + self.play( + FadeInFrom(c_word, UL), + GrowArrow(arrows[2]), + FadeInFrom(computation, UP) + ) + self.wait() + self.play( + FadeInFrom(u_word, DL), + GrowArrow(arrows[1]), + FadeInFromDown(graph) + ) + self.wait(2) + + self.play( + FadeOut(first_examples), + FadeIn(second_examples[:2]) + ) + self.wait() + self.play( + arrows.fade, 0.75, + s_word.fade, 0.75, + second_examples[1].fade, 0.75, + ShowCreation(new_arrows[0]), + FadeIn(second_examples[2]) + ) + self.play( + ShowCreation(new_arrows[1]), + Animation(second_examples), + ) + self.wait() + + +class WritePhaseSpace(Scene): + def construct(self): + word = TextMobject("Phase space") + word.scale(2) + word.shift(FRAME_WIDTH * LEFT / 4) + word.to_edge(UP) + word.add_background_rectangle() + + lines = VGroup(*[ + Line(v, 1.3 * v) + for v in compass_directions(50) + ]) + lines.replace(word, stretch=True) + lines.scale(1.5) + lines.set_stroke(YELLOW) + lines.shuffle() + + self.add(word) + self.play( + ShowPassingFlashWithThinningStrokeWidth( + lines, + lag_ratio=0.002, + run_time=1.5, + time_width=0.9, + n_segments=5, + ) + ) + self.wait() + + +class GleickQuote(Scene): + def construct(self): + quote = TextMobject( + "``[Phase space is] one of the most\\\\", + "powerful inventions", "of modern science.''\\\\", + ) + quote.power_part = quote.get_part_by_tex("power") + book = ImageMobject("ChaosBookCover") + book.set_height(5) + book.next_to(ORIGIN, LEFT) + book.to_edge(DOWN) + gleick = ImageMobject("JamesGleick") + gleick.set_height(5) + gleick.next_to(ORIGIN, RIGHT) + gleick.to_edge(DOWN) + quote.to_edge(UP) + + self.play( + FadeInFrom(book, RIGHT), + FadeInFrom(gleick, LEFT), + ) + self.wait() + self.play(Write(quote)) + self.play(Write( + quote.power_part.copy().set_color(BLUE), + run_time=1 + )) + self.wait() + + +class WritePhaseFlow(Scene): + def construct(self): + words = TextMobject("Phase flow") + words.scale(2) + words.shift(FRAME_WIDTH * LEFT / 4) + words.to_edge(UP) + words.add_background_rectangle() + self.play(Write(words)) + self.wait() + + +class ShowSineValues(Scene): + def construct(self): + angle_tracker = ValueTracker(60 * DEGREES) + get_angle = angle_tracker.get_value + formula = always_redraw( + lambda: self.get_sine_formula(get_angle()) + ) + self.add(formula) + + self.play( + angle_tracker.set_value, 0, + run_time=3, + ) + self.wait() + self.play( + angle_tracker.set_value, 90 * DEGREES, + run_time=3, + ) + self.wait() + + def get_sine_formula(self, angle): + sin, lp, rp = TexMobject( + "\\sin", "(", ") = " + ) + input_part = Integer( + angle / DEGREES, + unit="^\\circ", + ) + input_part.set_color(YELLOW) + output_part = DecimalNumber( + np.sin(input_part.get_value() * DEGREES), + num_decimal_places=3, + ) + result = VGroup( + sin, lp, input_part, rp, output_part + ) + result.arrange(RIGHT, buff=SMALL_BUFF) + sin.scale(1.1, about_edge=DOWN) + lp.align_to(rp, UP) + return result + + +class SetAsideSeekingSolution(Scene): + 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 ThreeBodyTitle(Scene): + def construct(self): + title = TextMobject("Three body problem") + title.scale(1.5) + title.to_edge(UP) + self.add(title) + + +class ThreeBodySymbols(Scene): + def construct(self): + self.init_coord_groups() + 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)) + + +class ThreeBodyEquation(Scene): + def construct(self): + x1 = "\\vec{\\textbf{x}}_1" + x2 = "\\vec{\\textbf{x}}_2" + x3 = "\\vec{\\textbf{x}}_3" + kw = { + "tex_to_color_map": { + x1: RED, + x2: GREEN, + x3: BLUE, + } + } + equations = VGroup(*[ + TexMobject( + "{d^2", t1, "\\over dt^2}", "=", + "G", "\\left(" + "{" + m2, "(", t2, "-", t1, ")" + "\\over" + "||", t2, "-", t1, "||^3}", + "+", + "{" + m3, "(", t3, "-", t1, ")" + "\\over" + "||", t3, "-", t1, "||^3}", + "\\right)", + **kw + ) + for t1, t2, t3, m1, m2, m3 in [ + (x1, x2, x3, "m_1", "m_2", "m_3"), + (x2, x3, x1, "m_2", "m_3", "m_1"), + (x3, x1, x2, "m_3", "m_1", "m_2"), + ] + ]) + equations.arrange(DOWN, buff=LARGE_BUFF) + + self.play(LaggedStartMap( + FadeInFrom, equations, + lambda m: (m, UP), + lag_ratio=0.2, + )) + self.wait() + + +class JumpToThisPoint(Scene): + def construct(self): + dot = Dot(color=YELLOW) + dot.scale(0.5) + + arrow = Vector(DR, color=WHITE) + arrow.next_to(dot, UL, SMALL_BUFF) + words = TextMobject( + "Jump directly to\\\\", + "this point?", + ) + words.add_background_rectangle_to_submobjects() + words.next_to(arrow.get_start(), UP, SMALL_BUFF) + + self.play( + FadeInFromLarge(dot, 20), + rate_func=rush_into, + ) + self.play( + GrowArrow(arrow), + FadeInFromDown(words), + ) + + +class ChaosTitle(Scene): + def construct(self): + title = TextMobject("Chaos theory") + title.scale(1.5) + title.to_edge(UP) + line = Line(LEFT, RIGHT) + line.set_width(FRAME_WIDTH - 1) + line.next_to(title, DOWN) + + self.play( + Write(title), + ShowCreation(line), + ) + self.wait() + + +class RevisitQuote(StrogatzQuote, PiCreatureScene): + def construct(self): + quote = self.get_quote() + quote.law_part.set_color(YELLOW) + quote.language_part.set_color(BLUE) + quote.set_stroke(BLACK, 6, background=True) + quote.scale(0.8, about_edge=UL) + + new_langauge_part = TextMobject( + "\\Large Language of differential equations" + ) + new_langauge_part.to_edge(UP) + new_langauge_part.match_style(quote.language_part) + + randy = self.pi_creature + + self.play( + FadeInFrom(quote[:-1], DOWN), + FadeInFrom(quote[-1:], LEFT), + randy.change, "raise_right_hand", + ) + self.play(Blink(randy)) + self.play(randy.change, "angry") + self.play( + Blink(randy), + VFadeOut(randy, run_time=3) + ) + mover = VGroup(quote.language_part) + self.add(quote, mover) + self.play( + ReplacementTransform( + mover, new_langauge_part, + ), + *[ + FadeOut(part) + for part in quote + if part is not quote.language_part + ], + run_time=2, + ) + self.wait() + + +class EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Juan Benet", + "Vassili Philippov", + "Burt Humburg", + "Carlos Vergara Del Rio", + "Matt Russell", + "Scott Gray", + "soekul", + "Tihan Seale", + "Ali Yahya", + "dave nicponski", + "Evan Phillips", + "Graham", + "Joseph Kelly", + "Kaustuv DeBiswas", + "LambdaLabs", + "Lukas Biewald", + "Mike Coleman", + "Peter Mcinerney", + "Quantopian", + "Roy Larson", + "Scott Walter, Ph.D.", + "Yana Chernobilsky", + "Yu Jun", + "Jordan Scales", + "Lukas -krtek.net- Novy", + "John Shaughnessy", + "Britt Selvitelle", + "David Gow", + "J", + "Jonathan Wilson", + "Joseph John Cox", + "Magnus Dahlström", + "Randy C. Will", + "Ryan Atallah", + "Luc Ritchie", + "1stViewMaths", + "Adrian Robinson", + "Alexis Olson", + "Andreas Benjamin Brössel", + "Andrew Busey", + "Ankalagon", + "Antonio Juarez", + "Arjun Chakroborty", + "Art Ianuzzi", + "Awoo", + "Bernd Sing", + "Boris Veselinovich", + "Brian Staroselsky", + "Chad Hurst", + "Charles Southerland", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Cooper Jones", + "Danger Dai", + "Dave B", + "Dave Kester", + "David Clark", + "DeathByShrimp", + "Delton Ding", + "eaglle", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Giovanni Filippi", + "Hal Hildebrand", + "Herman Dieset", + "Hitoshi Yamauchi", + "Isaac Jeffrey Lee", + "j eduardo perez", + "Jacob Magnuson", + "Jameel Syed", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Kai-Siang Ang", + "Kanan Gill", + "L0j1k", + "Lee Redden", + "Linh Tran", + "Ludwig Schubert", + "Magister Mugit", + "Mark B Bahu", + "Mark Heising", + "Martin Price", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Cocke", + "Michael Faust", + "Michael Hardel", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nikita Lesnikov", + "Omar Zrien", + "Owen Campbell-Moore", + "Peter Ehrnstrom", + "RedAgent14", + "rehmi post", + "Richard Burgmann", + "Richard Comish", + "Ripta Pasay", + "Rish Kundalia", + "Robert Teed", + "Roobie", + "Ryan Williams", + "Samuel D. Judge", + "Solara570", + "Stevie Metke", + "Tal Einav", + "Ted Suzman", + "Valeriy Skobelev", + "Xavier Bernard", + "Yavor Ivanov", + "Yaw Etse", + "YinYangBalance.Asia", + "Zach Cardwell", + ] + } diff --git a/active_projects/diffyq/part2/fourier_series.py b/active_projects/diffyq/part2/fourier_series.py new file mode 100644 index 00000000..6a85ebfc --- /dev/null +++ b/active_projects/diffyq/part2/fourier_series.py @@ -0,0 +1,898 @@ +from manimlib.imports import * +# import scipy + + +class FourierCirclesScene(Scene): + CONFIG = { + "n_vectors": 10, + "big_radius": 2, + "colors": [ + BLUE_D, + BLUE_C, + BLUE_E, + GREY_BROWN, + ], + "circle_style": { + "stroke_width": 2, + }, + "vector_config": { + "buff": 0, + "max_tip_length_to_length_ratio": 0.35, + "tip_length": 0.15, + "max_stroke_width_to_length_ratio": 10, + "stroke_width": 2, + }, + "circle_config": { + "stroke_width": 1, + }, + "base_frequency": 1, + "slow_factor": 0.25, + "center_point": ORIGIN, + "parametric_function_step_size": 0.001, + } + + def setup(self): + self.slow_factor_tracker = ValueTracker( + self.slow_factor + ) + self.vector_clock = ValueTracker(0) + self.vector_clock.add_updater( + lambda m, dt: m.increment_value( + self.get_slow_factor() * dt + ) + ) + self.add(self.vector_clock) + + def get_slow_factor(self): + return self.slow_factor_tracker.get_value() + + def get_vector_time(self): + return self.vector_clock.get_value() + + # + def get_freqs(self): + n = self.n_vectors + all_freqs = list(range(n // 2, -n // 2, -1)) + all_freqs.sort(key=abs) + return all_freqs + + def get_coefficients(self): + return [complex(0) for x in range(self.n_vectors)] + + def get_color_iterator(self): + return it.cycle(self.colors) + + def get_rotating_vectors(self, freqs=None, coefficients=None): + vectors = VGroup() + self.center_tracker = VectorizedPoint(self.center_point) + + if freqs is None: + freqs = self.get_freqs() + if coefficients is None: + coefficients = self.get_coefficients() + + last_vector = None + for freq, coefficient in zip(freqs, coefficients): + if last_vector: + center_func = last_vector.get_end + else: + center_func = self.center_tracker.get_location + vector = self.get_rotating_vector( + coefficient=coefficient, + freq=freq, + center_func=center_func, + ) + vectors.add(vector) + last_vector = vector + return vectors + + def get_rotating_vector(self, coefficient, freq, center_func): + vector = Vector(RIGHT, **self.vector_config) + vector.scale(abs(coefficient)) + if abs(coefficient) == 0: + phase = 0 + else: + phase = np.log(coefficient).imag + vector.rotate(phase, about_point=ORIGIN) + vector.freq = freq + vector.phase = phase + vector.coefficient = coefficient + vector.center_func = center_func + vector.add_updater(self.update_vector) + return vector + + def update_vector(self, vector, dt): + time = self.get_vector_time() + vector.set_angle( + vector.phase + time * vector.freq * TAU + ) + vector.shift( + vector.center_func() - vector.get_start() + ) + return vector + + def get_circles(self, vectors): + return VGroup(*[ + self.get_circle( + vector, + color=color + ) + for vector, color in zip( + vectors, + self.get_color_iterator() + ) + ]) + + def get_circle(self, vector, color=BLUE): + circle = Circle(color=color, **self.circle_config) + circle.center_func = vector.get_start + circle.radius_func = vector.get_length + circle.add_updater(self.update_circle) + return circle + + def update_circle(self, circle): + circle.set_width(2 * circle.radius_func()) + circle.move_to(circle.center_func()) + return circle + + def get_vector_sum_path(self, vectors, color=YELLOW): + coefs = [v.coefficient for v in vectors] + freqs = [v.freq for v in vectors] + center = vectors[0].get_start() + + path = ParametricFunction( + lambda t: center + reduce(op.add, [ + complex_to_R3( + coef * np.exp(TAU * 1j * freq * t) + ) + for coef, freq in zip(coefs, freqs) + ]), + t_min=0, + t_max=1, + color=color, + step_size=self.parametric_function_step_size, + ) + return path + + # TODO, this should be a general animated mobect + def get_drawn_path(self, vectors, stroke_width=2, **kwargs): + path = self.get_vector_sum_path(vectors, **kwargs) + broken_path = CurvesAsSubmobjects(path) + broken_path.curr_time = 0 + + def update_path(path, dt): + # alpha = path.curr_time * self.get_slow_factor() + alpha = self.get_vector_time() + n_curves = len(path) + for a, sp in zip(np.linspace(0, 1, n_curves), path): + b = alpha - a + if b < 0: + width = 0 + else: + width = stroke_width * (1 - (b % 1)) + sp.set_stroke(width=width) + path.curr_time += dt + return path + + broken_path.set_color(YELLOW) + broken_path.add_updater(update_path) + return broken_path + + def get_y_component_wave(self, + vectors, + left_x=1, + color=PINK, + n_copies=2, + right_shift_rate=5): + path = self.get_vector_sum_path(vectors) + wave = ParametricFunction( + lambda t: op.add( + right_shift_rate * t * LEFT, + path.function(t)[1] * UP + ), + t_min=path.t_min, + t_max=path.t_max, + color=color, + ) + wave_copies = VGroup(*[ + wave.copy() + for x in range(n_copies) + ]) + wave_copies.arrange(RIGHT, buff=0) + top_point = wave_copies.get_top() + wave.creation = ShowCreation( + wave, + run_time=(1 / self.get_slow_factor()), + rate_func=linear, + ) + cycle_animation(wave.creation) + wave.add_updater(lambda m: m.shift( + (m.get_left()[0] - left_x) * LEFT + )) + + def update_wave_copies(wcs): + index = int( + wave.creation.total_time * self.get_slow_factor() + ) + wcs[:index].match_style(wave) + wcs[index:].set_stroke(width=0) + wcs.next_to(wave, RIGHT, buff=0) + wcs.align_to(top_point, UP) + wave_copies.add_updater(update_wave_copies) + + return VGroup(wave, wave_copies) + + def get_wave_y_line(self, vectors, wave): + return DashedLine( + vectors[-1].get_end(), + wave[0].get_end(), + stroke_width=1, + dash_length=DEFAULT_DASH_LENGTH * 0.5, + ) + + # Computing Fourier series + # i.e. where all the math happens + def get_coefficients_of_path(self, path, n_samples=10000, freqs=None): + if freqs is None: + freqs = self.get_freqs() + dt = 1 / n_samples + ts = np.arange(0, 1, dt) + samples = np.array([ + path.point_from_proportion(t) + for t in ts + ]) + samples -= self.center_point + complex_samples = samples[:, 0] + 1j * samples[:, 1] + + result = [] + for freq in freqs: + riemann_sum = np.array([ + np.exp(-TAU * 1j * freq * t) * cs + for t, cs in zip(ts, complex_samples) + ]).sum() * dt + result.append(riemann_sum) + + return result + + +class FourierSeriesIntroBackground4(FourierCirclesScene): + CONFIG = { + "n_vectors": 4, + "center_point": 4 * LEFT, + "run_time": 30, + "big_radius": 1.5, + } + + def construct(self): + circles = self.get_circles() + path = self.get_drawn_path(circles) + wave = self.get_y_component_wave(circles) + h_line = always_redraw( + lambda: self.get_wave_y_line(circles, wave) + ) + + # Why? + circles.update(-1 / self.camera.frame_rate) + # + self.add(circles, path, wave, h_line) + self.wait(self.run_time) + + def get_ks(self): + return np.arange(1, 2 * self.n_vectors + 1, 2) + + def get_freqs(self): + return self.base_frequency * self.get_ks() + + def get_coefficients(self): + return self.big_radius / self.get_ks() + + +class FourierSeriesIntroBackground8(FourierSeriesIntroBackground4): + CONFIG = { + "n_vectors": 8, + } + + +class FourierSeriesIntroBackground12(FourierSeriesIntroBackground4): + CONFIG = { + "n_vectors": 12, + } + + +class FourierSeriesIntroBackground20(FourierSeriesIntroBackground4): + CONFIG = { + "n_vectors": 20, + } + + +class FourierOfPiSymbol(FourierCirclesScene): + CONFIG = { + "n_vectors": 51, + "center_point": ORIGIN, + "slow_factor": 0.1, + "n_cycles": 1, + "tex": "\\pi", + "start_drawn": False, + "max_circle_stroke_width": 1, + } + + def construct(self): + self.add_vectors_circles_path() + for n in range(self.n_cycles): + self.run_one_cycle() + + def add_vectors_circles_path(self): + path = self.get_path() + coefs = self.get_coefficients_of_path(path) + vectors = self.get_rotating_vectors(coefficients=coefs) + circles = self.get_circles(vectors) + self.set_decreasing_stroke_widths(circles) + # approx_path = self.get_vector_sum_path(circles) + drawn_path = self.get_drawn_path(vectors) + if self.start_drawn: + self.vector_clock.increment_value(1) + + self.add(path) + self.add(vectors) + self.add(circles) + self.add(drawn_path) + + self.vectors = vectors + self.circles = circles + self.path = path + self.drawn_path = drawn_path + + def run_one_cycle(self): + time = 1 / self.slow_factor + self.wait(time) + + def set_decreasing_stroke_widths(self, circles): + mcsw = self.max_circle_stroke_width + for k, circle in zip(it.count(1), circles): + circle.set_stroke(width=max( + # mcsw / np.sqrt(k), + mcsw / k, + mcsw, + )) + return circles + + def get_path(self): + tex_mob = TexMobject(self.tex) + tex_mob.set_height(6) + path = tex_mob.family_members_with_points()[0] + path.set_fill(opacity=0) + path.set_stroke(WHITE, 1) + return path + + +class FourierOfName(FourierOfPiSymbol): + CONFIG = { + "n_vectors": 100, + "name_color": WHITE, + "name_text": "Abc", + "time_per_symbol": 5, + "slow_factor": 1 / 5, + } + + def construct(self): + name = TextMobject(self.name_text) + max_width = FRAME_WIDTH - 2 + max_height = FRAME_HEIGHT - 2 + name.set_width(max_width) + if name.get_height() > max_height: + name.set_height(max_height) + + circles = VGroup(VectorizedPoint()) + for path in name.family_members_with_points(): + for subpath in path.get_subpaths(): + sp_mob = VMobject() + sp_mob.set_points(subpath) + coefs = self.get_coefficients_of_path(sp_mob) + new_circles = self.get_circles( + coefficients=coefs + ) + self.set_decreasing_stroke_widths(new_circles) + drawn_path = self.get_drawn_path(new_circles) + drawn_path.clear_updaters() + drawn_path.set_stroke(self.name_color, 3) + + new_circles.suspend_updating() + self.play(ReplacementTransform(circles, new_circles)) + new_circles.resume_updating() + circles = new_circles + self.play( + ShowCreation(drawn_path), + rate_func=linear, + run_time=self.time_per_symbol + ) + circles.suspend_updating() + self.play(FadeOut(circles)) + self.wait(3) + + +class FourierOfPiSymbol5(FourierOfPiSymbol): + CONFIG = { + "n_vectors": 5, + "run_time": 10, + } + + +class FourierOfTrebleClef(FourierOfPiSymbol): + CONFIG = { + "n_vectors": 101, + "run_time": 10, + "start_drawn": True, + "file_name": "TrebleClef", + "height": 7.5, + } + + def get_shape(self): + shape = SVGMobject(self.file_name) + return shape + + def get_path(self): + shape = self.get_shape() + path = shape.family_members_with_points()[0] + path.set_height(self.height) + path.set_fill(opacity=0) + path.set_stroke(WHITE, 0) + return path + + +class FourierOfIP(FourierOfTrebleClef): + CONFIG = { + "file_name": "IP_logo2", + "height": 6, + "n_vectors": 100, + } + + # def construct(self): + # path = self.get_path() + # self.add(path) + + def get_shape(self): + shape = SVGMobject(self.file_name) + return shape + + def get_path(self): + shape = self.get_shape() + path = shape.family_members_with_points()[0] + path.add_line_to(path.get_start()) + # path.make_smooth() + + path.set_height(self.height) + path.set_fill(opacity=0) + path.set_stroke(WHITE, 0) + return path + + +class FourierOfEighthNote(FourierOfTrebleClef): + CONFIG = { + "file_name": "EighthNote" + } + + +class FourierOfN(FourierOfTrebleClef): + CONFIG = { + "height": 6, + "n_vectors": 1000, + } + + def get_shape(self): + return TexMobject("N") + + +class FourierNailAndGear(FourierOfTrebleClef): + CONFIG = { + "height": 6, + "n_vectors": 200, + "run_time": 100, + "slow_factor": 0.01, + "parametric_function_step_size": 0.0001, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SVGMobject("Nail_And_Gear")[1] + return shape + + +class FourierBatman(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_vectors": 100, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SVGMobject("BatmanLogo")[1] + return shape + + +class FourierHeart(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_vectors": 100, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SuitSymbol("hearts") + return shape + + def get_drawn_path(self, *args, **kwargs): + kwargs["stroke_width"] = 5 + path = super().get_drawn_path(*args, **kwargs) + path.set_color(PINK) + return path + + +class FourierNDQ(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_vectors": 1000, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + path = VMobject() + shape = TexMobject("NDQ") + for sp in shape.family_members_with_points(): + path.append_points(sp.points) + return path + + +class FourierGoogleG(FourierOfTrebleClef): + CONFIG = { + "n_vectors": 10, + "height": 5, + "g_colors": [ + "#4285F4", + "#DB4437", + "#F4B400", + "#0F9D58", + ] + } + + def get_shape(self): + g = SVGMobject("google_logo")[5] + g.center() + self.add(g) + return g + + def get_drawn_path(self, *args, **kwargs): + kwargs["stroke_width"] = 7 + path = super().get_drawn_path(*args, **kwargs) + + blue, red, yellow, green = self.g_colors + + path[:250].set_color(blue) + path[250:333].set_color(green) + path[333:370].set_color(yellow) + path[370:755].set_color(red) + path[755:780].set_color(yellow) + path[780:860].set_color(green) + path[860:].set_color(blue) + + return path + + +class ExplainCircleAnimations(FourierCirclesScene): + CONFIG = { + "n_vectors": 100, + "center_point": 2 * DOWN, + "n_top_circles": 9, + "path_height": 3, + } + + def construct(self): + self.add_path() + self.add_circles() + self.wait(8) + self.organize_circles_in_a_row() + self.show_frequencies() + self.show_examples_for_frequencies() + self.show_as_vectors() + self.show_vector_sum() + self.tweak_starting_vectors() + + def add_path(self): + self.path = self.get_path() + self.add(self.path) + + def add_circles(self): + coefs = self.get_coefficients_of_path(self.path) + self.circles = self.get_circles(coefficients=coefs) + + self.add(self.circles) + self.drawn_path = self.get_drawn_path(self.circles) + self.add(self.drawn_path) + + def organize_circles_in_a_row(self): + circles = self.circles + top_circles = circles[:self.n_top_circles].copy() + + center_trackers = VGroup() + for circle in top_circles: + tracker = VectorizedPoint(circle.center_func()) + circle.center_func = tracker.get_location + center_trackers.add(tracker) + tracker.freq = circle.freq + tracker.circle = circle + + center_trackers.submobjects.sort( + key=lambda m: m.freq + ) + center_trackers.generate_target() + right_buff = 1.45 + center_trackers.target.arrange(RIGHT, buff=right_buff) + center_trackers.target.to_edge(UP, buff=1.25) + + self.add(top_circles) + self.play( + MoveToTarget(center_trackers), + run_time=2 + ) + self.wait(4) + + self.top_circles = top_circles + self.center_trackers = center_trackers + + def show_frequencies(self): + center_trackers = self.center_trackers + + freq_numbers = VGroup() + for ct in center_trackers: + number = Integer(ct.freq) + number.next_to(ct, DOWN, buff=1) + freq_numbers.add(number) + ct.circle.number = number + + ld, rd = [ + TexMobject("\\dots") + for x in range(2) + ] + ld.next_to(freq_numbers, LEFT, MED_LARGE_BUFF) + rd.next_to(freq_numbers, RIGHT, MED_LARGE_BUFF) + freq_numbers.add_to_back(ld) + freq_numbers.add(rd) + + freq_word = TextMobject("Frequencies") + freq_word.scale(1.5) + freq_word.set_color(YELLOW) + freq_word.next_to(freq_numbers, DOWN, MED_LARGE_BUFF) + + self.play( + LaggedStartMap( + FadeInFromDown, freq_numbers + ) + ) + self.play( + Write(freq_word), + LaggedStartMap( + ShowCreationThenFadeAround, freq_numbers, + ) + ) + self.wait(2) + + self.freq_numbers = freq_numbers + self.freq_word = freq_word + + def show_examples_for_frequencies(self): + top_circles = self.top_circles + c1, c2, c3 = [ + list(filter( + lambda c: c.freq == k, + top_circles + ))[0] + for k in (1, 2, 3) + ] + + neg_circles = VGroup(*filter( + lambda c: c.freq < 0, + top_circles + )) + + for c in [c1, c2, c3, *neg_circles]: + c.rect = SurroundingRectangle(c) + + self.play( + ShowCreation(c2.rect), + WiggleOutThenIn(c2.number), + ) + self.wait(2) + self.play( + ReplacementTransform(c2.rect, c1.rect), + ) + self.play(FadeOut(c1.rect)) + self.wait() + self.play( + ShowCreation(c3.rect), + WiggleOutThenIn(c3.number), + ) + self.play( + FadeOut(c3.rect), + ) + self.wait(2) + self.play( + LaggedStart(*[ + ShowCreationThenFadeOut(c.rect) + for c in neg_circles + ]) + ) + self.wait(3) + self.play(FadeOut(self.freq_word)) + + def show_as_vectors(self): + top_circles = self.top_circles + top_vectors = self.get_rotating_vectors(top_circles) + top_vectors.set_color(WHITE) + + original_circles = top_circles.copy() + self.play( + FadeIn(top_vectors), + top_circles.set_opacity, 0, + ) + self.wait(3) + self.play( + top_circles.match_style, original_circles + ) + self.remove(top_vectors) + + self.top_vectors = top_vectors + + def show_vector_sum(self): + trackers = self.center_trackers.copy() + trackers.sort( + submob_func=lambda t: abs(t.circle.freq - 0.1) + ) + plane = self.plane = NumberPlane( + x_min=-3, + x_max=3, + y_min=-2, + y_max=2, + axis_config={ + "stroke_color": LIGHT_GREY, + } + ) + plane.set_stroke(width=1) + plane.fade(0.5) + plane.move_to(self.center_point) + + self.play( + FadeOut(self.drawn_path), + FadeOut(self.circles), + self.slow_factor_tracker.set_value, 0.05, + ) + self.add(plane, self.path) + self.play(FadeIn(plane)) + + new_circles = VGroup() + last_tracker = None + for tracker in trackers: + if last_tracker: + tracker.new_location_func = last_tracker.circle.get_start + else: + tracker.new_location_func = lambda: self.center_point + + original_circle = tracker.circle + tracker.circle = original_circle.copy() + tracker.circle.center_func = tracker.get_location + new_circles.add(tracker.circle) + + self.add(tracker, tracker.circle) + start_point = tracker.get_location() + self.play( + UpdateFromAlphaFunc( + tracker, lambda t, a: t.move_to( + interpolate( + start_point, + tracker.new_location_func(), + a, + ) + ), + run_time=2 + ) + ) + tracker.add_updater(lambda t: t.move_to( + t.new_location_func() + )) + self.wait(2) + last_tracker = tracker + + self.wait(3) + + self.clear() + self.slow_factor_tracker.set_value(0.1) + self.add( + self.top_circles, + self.freq_numbers, + self.path, + ) + self.add_circles() + for tc in self.top_circles: + for c in self.circles: + if c.freq == tc.freq: + tc.rotate( + angle_of_vector(c.get_start() - c.get_center()) - + angle_of_vector(tc.get_start() - tc.get_center()) + ) + self.wait(10) + + def tweak_starting_vectors(self): + top_circles = self.top_circles + circles = self.circles + path = self.path + drawn_path = self.drawn_path + + new_path = self.get_new_path() + new_coefs = self.get_coefficients_of_path(new_path) + new_circles = self.get_circles(coefficients=new_coefs) + + new_top_circles = VGroup() + new_top_vectors = VGroup() + for top_circle in top_circles: + for circle in new_circles: + if circle.freq == top_circle.freq: + new_top_circle = circle.copy() + new_top_circle.center_func = top_circle.get_center + new_top_vector = self.get_rotating_vector( + new_top_circle + ) + new_top_circles.add(new_top_circle) + new_top_vectors.add(new_top_vector) + + self.play( + self.slow_factor_tracker.set_value, 0, + FadeOut(drawn_path) + ) + self.wait() + self.play( + ReplacementTransform(top_circles, new_top_circles), + ReplacementTransform(circles, new_circles), + FadeOut(path), + run_time=3, + ) + new_drawn_path = self.get_drawn_path( + new_circles, stroke_width=4, + ) + self.add(new_drawn_path) + self.slow_factor_tracker.set_value(0.1) + self.wait(20) + + # + def configure_path(self, path): + path.set_stroke(WHITE, 1) + path.set_fill(BLACK, opacity=1) + path.set_height(self.path_height) + path.move_to(self.center_point) + return path + + def get_path(self): + tex = TexMobject("f") + path = tex.family_members_with_points()[0] + self.configure_path(path) + return path + # return Square().set_height(3) + + def get_new_path(self): + shape = SVGMobject("TrebleClef") + path = shape.family_members_with_points()[0] + self.configure_path(path) + path.scale(1.5, about_edge=DOWN) + return path diff --git a/active_projects/diffyq/part2/heat_equation.py b/active_projects/diffyq/part2/heat_equation.py new file mode 100644 index 00000000..f50c1f41 --- /dev/null +++ b/active_projects/diffyq/part2/heat_equation.py @@ -0,0 +1,2969 @@ +from manimlib.imports import * +from active_projects.ode.part2.shared_constructs import * + + +class TwoDBodyWithManyTemperatures(ThreeDScene): + CONFIG = { + "cells_per_side": 20, + "body_height": 6, + } + + def construct(self): + self.introduce_body() + self.show_temperature_at_all_points() + + def introduce_body(self): + height = self.body_height + buff = 0.025 + rows = VGroup(*[ + VGroup(*[ + Dot( + # stroke_width=0.5, + stroke_width=0, + fill_opacity=1, + ) + for x in range(self.cells_per_side) + ]).arrange(RIGHT, buff=buff) + for y in range(self.cells_per_side) + ]).arrange(DOWN, buff=buff) + for row in rows[1::2]: + row.submobjects.reverse() + + body = self.body = VGroup(*it.chain(*rows)) + body.set_height(height) + body.center() + body.to_edge(LEFT) + + axes = self.axes = Axes( + x_min=-5, x_max=5, + y_min=-5, y_max=5, + ) + axes.match_height(body) + axes.move_to(body) + + for cell in body: + self.color_cell(cell) + # body.set_stroke(WHITE, 0.5) # Do this? + + plate = Square( + stroke_width=0, + fill_color=DARK_GREY, + sheen_direction=UL, + sheen_factor=1, + fill_opacity=1, + ) + plate.replace(body) + + plate_words = TextMobject("Piece of \\\\ metal") + plate_words.scale(2) + plate_words.set_stroke(BLACK, 2, background=True) + plate_words.set_color(BLACK) + plate_words.move_to(plate) + + self.play( + DrawBorderThenFill(plate), + Write( + plate_words, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ) + ) + self.wait() + + self.remove(plate_words) + + def show_temperature_at_all_points(self): + body = self.body + start_corner = body[0].get_center() + + dot = Dot(radius=0.01, color=WHITE) + dot.move_to(start_corner) + + get_point = dot.get_center + + lhs = TexMobject("T = ") + lhs.next_to(body, RIGHT, LARGE_BUFF) + + decimal = DecimalNumber( + num_decimal_places=1, + unit="^\\circ" + ) + decimal.next_to(lhs, RIGHT, MED_SMALL_BUFF, DOWN) + decimal.add_updater( + lambda d: d.set_value( + 40 + 50 * self.point_to_temp(get_point()) + ) + ) + + arrow = Arrow(color=YELLOW) + arrow.set_stroke(BLACK, 8, background=True) + arrow.tip.set_stroke(BLACK, 2, background=True) + # arrow.add_to_back(arrow.copy().set_stroke(BLACK, 5)) + arrow.add_updater(lambda a: a.put_start_and_end_on( + lhs.get_left() + MED_SMALL_BUFF * LEFT, + get_point(), + )) + + dot.add_updater(lambda p: p.move_to( + body[-1] if (1 < len(body)) else start_corner + )) + self.add(body, dot, lhs, decimal, arrow) + self.play( + ShowIncreasingSubsets( + body, + run_time=10, + rate_func=linear, + ) + ) + self.wait() + self.remove(dot) + self.play( + FadeOut(arrow), + FadeOut(lhs), + FadeOut(decimal), + ) + + # + def point_to_temp(self, point, time=0): + x, y = self.axes.point_to_coords(point) + return two_d_temp_func( + 0.3 * x, 0.3 * y, t=time + ) + + def color_cell(self, cell, vect=RIGHT): + p0 = cell.get_corner(-vect) + p1 = cell.get_corner(vect) + colors = [] + for point in p0, p1: + temp = self.point_to_temp(point) + color = temperature_to_color(temp) + colors.append(color) + cell.set_color(color=colors) + cell.set_sheen_direction(vect) + return cell + + +class TwoDBodyWithManyTemperaturesGraph(ExternallyAnimatedScene): + pass + + +class TwoDBodyWithManyTemperaturesContour(ExternallyAnimatedScene): + pass + + +class BringTwoRodsTogether(Scene): + CONFIG = { + "step_size": 0.05, + "axes_config": { + "x_min": -1, + "x_max": 11, + "y_min": -10, + "y_max": 100, + "y_axis_config": { + "unit_size": 0.06, + "tick_frequency": 10, + }, + }, + "graph_x_min": 0, + "graph_x_max": 10, + "wait_time": 30, + "default_n_rod_pieces": 20, + "alpha": 1.0, + } + + def construct(self): + self.setup_axes() + self.setup_graph() + self.setup_clock() + + self.show_rods() + self.show_equilibration() + + def setup_axes(self): + axes = Axes(**self.axes_config) + axes.center().to_edge(UP) + + y_label = axes.get_y_axis_label("\\text{Temperature}") + y_label.to_edge(UP) + axes.y_axis.label = y_label + axes.y_axis.add(y_label) + axes.y_axis.add_numbers( + *range(20, 100, 20) + ) + + self.axes = axes + self.y_label = y_label + + def setup_graph(self): + graph = self.axes.get_graph( + self.initial_function, + x_min=self.graph_x_min, + x_max=self.graph_x_max, + step_size=self.step_size, + discontinuities=[5], + ) + graph.color_using_background_image("VerticalTempGradient") + + self.graph = graph + + def setup_clock(self): + clock = Clock() + clock.set_height(1) + clock.to_corner(UR) + clock.shift(MED_LARGE_BUFF * LEFT) + + time_lhs = TextMobject("Time: ") + time_label = DecimalNumber( + 0, num_decimal_places=2, + ) + time_rhs = TextMobject("s") + time_group = VGroup( + time_lhs, + time_label, + # time_rhs + ) + time_group.arrange(RIGHT, aligned_edge=DOWN) + time_rhs.shift(SMALL_BUFF * LEFT) + time_group.next_to(clock, DOWN) + + self.time_group = time_group + self.time_label = time_label + self.clock = clock + + def show_rods(self): + rod1, rod2 = rods = VGroup( + self.get_rod(0, 5), + self.get_rod(5, 10), + ) + rod1.set_color(rod1[0].get_color()) + rod2.set_color(rod2[-1].get_color()) + + rods.save_state() + rods.space_out_submobjects(1.5) + rods.center() + + labels = VGroup( + TexMobject("90^\\circ"), + TexMobject("10^\\circ"), + ) + for rod, label in zip(rods, labels): + label.next_to(rod, DOWN) + rod.label = label + + self.play( + FadeInFrom(rod1, UP), + Write(rod1.label), + ) + self.play( + FadeInFrom(rod2, DOWN), + Write(rod2.label) + ) + self.wait() + + self.rods = rods + self.rod_labels = labels + + def show_equilibration(self): + rods = self.rods + axes = self.axes + graph = self.graph + labels = self.rod_labels + self.play( + Write(axes), + rods.restore, + rods.space_out_submobjects, 1.1, + FadeIn(self.time_group), + FadeIn(self.clock), + *[ + MaintainPositionRelativeTo( + rod.label, rod + ) + for rod in rods + ], + ) + + br1 = Rectangle(height=0.2, width=1) + br1.set_stroke(width=0) + br1.set_fill(BLACK, opacity=1) + br2 = br1.copy() + br1.add_updater(lambda b: b.move_to(axes.c2p(0, 90))) + br1.add_updater( + lambda b: b.align_to(rods[0].get_right(), LEFT) + ) + br2.add_updater(lambda b: b.move_to(axes.c2p(0, 10))) + br2.add_updater( + lambda b: b.align_to(rods[1].get_left(), RIGHT) + ) + + self.add(graph, br1, br2) + self.play( + ShowCreation(graph), + labels[0].align_to, axes.c2p(0, 87), UP, + labels[1].align_to, axes.c2p(0, 13), DOWN, + ) + self.play() + self.play( + rods.restore, + rate_func=rush_into, + ) + self.remove(br1, br2) + + graph.add_updater(self.update_graph) + self.time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + rods.add_updater(self.update_rods) + + self.play( + ClockPassesTime( + self.clock, + run_time=self.wait_time, + hours_passed=self.wait_time, + ), + FadeOut(labels) + ) + + # + def initial_function(self, x): + if x <= 5: + return 90 + else: + return 10 + + def update_graph(self, graph, dt, alpha=None, n_mini_steps=500): + if alpha is None: + alpha = self.alpha + points = np.append( + graph.get_start_anchors(), + [graph.get_last_point()], + axis=0, + ) + for k in range(n_mini_steps): + y_change = np.zeros(points.shape[0]) + dx = points[1][0] - points[0][0] + for i in range(len(points)): + p = points[i] + lp = points[max(i - 1, 0)] + rp = points[min(i + 1, len(points) - 1)] + d2y = (rp[1] - 2 * p[1] + lp[1]) + + if (0 < i < len(points) - 1): + second_deriv = d2y / (dx**2) + else: + second_deriv = 2 * d2y / (dx**2) + # second_deriv = 0 + + y_change[i] = alpha * second_deriv * dt / n_mini_steps + + # y_change[0] = y_change[1] + # y_change[-1] = y_change[-2] + # y_change[0] = 0 + # y_change[-1] = 0 + # y_change -= np.mean(y_change) + points[:, 1] += y_change + graph.set_points_smoothly(points) + return graph + + def get_second_derivative(self, x, dx=0.001): + graph = self.graph + x_min = self.graph_x_min + x_max = self.graph_x_max + + ly, y, ry = [ + graph.point_from_proportion( + inverse_interpolate(x_min, x_max, alt_x) + )[1] + for alt_x in (x - dx, x, x + dx) + ] + + # At the boundary, don't return the second deriv, + # but instead something matching the Neumann + # boundary condition. + if x == x_max: + return (ly - y) / dx + elif x == x_min: + return (ry - y) / dx + else: + d2y = ry - 2 * y + ly + return d2y / (dx**2) + + def get_rod(self, x_min, x_max, n_pieces=None): + if n_pieces is None: + n_pieces = self.default_n_rod_pieces + axes = self.axes + line = Line(axes.c2p(x_min, 0), axes.c2p(x_max, 0)) + rod = VGroup(*[ + Square() + for n in range(n_pieces) + ]) + rod.arrange(RIGHT, buff=0) + rod.match_width(line) + rod.set_height(0.2, stretch=True) + rod.move_to(axes.c2p(x_min, 0), LEFT) + rod.set_fill(opacity=1) + rod.set_stroke(width=1) + rod.set_sheen_direction(RIGHT) + self.color_rod_by_graph(rod) + return rod + + def update_rods(self, rods): + for rod in rods: + self.color_rod_by_graph(rod) + + def color_rod_by_graph(self, rod): + for piece in rod: + piece.set_color(color=[ + self.rod_point_to_color(piece.get_left()), + self.rod_point_to_color(piece.get_right()), + ]) + + def rod_point_to_graph_y(self, point): + axes = self.axes + x = axes.x_axis.p2n(point) + + graph = self.graph + alpha = inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + x, + ) + return axes.y_axis.p2n( + graph.point_from_proportion(alpha) + ) + + def y_to_color(self, y): + return temperature_to_color((y - 45) / 45) + + def rod_point_to_color(self, point): + return self.y_to_color( + self.rod_point_to_graph_y(point) + ) + + +class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether): + CONFIG = { + "alpha": 0.1, + "arrow_xs": np.linspace(0, 10, 22)[1:-1], + "arrow_scale_factor": 0.5, + "max_magnitude": 1.5, + "wait_time": 30, + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + (5, 0.3), + (7, 0.2), + (21, 0.1), + (41, 0.05), + ], + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + self.add_arrows() + self.initialize_updaters() + self.let_play() + + def add_axes(self): + self.setup_axes() + self.add(self.axes) + + def add_graph(self): + self.setup_graph() + self.add(self.graph) + + def add_clock(self): + self.setup_clock() + self.add(self.clock) + self.add(self.time_label) + self.time_label.next_to(self.clock, DOWN) + + def add_rod(self): + rod = self.rod = self.get_rod( + self.graph_x_min, + self.graph_x_max, + ) + self.add(rod) + + def add_arrows(self): + graph = self.graph + x_min = self.graph_x_min + x_max = self.graph_x_max + + xs = self.arrow_xs + arrows = VGroup(*[Vector(DOWN) for x in xs]) + asf = self.arrow_scale_factor + + def update_arrows(arrows): + for x, arrow in zip(xs, arrows): + d2y_dx2 = self.get_second_derivative(x) + mag = asf * np.sign(d2y_dx2) * abs(d2y_dx2) + mag = np.clip( + mag, + -self.max_magnitude, + self.max_magnitude, + ) + arrow.put_start_and_end_on( + ORIGIN, mag * UP + ) + point = graph.point_from_proportion( + inverse_interpolate(x_min, x_max, x) + ) + arrow.shift(point - arrow.get_start()) + arrow.set_color( + self.rod_point_to_color(point) + ) + + arrows.add_updater(update_arrows) + + self.add(arrows) + self.arrows = arrows + + def initialize_updaters(self): + if hasattr(self, "graph"): + self.graph.add_updater(self.update_graph) + if hasattr(self, "rod"): + self.rod.add_updater(self.color_rod_by_graph) + if hasattr(self, "time_label"): + self.time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + + def let_play(self): + self.run_clock(self.wait_time) + + def run_clock(self, time): + self.play( + ClockPassesTime( + self.clock, + run_time=time, + hours_passed=time, + ), + ) + + # + def temp_func(self, x, t): + new_x = TAU * x / 10 + return 50 + 20 * np.sum([ + amp * np.sin(freq * new_x) * + np.exp(-(self.alpha * freq**2) * t) + for freq, amp in self.freq_amplitude_pairs + ]) + + def initial_function(self, x, time=0): + return self.temp_func(x, 0) + + +class TalkThrough1DHeatGraph(ShowEvolvingTempGraphWithArrows, SpecialThreeDScene): + CONFIG = { + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + (5, 0.3), + (7, 0.2), + ], + "surface_resolution": 20, + "graph_slice_step": 10 / 20, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_rod() + + self.emphasize_graph() + self.emphasize_rod() + self.show_x_axis() + self.show_changes_over_time() + self.show_surface() + + def add_graph(self): + self.graph = self.get_graph() + self.add(self.graph) + + def emphasize_graph(self): + graph = self.graph + q_marks = VGroup(*[ + TexMobject("?").move_to( + graph.point_from_proportion(a), + UP, + ).set_stroke(BLACK, 3, background=True) + for a in np.linspace(0, 1, 20) + ]) + + self.play(LaggedStart(*[ + Succession( + FadeInFromLarge(q_mark), + FadeOutAndShift(q_mark, DOWN), + ) + for q_mark in q_marks + ])) + self.wait() + + def emphasize_rod(self): + alt_rod = self.get_rod(0, 10, 50) + word = TextMobject("Rod") + word.scale(2) + word.next_to(alt_rod, UP, MED_SMALL_BUFF) + + self.play( + LaggedStart( + *[ + Rotating(piece, rate_func=smooth) + for piece in alt_rod + ], + run_time=2, + lag_ratio=0.01, + ), + Write(word) + ) + self.remove(*alt_rod) + self.wait() + + self.rod_word = word + + def show_x_axis(self): + rod = self.rod + axes = self.axes + graph = self.graph + x_axis = axes.x_axis + x_numbers = x_axis.get_number_mobjects(*range(1, 11)) + x_axis_label = TexMobject("x") + x_axis_label.next_to(x_axis.get_right(), UP) + + self.play( + rod.set_opacity, 0.5, + FadeInFrom(x_axis_label, UL), + LaggedStartMap( + FadeInFrom, x_numbers, + lambda m: (m, UP), + ) + ) + self.wait() + + # Show x-values + triangle = ArrowTip( + start_angle=-90 * DEGREES, + color=LIGHT_GREY, + ) + x_tracker = ValueTracker(PI) + get_x = x_tracker.get_value + + def get_x_point(): + return x_axis.n2p(get_x()) + + def get_graph_point(): + return graph.point_from_proportion( + inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + get_x(), + ) + ) + + triangle.add_updater( + lambda t: t.next_to(get_x_point(), UP) + ) + x_label = VGroup( + TexMobject("x"), + TexMobject("="), + DecimalNumber( + 0, + num_decimal_places=3, + include_background_rectangle=True, + ).scale(0.9) + ) + x_label.set_stroke(BLACK, 5, background=True) + x_label.add_updater(lambda m: m[-1].set_value(get_x())) + x_label.add_updater(lambda m: m.arrange(RIGHT, buff=SMALL_BUFF)) + x_label.add_updater(lambda m: m[-1].align_to(m[0], DOWN)) + x_label.add_updater(lambda m: m.next_to(triangle, UP, SMALL_BUFF)) + x_label.add_updater(lambda m: m.shift(SMALL_BUFF * RIGHT)) + rod_piece = always_redraw( + lambda: self.get_rod( + get_x() - 0.05, get_x() + 0.05, + n_pieces=1, + ) + ) + + self.play( + FadeInFrom(triangle, UP), + FadeIn(x_label), + FadeIn(rod_piece), + FadeOut(self.rod_word), + ) + for value in [np.exp(2), (np.sqrt(5) + 1) / 2]: + self.play(x_tracker.set_value, value, run_time=2) + self.wait() + + # Show graph + v_line = always_redraw( + lambda: DashedLine( + get_x_point(), + get_graph_point(), + color=self.rod_point_to_color(get_x_point()), + ) + ) + graph_dot = Dot() + graph_dot.add_updater( + lambda m: m.set_color( + self.rod_point_to_color(m.get_center()) + ) + ) + graph_dot.add_updater( + lambda m: m.move_to(get_graph_point()) + ) + t_label = TexMobject("T(", "x", ")") + t_label.set_stroke(BLACK, 3, background=True) + t_label.add_updater( + lambda m: m.next_to(graph_dot, UR, buff=0) + ) + + self.add(v_line, rod_piece, x_label, triangle) + self.play( + TransformFromCopy(x_label[0], t_label[1]), + FadeIn(t_label[0::2]), + ShowCreation(v_line), + GrowFromPoint(graph_dot, get_x_point()), + ) + self.add(t_label) + self.wait() + self.play( + x_tracker.set_value, TAU, + run_time=5, + ) + + self.x_tracker = x_tracker + self.graph_label_group = VGroup( + v_line, rod_piece, triangle, x_label, + graph_dot, t_label, + ) + self.set_variables_as_attrs(*self.graph_label_group) + self.set_variables_as_attrs(x_numbers, x_axis_label) + + def show_changes_over_time(self): + graph = self.graph + t_label = self.t_label + new_t_label = TexMobject("T(", "x", ",", "t", ")") + new_t_label.set_color_by_tex("t", YELLOW) + new_t_label.match_updaters(t_label) + + self.setup_clock() + clock = self.clock + time_label = self.time_label + time_group = self.time_group + + time = 5 + self.play( + FadeIn(clock), + FadeIn(time_group), + ) + self.play( + self.get_graph_time_change_animation( + graph, time + ), + ClockPassesTime(clock), + ChangeDecimalToValue( + time_label, time, + rate_func=linear, + ), + ReplacementTransform( + t_label, + new_t_label, + rate_func=squish_rate_func(smooth, 0.5, 0.7), + ), + run_time=time + ) + self.play( + ShowCreationThenFadeAround( + new_t_label.get_part_by_tex("t") + ), + ) + self.wait() + self.play( + FadeOut(clock), + ChangeDecimalToValue(time_label, 0), + VFadeOut(time_group), + self.get_graph_time_change_animation( + graph, + new_time=0, + ), + run_time=1, + rate_func=smooth, + ) + + self.graph_label_group.remove(t_label) + self.graph_label_group.add(new_t_label) + + def show_surface(self): + axes = self.axes + graph = self.graph + t_min = 0 + t_max = 10 + + axes_copy = axes.deepcopy() + self.original_axes = self.axes + + # Set rod final state + final_graph = self.get_graph(t_max) + curr_graph = self.graph + self.graph = final_graph + final_rod = self.rod.copy() + final_rod.set_opacity(1) + self.color_rod_by_graph(final_rod) + self.graph = curr_graph + + # Time axis + t_axis = NumberLine( + x_min=t_min, + x_max=t_max, + ) + origin = axes.c2p(0, 0) + t_axis.shift(origin - t_axis.n2p(0)) + t_axis.add_numbers( + *range(1, t_max + 1), + direction=UP, + ) + time_label = TextMobject("Time") + time_label.scale(1.5) + time_label.next_to(t_axis, UP) + t_axis.time_label = time_label + t_axis.add(time_label) + # t_axis.rotate(90 * DEGREES, LEFT, about_point=origin) + t_axis.rotate(90 * DEGREES, UP, about_point=origin) + + # New parts of graph + step = self.graph_slice_step + graph_slices = VGroup(*[ + self.get_graph(time=t).shift( + t * IN + ) + for t in np.arange(0, t_max + step, step) + ]) + graph_slices.set_stroke(width=1) + graph_slices.set_shade_in_3d(True) + + # Input plane + x_axis = self.axes.x_axis + y = axes.c2p(0, 0)[1] + surface_config = { + "u_min": self.graph_x_min, + "u_max": self.graph_x_max, + "v_min": t_min, + "v_max": t_max, + "resolution": self.surface_resolution, + } + input_plane = ParametricSurface( + lambda x, t: np.array([ + x_axis.n2p(x)[0], + y, + t_axis.n2p(t)[2], + ]), + **surface_config, + ) + input_plane.set_style( + fill_opacity=0.5, + fill_color=BLUE_B, + stroke_width=0.5, + stroke_color=WHITE, + ) + + # Surface + y_axis = axes.y_axis + surface = ParametricSurface( + lambda x, t: np.array([ + x_axis.n2p(x)[0], + y_axis.n2p(self.temp_func(x, t))[1], + t_axis.n2p(t)[2], + ]), + **surface_config, + ) + surface.set_style( + fill_opacity=0.1, + fill_color=LIGHT_GREY, + stroke_width=0.5, + stroke_color=WHITE, + stroke_opacity=0.5, + ) + + # Rotate everything on screen and move camera + # in such a way that it looks the same + curr_group = Group(*self.get_mobjects()) + curr_group.clear_updaters() + self.set_camera_orientation( + phi=90 * DEGREES, + ) + mobs = [ + curr_group, + graph_slices, + t_axis, + input_plane, + surface, + ] + for mob in mobs: + self.orient_mobject_for_3d(mob) + + # Clean current mobjects + self.x_label.set_stroke(BLACK, 2, background=True) + self.x_label[-1][0].fade(1) + + self.move_camera( + phi=80 * DEGREES, + theta=-85 * DEGREES, + added_anims=[ + Write(input_plane), + Write(t_axis), + FadeOut(self.graph_label_group), + self.rod.set_opacity, 1, + ] + ) + self.begin_ambient_camera_rotation() + self.add(*graph_slices, *self.get_mobjects()) + self.play( + FadeIn(surface), + LaggedStart(*[ + TransformFromCopy(graph, graph_slice) + for graph_slice in graph_slices + ], lag_ratio=0.02) + ) + self.wait(4) + + # Show slices + self.axes = axes_copy # So get_graph works... + slicing_plane = Rectangle( + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.2, + ) + slicing_plane.set_shade_in_3d(True) + slicing_plane.replace( + Line(axes_copy.c2p(0, 0), axes_copy.c2p(10, 100)), + stretch=True + ) + self.orient_mobject_for_3d(slicing_plane) + + def get_time_slice(t): + new_slice = self.get_graph(t) + new_slice.set_shade_in_3d(True) + self.orient_mobject_for_3d(new_slice) + new_slice.shift(t * UP) + return new_slice + + graph.set_shade_in_3d(True) + t_tracker = ValueTracker(0) + graph.add_updater(lambda g: g.become( + get_time_slice(t_tracker.get_value()) + )) + + self.orient_mobject_for_3d(final_rod) + final_rod.shift(10 * UP) + kw = {"run_time": 10, "rate_func": linear} + self.rod.save_state() + self.play( + ApplyMethod(t_tracker.set_value, 10, **kw), + Transform(self.rod, final_rod, **kw), + ApplyMethod(slicing_plane.shift, 10 * UP, **kw), + ) + self.wait() + + self.set_variables_as_attrs( + t_axis, + input_plane, + surface, + graph_slices, + slicing_plane, + t_tracker, + ) + + # + def get_graph(self, time=0): + graph = self.axes.get_graph( + lambda x: self.temp_func(x, time), + x_min=self.graph_x_min, + x_max=self.graph_x_max, + step_size=self.step_size, + ) + graph.time = time + graph.color_using_background_image("VerticalTempGradient") + return graph + + def get_graph_time_change_animation(self, graph, new_time, **kwargs): + old_time = graph.time + graph.time = new_time + config = { + "run_time": abs(new_time - old_time), + "rate_func": linear, + } + config.update(kwargs) + + return UpdateFromAlphaFunc( + graph, + lambda g, a: g.become( + self.get_graph(interpolate( + old_time, new_time, a + )) + ), + **config + ) + + def orient_mobject_for_3d(self, mob): + mob.rotate( + 90 * DEGREES, + axis=RIGHT, + about_point=ORIGIN + ) + return mob + + +class ContrastXChangesToTChanges(TalkThrough1DHeatGraph): + CONFIG = { + # "surface_resolution": 5, + # "graph_slice_step": 1, + } + + def construct(self): + self.catchup_with_last_scene() + self.emphasize_dimensions_of_input_space() + self.reset_time_to_zero() + + self.show_changes_with_x() + self.show_changes_with_t() + + def catchup_with_last_scene(self): + self.force_skipping() + + self.add_axes() + self.add_graph() + self.add_rod() + + self.rod_word = Point() + self.show_x_axis() + self.show_surface() + + self.revert_to_original_skipping_status() + + def emphasize_dimensions_of_input_space(self): + plane = self.input_plane + plane_copy = plane.copy() + plane_copy.set_color(BLUE_E) + plane_copy.shift(SMALL_BUFF * 0.5 * OUT) + + plane_copy1 = plane_copy.copy() + plane_copy1.stretch(0.01, 1, about_edge=DOWN) + plane_copy0 = plane_copy1.copy() + plane_copy0.stretch(0, 0, about_edge=LEFT) + + words = TextMobject("2d input\\\\space") + words.scale(2) + words.move_to(plane.get_center() + SMALL_BUFF * OUT) + + self.play( + Write(words), + self.camera.phi_tracker.set_value, 60 * DEGREES, + self.camera.theta_tracker.set_value, -90 * DEGREES, + run_time=1 + ) + self.play( + ReplacementTransform(plane_copy0, plane_copy1) + ) + self.play( + ReplacementTransform(plane_copy1, plane_copy) + ) + self.wait(2) + self.play(FadeOut(plane_copy)) + + self.input_plane_words = words + + def reset_time_to_zero(self): + self.play( + self.t_tracker.set_value, 0, + VFadeOut(self.slicing_plane), + Restore(self.rod), + ) + + def show_changes_with_x(self): + alpha_tracker = ValueTracker(0) + line = always_redraw( + lambda: self.get_tangent_line( + self.graph, alpha_tracker.get_value(), + ) + ) + + self.stop_ambient_camera_rotation() + self.play( + ShowCreation(line), + FadeOut(self.input_plane_words), + self.camera.phi_tracker.set_value, 80 * DEGREES, + self.camera.theta_tracker.set_value, -90 * DEGREES, + ) + self.play( + alpha_tracker.set_value, 0.425, + run_time=5, + rate_func=bezier([0, 0, 1, 1]), + ) + + # Show dx and dT + p0 = line.point_from_proportion(0.3) + p2 = line.point_from_proportion(0.7) + p1 = np.array([p2[0], *p0[1:]]) + dx_line = DashedLine(p0, p1) + dT_line = DashedLine(p1, p2) + dx = TexMobject("dx") + dT = TexMobject("dT") + VGroup(dx, dT).scale(0.7) + VGroup(dx, dT).rotate(90 * DEGREES, RIGHT) + dx.next_to(dx_line, IN, SMALL_BUFF) + dT.next_to(dT_line, RIGHT, SMALL_BUFF) + + self.play( + ShowCreation(dx_line), + FadeInFrom(dx, LEFT) + ) + self.wait() + self.play( + ShowCreation(dT_line), + FadeInFrom(dT, IN) + ) + self.wait() + self.play(*map(FadeOut, [ + line, dx_line, dT_line, dx, dT, + ])) + + def show_changes_with_t(self): + slices = self.graph_slices + slice_alpha = 0.075 + graph = VMobject() + graph.set_points_smoothly([ + gs.point_from_proportion(slice_alpha) + for gs in slices + ]) + graph.color_using_background_image("VerticalTempGradient") + graph.set_shade_in_3d(True) + + alpha_tracker = ValueTracker(0) + line = always_redraw( + lambda: self.get_tangent_line( + graph, alpha_tracker.get_value(), + ) + ) + + plane = Square() + plane.set_stroke(width=0) + plane.set_fill(WHITE, 0.1) + plane.set_shade_in_3d(True) + plane.rotate(90 * DEGREES, RIGHT) + plane.rotate(90 * DEGREES, OUT) + plane.set_height(10) + plane.set_depth(8, stretch=True) + plane.move_to(self.t_axis.n2p(0), IN + DOWN) + plane.shift(RIGHT) + + self.play( + self.camera.theta_tracker.set_value, -20 * DEGREES, + self.camera.frame_center.shift, 4 * LEFT, + ) + + self.play( + ShowCreation( + graph.copy(), + remover=True + ), + FadeInFrom(plane, 6 * DOWN, run_time=2), + VFadeIn(line), + ApplyMethod( + alpha_tracker.set_value, 1, + run_time=8, + ), + ) + self.add(graph) + + self.begin_ambient_camera_rotation(-0.02) + self.camera.frame_center.add_updater( + lambda m, dt: m.shift(0.05 * dt * RIGHT) + ) + + self.play( + FadeOut(line), + FadeOut(plane), + ) + self.wait(30) # Let rotate + + self.t_graph = graph + + # + def get_tangent_line(self, graph, alpha, d_alpha=0.001, length=2): + if alpha < 1 - d_alpha: + a1 = alpha + a2 = alpha + d_alpha + else: + a1 = alpha - d_alpha + a2 = alpha + + p1 = graph.point_from_proportion(a1) + p2 = graph.point_from_proportion(a2) + line = Line(p1, p2, color=WHITE) + line.scale( + length / line.get_length() + ) + line.move_to(p1) + return line + + +class TransitionToTempVsTime(ContrastXChangesToTChanges): + CONFIG = { + # "surface_resolution": 5, + # "graph_slice_step": 1, + } + + def construct(self): + self.catchup_with_last_scene() + + axes = self.original_axes + t_axis = self.t_axis + y_axis = axes.y_axis + x_axis = axes.x_axis + + for mob in self.get_mobjects(): + mob.clear_updaters() + self.stop_ambient_camera_rotation() + self.move_camera( + phi=90 * DEGREES, + theta=0 * DEGREES, + added_anims=[ + Rotate( + y_axis, 90 * DEGREES, + axis=OUT, + about_point=y_axis.n2p(0), + ), + FadeOut(VGroup( + self.graph_slices, + self.surface, + self.slicing_plane, + self.rod, + self.graph, + self.x_numbers, + self.x_axis_label, + self.t_graph, + )), + self.camera.frame_center.move_to, 5 * LEFT, + ] + ) + self.play( + VGroup(x_axis, self.input_plane).stretch, + 0, 0, {"about_point": y_axis.n2p(0)}, + ) + self.play( + t_axis.time_label.scale, 1 / 1.5, + t_axis.time_label.next_to, t_axis, IN, MED_LARGE_BUFF, + t_axis.numbers.shift, 0.7 * IN, + ) + self.wait() + + def catchup_with_last_scene(self): + self.force_skipping() + + self.add_axes() + self.add_graph() + self.add_rod() + + self.rod_word = Point() + self.show_x_axis() + self.show_surface() + + self.emphasize_dimensions_of_input_space() + self.reset_time_to_zero() + + self.show_changes_with_x() + self.show_changes_with_t() + + self.revert_to_original_skipping_status() + + +class ShowDelTermsAsTinyNudges(TransitionToTempVsTime): + CONFIG = { + # "surface_resolution": 5, + # "graph_slice_step": 1, + "tangent_line_length": 4, + } + + def construct(self): + self.catchup_with_last_scene() + self.stop_camera() + self.show_del_t() + self.show_del_x() + + def stop_camera(self): + self.stop_ambient_camera_rotation() + for mob in self.get_mobjects(): + mob.clear_updaters() + + def show_del_x(self): + x_tracker = ValueTracker(3) + dx_tracker = ValueTracker(0.5) + + line_group = self.get_line_group( + self.graph, + x_tracker, + dx_tracker, + corner_index=0, + ) + dx_line, dT_line, tan_line = line_group + + del_x = TexMobject("\\partial x") + del_x.set_color(GREEN) + del_x.line = dx_line + del_x.direction = OUT + del_T = TexMobject("\\partial T") + del_T.line = dT_line + del_T.direction = RIGHT + syms = VGroup(del_T, del_x) + for sym in syms: + sym.add_updater(lambda m: m.set_width( + dx_line.get_length() + )) + sym.rect = SurroundingRectangle(sym) + group = VGroup(sym, sym.rect) + group.rotate(90 * DEGREES, RIGHT) + + for sym in syms: + sym.add_updater(lambda m: m.next_to( + m.line, m.direction, SMALL_BUFF, + )) + sym.rect.move_to(sym) + + self.move_camera( + phi=80 * DEGREES, + theta=-90 * DEGREES, + added_anims=[ + self.camera.frame_center.move_to, ORIGIN, + ], + ) + for sym in reversed(syms): + self.play( + FadeInFrom(sym, -sym.direction), + ShowCreation( + sym.line.copy(), + remover=True + ), + ) + self.add(sym.line) + self.play(ShowCreation(tan_line)) + for sym in syms: + self.play( + ShowCreationThenDestruction(sym.rect) + ) + self.wait() + self.wait() + self.add(line_group) + self.play( + dx_tracker.set_value, 0.01, + run_time=5, + ) + self.play( + FadeOut(syms), + FadeOut(line_group), + ) + + def show_del_t(self): + # Largely copy pasted from above. + # Reconsolidate if any of this will actually + # be used later. + t_tracker = ValueTracker(1) + dt_tracker = ValueTracker(1) + + line_group = self.get_line_group( + self.t_graph, t_tracker, dt_tracker, + corner_index=1, + ) + dt_line, dT_line, tan_line = line_group + + del_t = TexMobject("\\partial t") + del_t.set_color(YELLOW) + del_t.line = dt_line + del_t.direction = OUT + del_T = TexMobject("\\partial T") + del_T.line = dT_line + del_T.direction = UP + syms = VGroup(del_T, del_t) + for sym in syms: + sym.rect = SurroundingRectangle(sym) + group = VGroup(sym, sym.rect) + group.rotate(90 * DEGREES, RIGHT) + group.rotate(90 * DEGREES, OUT) + sym.add_updater(lambda m: m.set_height( + 0.8 * dT_line.get_length() + )) + + del_t.add_updater(lambda m: m.set_height( + min(0.5, m.line.get_length()) + )) + del_T.add_updater(lambda m: m.set_depth( + min(0.5, m.line.get_length()) + )) + for sym in syms: + sym.add_updater(lambda m: m.next_to( + m.line, m.direction, SMALL_BUFF, + )) + sym.rect.move_to(sym) + + self.move_camera( + phi=80 * DEGREES, + theta=-10 * DEGREES, + added_anims=[ + self.camera.frame_center.move_to, 5 * LEFT, + ], + ) + for sym in reversed(syms): + self.play( + FadeInFrom(sym, -sym.direction), + ShowCreation( + sym.line.copy(), + remover=True + ), + ) + self.add(sym.line) + self.play(ShowCreation(tan_line)) + for sym in syms: + self.play( + ShowCreationThenDestruction(sym.rect) + ) + self.wait() + self.wait() + self.add(line_group) + self.play( + dt_tracker.set_value, 0.01, + run_time=5, + ) + self.play( + FadeOut(syms), + FadeOut(line_group), + ) + + # + def get_line_group(self, graph, input_tracker, nudge_tracker, corner_index): + get_x = input_tracker.get_value + get_dx = nudge_tracker.get_value + + def get_graph_point(x): + return graph.point_from_proportion( + inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + x, + ) + ) + + def get_corner(p1, p2): + result = np.array(p1) + result[corner_index] = p2[corner_index] + return result + + line_group = VGroup( + Line(color=WHITE), + Line(color=RED), + Line(color=WHITE, stroke_width=2), + ) + + def update_line_group(group): + dxl, dTl, tl = group + p0 = get_graph_point(get_x()) + p2 = get_graph_point(get_x() + get_dx()) + p1 = get_corner(p0, p2) + + dxl.set_points_as_corners([p0, p1]) + dTl.set_points_as_corners([p1, p2]) + tl.set_points_as_corners([p0, p2]) + tl.scale( + self.tangent_line_length / tl.get_length() + ) + line_group.add_updater(update_line_group) + return line_group + + +class ShowCurvatureToRateOfChangeIntuition(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "freq_amplitude_pairs": [ + (1, 0.7), + (2, 1), + (3, 0.5), + (4, 0.3), + (5, 0.3), + (7, 0.2), + ], + "arrow_xs": [0.7, 3.8, 4.6, 5.4, 6.2, 9.3], + "arrow_scale_factor": 0.2, + "max_magnitude": 1.0, + "wait_time": 20, + } + + def let_play(self): + arrows = self.arrows + curves = VGroup(*[ + self.get_mini_curve( + inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + x, + ) + ) + for x in self.arrow_xs + ]) + curves.set_stroke(WHITE, 5) + + curve_words = VGroup() + for curve, arrow in zip(curves, arrows): + word = TextMobject("curve") + word.scale(0.7) + word.next_to(curve, arrow.get_vector()[1] * DOWN, SMALL_BUFF) + curve_words.add(word) + + self.remove(arrows) + + self.play( + ShowCreation(curves), + LaggedStartMap(FadeIn, curve_words), + self.y_label.set_fill, {"opacity": 0}, + ) + self.wait() + self.add(*arrows, curves) + self.play(LaggedStartMap(GrowArrow, arrows)) + self.wait() + + self.play(FadeOut(VGroup(curves, curve_words))) + self.add(arrows) + super().let_play() + + def get_mini_curve(self, alpha, d_alpha=0.02): + result = VMobject() + result.pointwise_become_partial( + self.graph, + alpha - d_alpha, + alpha + d_alpha, + ) + return result + + +class DiscreteSetup(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "step_size": 1, + "rod_piece_size_ratio": 1 / 3, + "dashed_line_stroke_opacity": 1.0, + "dot_radius": DEFAULT_DOT_RADIUS, + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + (5, 0.3), + (7, 0.2), + (21, 0.1), + # (41, 0.05), + ], + } + + def construct(self): + self.add_axes() + self.add_graph() + self.discretize() + self.let_time_pass() + self.show_nieghbor_rule() + self.focus_on_three_points() + self.show_difference_formula() + self.gut_check_new_interpretation() + self.write_second_difference() + self.emphasize_final_expression() + + def add_axes(self): + super().add_axes() + self.axes.shift(MED_SMALL_BUFF * LEFT) + + def add_graph(self): + points = self.get_points(time=0) + graph = VMobject() + graph.set_points_smoothly(points) + graph.color_using_background_image("VerticalTempGradient") + + self.add(graph) + + self.graph = graph + self.points = points + + def discretize(self): + axes = self.axes + x_axis = axes.x_axis + graph = self.graph + + piecewise_graph = CurvesAsSubmobjects(graph) + dots = self.get_dots() + v_lines = VGroup(*map(self.get_v_line, dots)) + + rod_pieces = VGroup() + for x in self.get_sample_inputs(): + piece = Line(LEFT, RIGHT) + piece.set_width( + self.step_size * self.rod_piece_size_ratio + ) + piece.move_to(axes.c2p(x, 0)) + piece.set_color( + self.rod_point_to_color(piece.get_center()) + ) + rod_pieces.add(piece) + + word = TextMobject("Discrete version") + word.scale(1.5) + word.next_to(x_axis, UP) + word.set_stroke(BLACK, 3, background=True) + + self.remove(graph) + self.play( + ReplacementTransform( + piecewise_graph, dots, + ), + Write(word, run_time=1) + ) + self.add(v_lines, word) + self.play( + x_axis.fade, 0.8, + TransformFromCopy( + x_axis.tick_marks[1:], + rod_pieces, + ), + LaggedStartMap(ShowCreation, v_lines) + ) + self.play(FadeOut(word)) + self.wait() + + self.rod_pieces = rod_pieces + self.dots = dots + self.v_lines = v_lines + + def let_time_pass(self): + dots = self.dots + + t_tracker = ValueTracker(0) + t_tracker.add_updater(lambda m, dt: m.increment_value(dt)) + self.add(t_tracker) + + self.add_clock() + self.time_label.next_to(self.clock, DOWN) + self.time_label.add_updater( + lambda m: m.set_value(t_tracker.get_value()) + ) + dots.add_updater(lambda d: d.become( + self.get_dots(t_tracker.get_value()) + )) + run_time = 5 + self.play( + ClockPassesTime( + self.clock, + run_time=run_time, + hours_passed=run_time, + ), + ) + t_tracker.clear_updaters() + t_tracker.set_value(run_time) + self.wait() + self.play( + t_tracker.set_value, 0, + FadeOut(self.clock), + FadeOut(self.time_label), + ) + self.remove(t_tracker) + dots.clear_updaters() + + def show_nieghbor_rule(self): + dots = self.dots + rod_pieces = self.rod_pieces + index = self.index = 2 + + p1, p2, p3 = rod_pieces[index:index + 3] + d1, d2, d3 = dots[index:index + 3] + point_label = TextMobject("Point") + neighbors_label = TextMobject("Neighbors") + words = VGroup(point_label, neighbors_label) + for word in words: + word.scale(0.7) + word.add_background_rectangle() + + point_label.next_to(p2, DOWN) + neighbors_label.next_to(p2, UP, buff=1) + bottom = neighbors_label.get_bottom() + kw = { + "buff": 0.1, + "stroke_width": 2, + "tip_length": 0.15 + } + arrows = VGroup( + Arrow(bottom, p1.get_center(), **kw), + Arrow(bottom, p3.get_center(), **kw), + ) + arrows.set_color(WHITE) + + dot = Dot() + dot.set_fill(GREY, opacity=0.2) + dot.replace(p2) + dot.scale(3) + + self.play( + dot.scale, 0, + dot.set_opacity, 0, + FadeInFrom(point_label, DOWN) + ) + self.play( + FadeInFrom(neighbors_label, DOWN), + *map(GrowArrow, arrows) + ) + self.wait() + + # Let d2 change + self.play( + d1.set_y, 3, + d3.set_y, 3, + ) + + def get_v(): + return 0.25 * np.sum([ + d1.get_y(), + -2 * d2.get_y(), + + d3.get_y(), + ]) + v_vect_fader = ValueTracker(0) + v_vect = always_redraw( + lambda: Vector( + get_v() * UP, + color=temperature_to_color( + get_v(), -2, 2, + ), + ).shift(d2.get_center()).set_opacity( + v_vect_fader.get_value(), + ) + ) + d2.add_updater( + lambda d, dt: d.shift( + get_v() * dt * UP, + ) + ) + + self.add(v_vect) + self.play(v_vect_fader.set_value, 1) + self.wait(3) + self.play( + d1.set_y, 0, + d3.set_y, 0, + ) + self.wait(4) + self.play(FadeOut(VGroup( + point_label, + neighbors_label, + arrows + ))) + + self.v_vect = v_vect + self.example_pieces = VGroup(p1, p2, p3) + self.example_dots = VGroup(d1, d2, d3) + + def focus_on_three_points(self): + dots = self.example_dots + d1, d2, d3 = dots + pieces = self.example_pieces + y_axis = self.axes.y_axis + + x_labels, T_labels = [ + VGroup(*[ + TexMobject("{}_{}".format(s, i)) + for i in [1, 2, 3] + ]).scale(0.8) + for s in ("x", "T") + ] + for xl, piece in zip(x_labels, pieces): + xl.next_to(piece, DOWN) + xl.add_background_rectangle() + for Tl, dot in zip(T_labels, dots): + Tl.dot = dot + Tl.add_updater(lambda m: m.next_to( + m.dot, RIGHT, SMALL_BUFF + )) + Tl.add_background_rectangle() + T1, T2, T3 = T_labels + + d2.movement_updater = d2.get_updaters()[0] + dots.clear_updaters() + self.remove(self.v_vect) + + self.play( + ShowCreationThenFadeAround(pieces), + FadeOut(self.dots[:self.index]), + FadeOut(self.v_lines[:self.index]), + FadeOut(self.rod_pieces[:self.index]), + FadeOut(self.dots[self.index + 3:]), + FadeOut(self.v_lines[self.index + 3:]), + FadeOut(self.rod_pieces[self.index + 3:]), + ) + self.play(LaggedStartMap( + FadeInFrom, x_labels, + lambda m: (m, LEFT), + lag_ratio=0.3, + run_time=2, + )) + self.play( + d3.set_y, 1, + d2.set_y, 0.25, + d1.set_y, 0, + ) + self.wait() + self.play(LaggedStart(*[ + TransformFromCopy(xl, Tl) + for xl, Tl in zip(x_labels, T_labels) + ], lag_ratio=0.3, run_time=2)) + self.wait() + + # Show lines + h_lines = VGroup(*map(self.get_h_line, dots)) + hl1, hl2, hl3 = h_lines + + average_pointer = ArrowTip( + start_angle=0, + length=0.2, + ) + average_pointer.set_color(YELLOW) + average_pointer.stretch(0.25, 1) + average_pointer.add_updater( + lambda m: m.move_to( + 0.5 * (hl1.get_start() + hl3.get_start()), + RIGHT + ) + ) + average_arrows = always_redraw(lambda: VGroup(*[ + Arrow( + hl.get_start(), + average_pointer.get_right(), + color=WHITE, + buff=0.0, + ) + for hl in [hl1, hl3] + ])) + average_label = TexMobject( + "{T_1", "+", "T_3", "\\over", "2}" + ) + average_label.scale(0.5) + average_label.add_updater(lambda m: m.next_to( + average_pointer, LEFT, SMALL_BUFF + )) + + average_rect = SurroundingRectangle(average_label) + average_rect.add_updater( + lambda m: m.move_to(average_label) + ) + average_words = TextMobject("Neighbor\\\\average") + average_words.match_width(average_rect) + average_words.match_color(average_rect) + average_words.add_updater( + lambda m: m.next_to(average_rect, UP, SMALL_BUFF) + ) + + mini_T1 = average_label.get_part_by_tex("T_1") + mini_T3 = average_label.get_part_by_tex("T_3") + for mini, line in (mini_T1, hl1), (mini_T3, hl3): + mini.save_state() + mini.next_to(line, LEFT, SMALL_BUFF) + + self.add(hl1, hl3, T_labels) + y_axis.remove(y_axis.numbers) + self.play( + GrowFromPoint(hl1, hl1.get_end()), + GrowFromPoint(hl3, hl3.get_end()), + TransformFromCopy( + T1, mini_T1, + ), + TransformFromCopy( + T3, mini_T3, + ), + FadeOut(y_axis.numbers), + y_axis.set_stroke, {"width": 1}, + ) + self.play( + FadeIn(average_pointer), + Restore(mini_T1), + Restore(mini_T3), + FadeIn(average_label[1]), + FadeIn(average_label[3:]), + *map(GrowArrow, average_arrows) + ) + self.add(average_arrows, average_label) + self.play( + ShowCreation(average_rect), + FadeIn(average_words), + ) + self.play( + GrowFromPoint(hl2, hl2.get_end()) + ) + self.wait() + + # Show formula + formula = TexMobject( + "\\left(", + "{T_1", "+", "T_3", "\\over", "2}", + "-", "T_2", + "\\right)" + ) + formula.to_corner(UR, buff=MED_LARGE_BUFF) + formula.shift(1.7 * LEFT) + brace = Brace(formula, DOWN) + diff_value = DecimalNumber(include_sign=True) + diff_value.add_updater(lambda m: m.set_value( + y_axis.p2n(average_pointer.get_right()) - + y_axis.p2n(d2.get_center()) + )) + diff_value.next_to(brace, DOWN) + + self.play( + ReplacementTransform( + average_label.deepcopy(), + formula[1:1 + len(average_label)] + ), + TransformFromCopy(T2, formula[-2]), + FadeIn(formula[-3]), + FadeIn(formula[-1]), + FadeIn(formula[0]), + GrowFromCenter(brace), + FadeIn(diff_value) + ) + self.wait() + + # Changes + self.play(FadeIn(self.v_vect)) + d2.add_updater(d2.movement_updater) + self.wait(5) + + self.play( + d3.set_y, 3, + d1.set_y, 2.5, + d2.set_y, -2, + ) + self.wait(5) + self.play( + d3.set_y, 1, + d1.set_y, -1, + ) + self.wait(8) + + # Show derivative + lhs = TexMobject( + "{dT_2", "\\over", "dt}", "=", "\\alpha" + ) + dt = lhs.get_part_by_tex("dt") + alpha = lhs.get_part_by_tex("\\alpha") + lhs.next_to(formula, LEFT, SMALL_BUFF) + + self.play(Write(lhs)) + self.play(ShowCreationThenFadeAround(dt)) + self.wait() + self.play(ShowCreationThenFadeAround(alpha)) + self.wait() + self.play( + FadeOut(brace), + FadeOut(diff_value), + ) + + self.lhs = lhs + self.rhs = formula + + def show_difference_formula(self): + lhs = self.lhs + rhs = self.rhs + d1, d2, d3 = self.example_dots + + new_rhs = TexMobject( + "=", + "{\\alpha", "\\over", "2}", + "\\left(", + "(", "T_3", "-", "T_2", ")", + "-", + "(", "T_2", "-", "T_1", ")", + "\\right)" + ) + big_parens = VGroup( + new_rhs.get_part_by_tex("\\left("), + new_rhs.get_part_by_tex("\\right)"), + ) + for paren in big_parens: + paren.scale(2) + new_rhs.next_to(rhs, DOWN) + new_rhs.align_to(lhs.get_part_by_tex("="), LEFT) + + def p2p_anim(mob1, mob2, tex, index=0): + return TransformFromCopy( + mob1.get_parts_by_tex(tex)[index], + mob2.get_parts_by_tex(tex)[index], + ) + + self.play( + p2p_anim(lhs, new_rhs, "="), + p2p_anim(rhs, new_rhs, "\\left("), + p2p_anim(rhs, new_rhs, "\\right)"), + p2p_anim(lhs, new_rhs, "\\alpha"), + p2p_anim(rhs, new_rhs, "\\over"), + p2p_anim(rhs, new_rhs, "2"), + ) + self.play( + p2p_anim(rhs, new_rhs, "T_3"), + p2p_anim(rhs, new_rhs, "-"), + p2p_anim(rhs, new_rhs, "T_2"), + FadeIn(new_rhs.get_parts_by_tex("(")[1]), + FadeIn(new_rhs.get_parts_by_tex(")")[0]), + ) + self.play( + p2p_anim(rhs, new_rhs, "T_2", -1), + p2p_anim(rhs, new_rhs, "-", -1), + p2p_anim(rhs, new_rhs, "T_1"), + FadeIn(new_rhs.get_parts_by_tex("-")[1]), + FadeIn(new_rhs.get_parts_by_tex("(")[2]), + FadeIn(new_rhs.get_parts_by_tex(")")[1]), + ) + self.wait() + + self.rhs2 = new_rhs + + # Show deltas + T1_index = new_rhs.index_of_part_by_tex("T_1") + T3_index = new_rhs.index_of_part_by_tex("T_3") + diff1 = new_rhs[T1_index - 2:T1_index + 1] + diff2 = new_rhs[T3_index:T3_index + 3] + brace1 = Brace(diff1, DOWN, buff=SMALL_BUFF) + brace2 = Brace(diff2, DOWN, buff=SMALL_BUFF) + delta_T1 = TexMobject("\\Delta T_1") + delta_T1.next_to(brace1, DOWN, SMALL_BUFF) + delta_T2 = TexMobject("\\Delta T_2") + delta_T2.next_to(brace2, DOWN, SMALL_BUFF) + minus = TexMobject("-") + minus.move_to(Line( + delta_T1.get_right(), + delta_T2.get_left(), + )) + braces = VGroup(brace1, brace2) + deltas = VGroup(delta_T1, delta_T2) + + kw = { + "direction": LEFT, + "buff": SMALL_BUFF, + "min_num_quads": 2, + } + lil_brace1 = always_redraw(lambda: Brace( + Line(d1.get_left(), d2.get_left()), **kw + )) + lil_brace2 = always_redraw(lambda: Brace( + Line(d2.get_left(), d3.get_left()), **kw + )) + lil_braces = VGroup(lil_brace1, lil_brace2) + lil_delta_T1 = delta_T1.copy() + lil_delta_T2 = delta_T2.copy() + lil_deltas = VGroup(lil_delta_T1, lil_delta_T2) + for brace, delta in zip(lil_braces, lil_deltas): + delta.brace = brace + delta.add_updater(lambda d: d.next_to( + d.brace, LEFT, SMALL_BUFF, + )) + + delta_T1.set_color(BLUE) + lil_delta_T1.set_color(BLUE) + delta_T2.set_color(RED) + lil_delta_T2.set_color(RED) + + double_difference_brace = Brace(deltas, DOWN) + double_difference_words = TextMobject( + "Difference of differences" + ) + double_difference_words.next_to( + double_difference_brace, DOWN + ) + + self.play( + GrowFromCenter(brace1), + GrowFromCenter(lil_brace1), + FadeIn(delta_T1), + FadeIn(lil_delta_T1), + ) + self.wait() + self.play( + GrowFromCenter(brace2), + GrowFromCenter(lil_brace2), + FadeIn(delta_T2), + FadeIn(lil_delta_T2), + ) + self.wait() + self.play( + Write(minus), + GrowFromCenter(double_difference_brace), + Write(double_difference_words), + ) + self.wait() + + self.braces = braces + self.deltas = deltas + self.delta_minus = minus + self.lil_braces = lil_braces + self.lil_deltas = lil_deltas + self.double_difference_brace = double_difference_brace + self.double_difference_words = double_difference_words + + def gut_check_new_interpretation(self): + lil_deltas = self.lil_deltas + d1, d2, d3 = self.example_dots + + self.play(ShowCreationThenFadeAround(lil_deltas[0])) + self.play(ShowCreationThenFadeAround(lil_deltas[1])) + self.wait() + self.play( + d2.shift, MED_SMALL_BUFF * UP, + rate_func=there_and_back, + ) + self.wait() + self.play( + d3.set_y, 3, + d1.set_y, -0.5, + ) + self.wait(5) + self.play( + d3.set_y, 1.5, + d1.set_y, -2, + ) + self.wait(5) + + def write_second_difference(self): + dd_word = self.double_difference_words + + delta_delta = TexMobject("\\Delta \\Delta T_1") + delta_delta.set_color(MAROON_B) + + delta_delta.move_to(dd_word, UP) + + second_difference_word = TextMobject( + "``Second difference''" + ) + second_difference_word.next_to(delta_delta, DOWN) + + self.play( + FadeOutAndShift(dd_word, UP), + FadeInFrom(delta_delta, UP), + ) + self.wait() + self.play( + Write(second_difference_word), + ) + self.wait() + + # Random play + d1, d2, d3 = self.example_dots + self.play( + d3.set_y, 3, + d1.set_y, -0.5, + ) + self.wait(5) + self.play( + d3.set_y, 1.5, + d1.set_y, -2, + ) + self.wait(5) + + self.delta_delta = delta_delta + self.second_difference_word = second_difference_word + + def emphasize_final_expression(self): + lhs = self.lhs + rhs = self.rhs + rhs2 = self.rhs2 + old_dd = self.delta_delta + dd = old_dd.copy() + old_ao2 = rhs2[1:4] + ao2 = old_ao2.copy() + + new_lhs = lhs[:-1] + full_rhs = VGroup( + lhs[-1], + lhs[-2].copy(), + rhs, + rhs2, + self.braces, + self.deltas, + self.delta_minus, + self.double_difference_brace, + old_dd, + self.second_difference_word, + ) + new_rhs = VGroup(ao2, dd) + new_rhs.arrange(RIGHT, buff=SMALL_BUFF) + new_rhs.next_to(new_lhs, RIGHT) + + self.play( + full_rhs.to_edge, DOWN, {"buff": LARGE_BUFF}, + ) + self.play( + TransformFromCopy(old_ao2, ao2), + TransformFromCopy(old_dd, dd), + ) + self.play( + ShowCreationThenFadeAround( + VGroup(new_lhs, new_rhs) + ) + ) + self.wait() + + # + def get_sample_inputs(self): + return np.arange( + self.graph_x_min, + self.graph_x_max + self.step_size, + self.step_size, + ) + + def get_points(self, time=0): + return [ + self.axes.c2p(x, self.temp_func(x, t=time)) + for x in self.get_sample_inputs() + ] + + def get_dots(self, time=0): + points = self.get_points(time) + dots = VGroup(*[ + Dot( + point, + radius=self.dot_radius + ) + for point in points + ]) + dots.color_using_background_image("VerticalTempGradient") + return dots + + def get_dot_dashed_line(self, dot, index, color=False): + direction = np.zeros(3) + direction[index] = -1 + + def get_line(): + p1 = dot.get_edge_center(direction) + p0 = np.array(p1) + p0[index] = self.axes.c2p(0, 0)[index] + result = DashedLine( + p0, p1, + stroke_width=2, + color=WHITE, + stroke_opacity=self.dashed_line_stroke_opacity, + ) + if color: + result.color_using_background_image( + "VerticalTempGradient" + ) + return result + return always_redraw(get_line) + + def get_h_line(self, dot): + return self.get_dot_dashed_line(dot, 0, True) + + def get_v_line(self, dot): + return self.get_dot_dashed_line(dot, 1) + + +class ShowFinitelyManyX(DiscreteSetup): + def construct(self): + self.setup_axes() + axes = self.axes + axes.fade(1) + points = [ + axes.c2p(x, 0) + for x in self.get_sample_inputs()[1:] + ] + x_labels = VGroup(*[ + TexMobject("x_{}".format(i)).next_to( + p, DOWN + ) + for i, p in enumerate(points) + ]) + + self.play(LaggedStartMap( + FadeInFromLarge, x_labels + )) + self.play(LaggedStartMap(FadeOut, x_labels)) + self.wait() + + +class DiscreteGraphStillImage1(DiscreteSetup): + CONFIG = { + "step_size": 1, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.discretize() + + +class DiscreteGraphStillImageFourth(DiscreteGraphStillImage1): + CONFIG = { + "step_size": 0.25, + } + + +class DiscreteGraphStillImageTenth(DiscreteGraphStillImage1): + CONFIG = { + "step_size": 0.1, + "dashed_line_stroke_opacity": 0.25, + "dot_radius": 0.04, + } + + +class DiscreteGraphStillImageHundredth(DiscreteGraphStillImage1): + CONFIG = { + "step_size": 0.01, + "dashed_line_stroke_opacity": 0.1, + "dot_radius": 0.01, + } + + +class TransitionToContinuousCase(DiscreteSetup): + CONFIG = { + "step_size": 0.1, + "tangent_line_length": 3, + "wait_time": 30, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.show_temperature_difference() + self.show_second_derivative() + self.show_curvature_examples() + self.show_time_changes() + + def add_graph(self): + self.setup_graph() + self.play( + ShowCreation( + self.graph, + run_time=3, + ) + ) + self.wait() + + def show_temperature_difference(self): + x_tracker = ValueTracker(2) + dx_tracker = ValueTracker(1) + + line_group = self.get_line_group( + x_tracker, + dx_tracker, + ) + dx_line, dT_line, tan_line, dx_sym, dT_sym = line_group + tan_line.set_stroke(width=0) + + brace = Brace(dx_line, UP) + fixed_distance = TextMobject("Fixed\\\\distance") + fixed_distance.scale(0.7) + fixed_distance.next_to(brace, UP) + delta_T = TexMobject("\\Delta T") + delta_T.move_to(dT_sym, LEFT) + + self.play( + ShowCreation(VGroup(dx_line, dT_line)), + FadeInFrom(delta_T, LEFT) + ) + self.play( + GrowFromCenter(brace), + FadeInFromDown(fixed_distance), + ) + self.wait() + self.play( + FadeOut(delta_T, UP), + FadeIn(dT_sym, DOWN), + FadeOut(brace, UP), + FadeOut(fixed_distance, UP), + FadeIn(dx_sym, DOWN), + ) + self.add(line_group) + self.play( + dx_tracker.set_value, 0.01, + run_time=5 + ) + self.wait() + self.play( + dx_tracker.set_value, 0.3, + ) + + # Show rate of change + to_zero = TexMobject("\\rightarrow 0") + to_zero.match_height(dT_sym) + to_zero.next_to(dT_sym, buff=SMALL_BUFF) + + ratio = TexMobject( + "{\\partial T", "\\over", "\\partial x}" + ) + ratio[0].match_style(dT_sym) + ratio.to_edge(UP) + + self.play(ShowCreationThenFadeAround( + dT_sym, + surrounding_rectangle_config={ + "buff": 0.05, + "stroke_width": 1, + } + )) + self.play(GrowFromPoint(to_zero, dT_sym.get_right())) + self.wait() + self.play( + TransformFromCopy( + dT_sym, + ratio.get_part_by_tex("\\partial T") + ), + TransformFromCopy( + dx_sym, + ratio.get_part_by_tex("\\partial x") + ), + Write(ratio.get_part_by_tex("\\over")) + ) + self.play( + ShowCreation( + tan_line.copy().set_stroke(width=2), + remover=True + ), + FadeOut(to_zero), + ) + tan_line.set_stroke(width=2) + self.wait() + + # Look at neighbors + x0 = x_tracker.get_value() + dx = dx_tracker.get_value() + v_line, lv_line, rv_line = v_lines = VGroup(*[ + self.get_v_line(x) + for x in [x0, x0 - dx, x0 + dx] + ]) + v_lines[1:].set_color(BLUE) + + self.play(ShowCreation(v_line)) + self.play( + TransformFromCopy(v_line, lv_line), + TransformFromCopy(v_line, rv_line), + ) + self.wait() + self.play( + FadeOut(v_lines[1:]), + ApplyMethod( + dx_tracker.set_value, 0.01, + run_time=2 + ), + ) + + self.line_group = line_group + self.deriv = ratio + self.x_tracker = x_tracker + self.dx_tracker = dx_tracker + self.v_line = v_line + + def show_second_derivative(self): + x_tracker = self.x_tracker + deriv = self.deriv + v_line = self.v_line + + deriv_of_deriv = TexMobject( + "{\\partial", + "\\left(", + "{\\partial T", "\\over", "\\partial x}", + "\\right)", + "\\over", + "\\partial x}" + ) + deriv_of_deriv.set_color_by_tex("\\partial T", RED) + + deriv_of_deriv.to_edge(UP) + dT_index = deriv_of_deriv.index_of_part_by_tex("\\partial T") + inner_deriv = deriv_of_deriv[dT_index:dT_index + 3] + + self.play( + ReplacementTransform(deriv, inner_deriv), + Write(VGroup(*filter( + lambda m: m not in inner_deriv, + deriv_of_deriv, + ))) + ) + v_line.add_updater(lambda m: m.become( + self.get_v_line(x_tracker.get_value()) + )) + for change in [-0.1, 0.1]: + self.play( + x_tracker.increment_value, change, + run_time=3 + ) + + # Write second deriv + second_deriv = TexMobject( + "{\\partial^2 T", "\\over", "\\partial x^2}" + ) + second_deriv[0].set_color(RED) + eq = TexMobject("=") + eq.next_to(deriv_of_deriv, RIGHT) + second_deriv.next_to(eq, RIGHT) + second_deriv.align_to(deriv_of_deriv, DOWN) + eq.match_y(second_deriv.get_part_by_tex("\\over")) + + self.play(Write(eq)) + self.play( + TransformFromCopy( + deriv_of_deriv.get_parts_by_tex("\\partial")[:2], + second_deriv.get_parts_by_tex("\\partial^2 T"), + ), + ) + self.play( + Write(second_deriv.get_part_by_tex("\\over")), + TransformFromCopy( + deriv_of_deriv.get_parts_by_tex("\\partial x"), + second_deriv.get_parts_by_tex("\\partial x"), + ), + ) + self.wait() + + def show_curvature_examples(self): + x_tracker = self.x_tracker + v_line = self.v_line + line_group = self.line_group + + x_tracker.set_value(3.6) + self.wait() + self.play( + x_tracker.set_value, 3.8, + run_time=4, + ) + self.wait() + x_tracker.set_value(6.2) + self.wait() + self.play( + x_tracker.set_value, 6.4, + run_time=4, + ) + self.wait() + + # + dx = 0.2 + neighbor_lines = always_redraw(lambda: VGroup(*[ + self.get_v_line( + x_tracker.get_value() + u * dx, + line_class=Line, + ) + for u in [-1, 1] + ])) + neighbor_lines.set_color(BLUE) + + self.play(FadeOut(line_group)) + self.play(*[ + TransformFromCopy(v_line, nl) + for nl in neighbor_lines + ]) + self.add(neighbor_lines) + self.play( + x_tracker.set_value, 5, + run_time=5, + rate_func=lambda t: smooth(t, 3) + ) + v_line.clear_updaters() + self.play( + FadeOut(v_line), + FadeOut(neighbor_lines), + ) + self.wait() + + def show_time_changes(self): + self.setup_clock() + graph = self.graph + + time_label = self.time_label + clock = self.clock + time_label.next_to(clock, DOWN) + + graph.add_updater(self.update_graph) + time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + + self.add(time_label) + self.add_arrows() + self.play( + ClockPassesTime( + clock, + run_time=self.wait_time, + hours_passed=self.wait_time, + ), + ) + + # + def get_v_line(self, x, line_class=DashedLine, stroke_width=2): + axes = self.axes + graph = self.graph + line = line_class( + axes.c2p(x, 0), + graph.point_from_proportion( + inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + x, + ) + ), + stroke_width=stroke_width, + ) + return line + + def get_line_group(self, + x_tracker, + dx_tracker, + dx_tex="\\partial x", + dT_tex="\\partial T", + max_sym_width=0.5, + ): + graph = self.graph + get_x = x_tracker.get_value + get_dx = dx_tracker.get_value + + dx_line = Line(color=WHITE) + dT_line = Line(color=RED) + tan_line = Line(color=WHITE) + lines = VGroup(dx_line, dT_line, tan_line) + lines.set_stroke(width=2) + dx_sym = TexMobject(dx_tex) + dT_sym = TexMobject(dT_tex) + dT_sym.match_color(dT_line) + syms = VGroup(dx_sym, dT_sym) + + group = VGroup(*lines, *syms) + + def update_group(group): + dxl, dTl, tanl, dxs, dTs = group + x = get_x() + dx = get_dx() + p0, p2 = [ + graph.point_from_proportion( + inverse_interpolate( + self.graph_x_min, + self.graph_x_max, + x + ) + ) + for x in [x, x + dx] + ] + p1 = np.array([p2[0], *p0[1:]]) + dxl.put_start_and_end_on(p0, p1) + dTl.put_start_and_end_on(p1, p2) + tanl.put_start_and_end_on(p0, p2) + tanl.scale( + self.tangent_line_length / + tanl.get_length() + ) + dxs.match_width(dxl) + dTs.set_height(0.7 * dTl.get_height()) + for sym in dxs, dTs: + if sym.get_width() > max_sym_width: + sym.set_width(max_sym_width) + dxs.next_to( + dxl, -dTl.get_vector(), SMALL_BUFF, + ) + dTs.next_to( + dTl, dxl.get_vector(), SMALL_BUFF, + ) + + group.add_updater(update_group) + return group + + +class ShowManyVLines(TransitionToContinuousCase): + CONFIG = { + "wait_time": 20, + "max_denom": 10, + "x_step": 0.025, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_v_lines() + self.show_time_changes() + + def add_arrows(self): + pass + + def add_v_lines(self): + axes = self.axes + + v_lines = always_redraw(lambda: VGroup(*[ + self.get_v_line( + x, + line_class=Line, + stroke_width=0.5, + ) + for x in np.arange(0, 10, self.x_step) + ])) + group = VGroup(*v_lines) + + x_pointer = ArrowTip(start_angle=PI / 2) + x_pointer.set_color(WHITE) + x_pointer.next_to(axes.c2p(0, 0), DOWN, buff=0) + x_eq = VGroup( + TexMobject("x="), + DecimalNumber(0) + ) + x_eq.add_updater( + lambda m: m.arrange(RIGHT, buff=SMALL_BUFF) + ) + x_eq.add_updater( + lambda m: m[1].set_value(axes.x_axis.p2n(x_pointer.get_top())) + ) + x_eq.add_updater(lambda m: m.next_to( + x_pointer, DOWN, SMALL_BUFF, + submobject_to_align=x_eq[0] + )) + + self.add(x_pointer, x_eq) + self.play( + Write( + group, + remover=True, + lag_ratio=self.x_step / 2, + run_time=6, + ), + ApplyMethod( + x_pointer.next_to, + axes.c2p(10, 0), + DOWN, {"buff": 0}, + rate_func=linear, + run_time=5, + ), + ) + self.add(v_lines) + x_eq.clear_updaters() + self.play( + FadeOut(x_eq), + FadeOut(x_pointer), + ) + + +class ShowNewtonsLawGraph(Scene): + CONFIG = { + "k": 0.2, + "initial_water_temp": 80, + "room_temp": 20, + "delta_T_color": YELLOW, + } + + def construct(self): + self.setup_axes() + self.show_temperatures() + self.show_graph() + self.show_equation() + self.talk_through_examples() + + def setup_axes(self): + axes = Axes( + x_min=0, + x_max=10, + y_min=0, + y_max=100, + y_axis_config={ + "unit_size": 0.06, + "tick_frequency": 10, + }, + center_point=5 * LEFT + 2.5 * DOWN + ) + x_axis = axes.x_axis + y_axis = axes.y_axis + y_axis.add_numbers(*range(20, 100, 20)) + x_axis.add_numbers(*range(1, 11)) + + x_axis.label = TextMobject("Time") + x_axis.label.next_to(x_axis, DOWN, MED_SMALL_BUFF) + + y_axis.label = TexMobject("\\text{Temperature}") + y_axis.label.next_to(y_axis, RIGHT, buff=SMALL_BUFF) + y_axis.label.align_to(axes, UP) + for axis in [x_axis, y_axis]: + axis.add(axis.label) + + self.add(axes) + self.axes = axes + + def show_temperatures(self): + axes = self.axes + + water_dot = Dot() + water_dot.color_using_background_image("VerticalTempGradient") + water_dot.move_to(axes.c2p(0, self.initial_water_temp)) + room_line = DashedLine( + axes.c2p(0, self.room_temp), + axes.c2p(10, self.room_temp), + ) + room_line.set_color(BLUE) + room_line.color_using_background_image("VerticalTempGradient") + + water_arrow = Vector(LEFT, color=WHITE) + water_arrow.next_to(water_dot, RIGHT, SMALL_BUFF) + water_words = TextMobject( + "Initial water\\\\temperature" + ) + water_words.scale(0.7) + water_words.next_to(water_arrow, RIGHT) + + room_words = TextMobject("Room temperature") + room_words.scale(0.7) + room_words.next_to(room_line, DOWN, SMALL_BUFF) + + self.play( + FadeInFrom(water_dot, RIGHT), + GrowArrow(water_arrow), + Write(water_words), + run_time=1, + ) + self.play(ShowCreation(room_line)) + self.play(FadeInFromDown(room_words)) + self.wait() + + self.set_variables_as_attrs( + water_dot, + water_arrow, + water_words, + room_line, + room_words, + ) + + def show_graph(self): + axes = self.axes + water_dot = self.water_dot + + k = self.k + rt = self.room_temp + t0 = self.initial_water_temp + graph = axes.get_graph( + lambda t: rt + (t0 - rt) * np.exp(-k * t) + ) + graph.color_using_background_image("VerticalTempGradient") + + def get_x(): + return axes.x_axis.p2n(water_dot.get_center()) + + brace_line = always_redraw(lambda: Line( + axes.c2p(get_x(), rt), + water_dot.get_center(), + stroke_width=0, + )) + brace = always_redraw( + lambda: Brace( + brace_line, RIGHT, buff=SMALL_BUFF + ) + ) + + delta_T = TexMobject("\\Delta T") + delta_T.set_color(self.delta_T_color) + delta_T.add_updater(lambda m: m.next_to( + brace, RIGHT, SMALL_BUFF + )) + + self.add(brace_line) + self.play( + GrowFromCenter(brace), + Write(delta_T), + ) + self.play( + ShowCreation(graph), + UpdateFromFunc( + water_dot, + lambda m: m.move_to(graph.get_end()) + ), + run_time=10, + rate_func=linear, + ) + self.wait() + + self.graph = graph + self.brace = brace + self.delta_T = delta_T + + def show_equation(self): + delta_T = self.delta_T + + equation = TexMobject( + "{d ({\\Delta T}) \\over dt} = -k \\cdot {\\Delta T}", + tex_to_color_map={ + "{\\Delta T}": self.delta_T_color, + "-k": WHITE, + "=": WHITE, + } + ) + equation.to_corner(UR) + equation.shift(LEFT) + + delta_T_parts = equation.get_parts_by_tex("\\Delta T") + eq_i = equation.index_of_part_by_tex("=") + deriv = equation[:eq_i] + prop_to = equation.get_part_by_tex("-k") + parts = VGroup(deriv, prop_to, delta_T_parts[1]) + + words = TextMobject( + "Rate of change", + "is proportional to", + "itself", + ) + words.scale(0.7) + words.next_to(equation, DOWN) + colors = [BLUE, WHITE, YELLOW] + for part, word, color in zip(parts, words, colors): + part.word = word + word.set_color(color) + word.save_state() + words[0].next_to(parts[0], DOWN) + + self.play( + TransformFromCopy( + VGroup(delta_T), + delta_T_parts, + ), + Write(VGroup(*filter( + lambda p: p not in delta_T_parts, + equation + ))) + ) + + rects = VGroup() + for part in parts: + rect = SurroundingRectangle( + part, + color=part.word.get_color(), + buff=SMALL_BUFF, + stroke_width=2, + ) + anims = [ + ShowCreation(rect), + FadeIn(part.word), + ] + if part is parts[1]: + anims.append(Restore(words[0])) + self.play(*anims) + rects.add(rect) + + self.play(FadeOut(rects, lag_ratio=0.2)) + + self.equation = equation + self.equation_words = words + + def talk_through_examples(self): + dot = self.water_dot + graph = self.graph + + self.play( + MoveAlongPath( + dot, graph, + rate_func=lambda t: smooth(1 - t), + run_time=2, + ) + ) + + # + def get_slope_line(self, graph, x): + pass diff --git a/active_projects/diffyq/part2/pi_scenes.py b/active_projects/diffyq/part2/pi_scenes.py new file mode 100644 index 00000000..c044996b --- /dev/null +++ b/active_projects/diffyq/part2/pi_scenes.py @@ -0,0 +1,142 @@ +from manimlib.imports import * +from active_projects.ode.part2.wordy_scenes import WriteHeatEquationTemplate + + +class ReactionsToInitialHeatEquation(PiCreatureScene): + def construct(self): + randy = self.pi_creature + randy.set_color(BLUE_C) + randy.center() + + point = VectorizedPoint().next_to(randy, UL, LARGE_BUFF) + randy.add_updater(lambda r: r.look_at(point)) + + self.play(randy.change, "horrified") + self.wait() + self.play(randy.change, "pondering") + self.wait() + self.play( + randy.change, "confused", + point.next_to, randy, UR, LARGE_BUFF, + ) + self.wait(2) + self.play( + point.shift, 2 * DOWN, + randy.change, "horrified" + ) + self.wait(4) + + +class ContrastPDEToODE(TeacherStudentsScene): + CONFIG = { + "random_seed": 2, + } + + def construct(self): + student = self.students[2] + pde, ode = words = VGroup(*[ + TextMobject( + text + "\\\\", + "Differential\\\\", + "Equation" + ) + for text in ("Partial", "Ordinary") + ]) + pde[0].set_color(YELLOW) + ode[0].set_color(BLUE) + for word in words: + word.arrange(DOWN, aligned_edge=LEFT) + + words.arrange(RIGHT, buff=LARGE_BUFF) + words.next_to(student.get_corner(UR), UP, MED_LARGE_BUFF) + words.shift(UR) + lt = TexMobject("<") + lt.scale(1.5) + lt.move_to(Line(pde.get_right(), ode.get_left())) + + for pi in self.pi_creatures: + pi.add_updater(lambda p: p.look_at(pde)) + + self.play( + FadeInFromDown(VGroup(words, lt)), + student.change, "raise_right_hand", + ) + self.play( + self.get_student_changes("pondering", "pondering", "hooray"), + self.teacher.change, "happy" + ) + self.wait(3) + self.play( + Swap(ode, pde), + self.teacher.change, "raise_right_hand", + self.get_student_changes( + "erm", "sassy", "confused" + ) + ) + self.look_at(words) + self.change_student_modes( + "thinking", "thinking", "tease", + ) + self.wait(3) + + +class AskAboutWhereEquationComesFrom(TeacherStudentsScene, WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation() + equation.move_to(self.hold_up_spot, DOWN) + + self.play( + FadeInFromDown(equation), + self.teacher.change, "raise_right_hand" + ) + self.student_says( + "Um...why?", + target_mode="sassy", + student_index=2, + bubble_kwargs={"direction": RIGHT}, + ) + self.change_student_modes( + "confused", "confused", "sassy", + ) + self.wait() + self.play( + self.teacher.change, "pondering", + ) + self.wait(2) + + +class AskWhyRewriteIt(TeacherStudentsScene): + def construct(self): + self.student_says( + "Why?", student_index=1, + bubble_kwargs={"height": 2, "width": 2}, + ) + self.students[1].bubble = None + self.teacher_says( + "One step closer\\\\to derivatives" + ) + self.change_student_modes( + "thinking", "thinking", "thinking", + look_at_arg=4 * LEFT + 2 * UP + ) + self.wait(2) + + +class ReferenceKhanVideo(TeacherStudentsScene): + def construct(self): + khan_logo = ImageMobject("KhanLogo") + khan_logo.set_height(1) + khan_logo.next_to(self.teacher, UP, buff=2) + khan_logo.shift(2 * LEFT) + + self.play( + self.teacher.change, "raise_right_hand", + ) + self.change_student_modes( + "thinking", "pondering", "thinking", + look_at_arg=self.screen + ) + self.wait() + self.play(FadeInFromDown(khan_logo)) + self.look_at(self.screen) + self.wait(15) diff --git a/active_projects/diffyq/part2/shared_constructs.py b/active_projects/diffyq/part2/shared_constructs.py new file mode 100644 index 00000000..9418145e --- /dev/null +++ b/active_projects/diffyq/part2/shared_constructs.py @@ -0,0 +1,35 @@ +from manimlib.imports import * + +TIME_COLOR = YELLOW +X_COLOR = GREEN + + +def get_heat_equation(): + pass + + +def temperature_to_color(temp, min_temp=-1, max_temp=1): + colors = [BLUE, TEAL, GREEN, YELLOW, "#ff0000"] + + alpha = inverse_interpolate(min_temp, max_temp, temp) + index, sub_alpha = integer_interpolate( + 0, len(colors) - 1, alpha + ) + return interpolate_color( + colors[index], colors[index + 1], sub_alpha + ) + + +def two_d_temp_func(x, y, t): + return np.sum([ + c * np.sin(f * var) * np.exp(-(f**2) * t) + for c, f, var in [ + (0.2, 1, x), + (0.3, 3, x), + (0.02, 5, x), + (0.01, 7, x), + (0.5, 2, y), + (0.1, 10, y), + (0.01, 20, y), + ] + ]) diff --git a/active_projects/diffyq/part2/staging.py b/active_projects/diffyq/part2/staging.py new file mode 100644 index 00000000..7450c823 --- /dev/null +++ b/active_projects/diffyq/part2/staging.py @@ -0,0 +1,794 @@ +from manimlib.imports import * +from active_projects.ode.part1.staging import TourOfDifferentialEquations + + +class PartTwoOfTour(TourOfDifferentialEquations): + CONFIG = { + "zoomed_thumbnail_index": 1, + } + + def construct(self): + self.add_title() + self.show_thumbnails() + self.zoom_in_to_one_thumbnail() + + def zoom_in_to_one_thumbnail(self): + frame = self.camera_frame + thumbnails = self.thumbnails + + ode = TextMobject("Ordinary\\\\", "Differential Equation") + pde = TextMobject("Partial\\\\", "Differential Equation") + for word, thumbnail, vect in zip([ode, pde], thumbnails, [DOWN, UP]): + word.match_width(thumbnail) + word.next_to(thumbnail, vect) + ode[0].set_color(BLUE) + pde[0].set_color(YELLOW) + + self.add(ode) + + frame.save_state() + self.play( + frame.replace, + thumbnails[0], + run_time=1, + ) + self.play( + Restore(frame, run_time=3), + ) + self.play( + TransformFromCopy(ode, pde), + ) + self.play( + ApplyMethod( + frame.replace, thumbnails[1], + path_arc=(-30 * DEGREES), + run_time=3 + ), + ) + self.wait() + + +class BrownianMotion(Scene): + CONFIG = { + "wait_time": 60, + "L": 3, # Box in [-L, L] x [-L, L] + "n_particles": 100, + "m1": 1, + "m2": 100, + "r1": 0.05, + "r2": 0.5, + "max_v": 5, + "random_seed": 2, + } + + def construct(self): + self.add_title() + self.add_particles() + self.wait(self.wait_time) + + def add_title(self): + square = Square(side_length=2 * self.L) + title = TextMobject("Brownian motion") + title.scale(1.5) + title.next_to(square, UP) + + self.add(square) + self.add(title) + + def add_particles(self): + m1 = self.m1 + m2 = self.m2 + r1 = self.r1 + r2 = self.r2 + L = self.L + max_v = self.max_v + n_particles = self.n_particles + + lil_particles = VGroup(*[ + self.get_particle(m1, r1, L, max_v) + for k in range(n_particles) + ]) + big_particle = self.get_particle(m2, r2, L=r2, max_v=0) + big_particle.set_fill(YELLOW, 1) + + for p in lil_particles: + if self.are_colliding(p, big_particle): + lil_particles.remove(p) + all_particles = VGroup(big_particle, *lil_particles) + all_particles.add_updater(self.update_particles) + + path = self.get_traced_path(big_particle) + + self.add(all_particles) + self.add(path) + + self.particles = all_particles + self.big_particle = big_particle + self.path = path + + def get_particle(self, m, r, L, max_v): + dot = Dot(radius=r) + dot.set_fill(WHITE, 0.7) + dot.mass = m + dot.radius = r + dot.center = op.add( + np.random.uniform(-L + r, L - r) * RIGHT, + np.random.uniform(-L + r, L - r) * UP + ) + dot.move_to(dot.center) + dot.velocity = rotate_vector( + np.random.uniform(0, max_v) * RIGHT, + np.random.uniform(0, TAU), + ) + return dot + + def are_colliding(self, p1, p2): + d = get_norm(p1.get_center() - p2.get_center()) + return (d < p1.radius + p2.radius) + + def get_traced_path(self, particle): + path = VMobject() + path.set_stroke(BLUE, 3) + path.start_new_path(particle.get_center()) + + buff = 0.02 + + def update_path(path): + new_point = particle.get_center() + if get_norm(new_point - path.get_last_point()) > buff: + path.add_line_to(new_point) + + path.add_updater(update_path) + return path + + def update_particles(self, particles, dt): + for p1 in particles: + p1.center += p1.velocity * dt + + # Check particle collisions + buff = 0.01 + for p2 in particles: + if p1 is p2: + continue + v = p2.center - p1.center + dist = get_norm(v) + r_sum = p1.radius + p2.radius + diff = dist - r_sum + if diff < 0: + unit_v = v / dist + p1.center += (diff - buff) * unit_v / 2 + p2.center += -(diff - buff) * unit_v / 2 + u1 = p1.velocity + u2 = p2.velocity + m1 = p1.mass + m2 = p2.mass + v1 = ( + (m2 * (u2 - u1) + m1 * u1 + m2 * u2) / + (m1 + m2) + ) + v2 = ( + (m1 * (u1 - u2) + m1 * u1 + m2 * u2) / + (m1 + m2) + ) + p1.velocity = v1 + p2.velocity = v2 + + # Check edge collisions + r1 = p1.radius + c1 = p1.center + for i in [0, 1]: + if abs(c1[i]) + r1 > self.L: + c1[i] = np.sign(c1[i]) * (self.L - r1) + p1.velocity[i] *= -1 * op.mul( + np.sign(p1.velocity[i]), + np.sign(c1[i]) + ) + + for p in particles: + p.move_to(p.center) + return particles + + +class AltBrownianMotion(BrownianMotion): + CONFIG = { + "wait_time": 20, + "n_particles": 100, + "m2": 10, + } + + +class BlackScholes(AltBrownianMotion): + def construct(self): + # For some reason I'm amused by the thought + # Of this graph perfectly matching the Brownian + # Motion y-coordiante + self.add_title() + self.add_particles() + self.particles.set_opacity(0) + self.remove(self.path) + self.add_graph() + self.wait(self.wait_time) + + def add_title(self): + title = TextMobject("Black-Sholes equations") + title.scale(1.5) + title.next_to(2 * UP, UP) + + equation = TexMobject( + "{\\partial V \\over \\partial t}", "+", + "\\frac{1}{2} \\sigma^2 S^2", + "{\\partial^2 V \\over \\partial S^2}", "+", + "rS", "{\\partial V \\over \\partial S}", + "-rV", "=", "0", + ) + equation.scale(0.8) + equation.next_to(title, DOWN) + + self.add(title) + self.add(equation) + self.title = title + self.equation = equation + + def add_graph(self): + axes = Axes( + x_min=-1, + x_max=20, + y_min=0, + y_max=10, + number_line_config={ + "unit_size": 0.5, + }, + ) + axes.set_height(4) + axes.move_to(DOWN) + + def get_graph_point(): + return axes.c2p( + self.get_time(), + 5 + 2 * self.big_particle.get_center()[1] + ) + + graph = VMobject() + graph.match_style(self.path) + graph.start_new_path(get_graph_point()) + graph.add_updater( + lambda g: g.add_line_to(get_graph_point()) + ) + + self.add(axes) + self.add(graph) + + +class ContrastChapters1And2(Scene): + def construct(self): + c1_frame, c2_frame = frames = VGroup(*[ + ScreenRectangle(height=3.5) + for x in range(2) + ]) + frames.arrange(RIGHT, buff=LARGE_BUFF) + + c1_title, c2_title = titles = VGroup( + TextMobject("Chapter 1"), + TextMobject("Chapter 2"), + ) + titles.scale(1.5) + + ode, pde = des = VGroup( + TextMobject( + "Ordinary", + "Differential Equations\\\\", + "ODEs", + ), + TextMobject( + "Partial", + "Differential Equations\\\\", + "PDEs", + ), + ) + ode[0].set_color(BLUE) + pde[0].set_color(YELLOW) + for de in des: + de[-1][0].match_color(de[0]) + de[-1].scale(1.5, about_point=de.get_top()) + + for title, frame, de in zip(titles, frames, des): + title.next_to(frame, UP) + de.match_width(frame) + de.next_to(frame, DOWN) + + lt = TexMobject("<") + lt.move_to(Line(ode.get_right(), pde.get_left())) + lt.scale(2, about_edge=UP) + + c1_words = TextMobject( + "They're", "really\\\\", "{}", + "freaking", "hard\\\\", + "to", "solve\\\\", + ) + c1_words.set_height(0.5 * c1_frame.get_height()) + c1_words.move_to(c1_frame) + + c2_words = TextMobject( + "They're", "really", "\\emph{really}\\\\", + "freaking", "hard\\\\", + "to", "solve\\\\", + ) + c2_words.set_color_by_tex("\\emph", YELLOW) + c2_words.move_to(c2_frame) + edit_shift = MED_LARGE_BUFF * RIGHT + c2_edits = VGroup( + TextMobject("sometimes").next_to( + c2_words[1:3], UP, + aligned_edge=LEFT, + ), + Line( + c2_words[1].get_left(), + c2_words[2].get_right(), + stroke_width=8, + ), + TextMobject("not too").next_to( + c2_words[3], LEFT, + ), + Line( + c2_words[3].get_left(), + c2_words[3].get_right(), + stroke_width=8, + ), + ) + c2_edits.set_color(RED) + c2_edits[2:].shift(edit_shift) + + self.add(titles) + self.add(frames) + self.add(des) + + self.wait() + self.play(LaggedStartMap( + FadeInFromDown, c1_words, + lag_ratio=0.1, + )) + self.wait() + # self.play(FadeIn(ode)) + self.play( + # TransformFromCopy(ode, pde), + TransformFromCopy(c1_words, c2_words), + Write(lt) + ) + self.wait() + self.play( + Write(c2_edits[:2], run_time=1), + ) + self.play( + c2_words[3:5].shift, edit_shift, + Write(c2_edits[2:]), + run_time=1, + ) + self.wait() + + +class ShowCubeFormation(ThreeDScene): + CONFIG = { + "camera_config": { + "shading_factor": 1.0, + }, + "color": False, + } + + def construct(self): + light_source = self.camera.light_source + light_source.move_to(np.array([-6, -3, 6])) + + cube = Cube( + side_length=4, + fill_color=GREY, + stroke_color=WHITE, + stroke_width=0.5, + ) + cube.set_fill(opacity=1) + if self.color: + # cube[0].set_color(BLUE) + # cube[1].set_color(RED) + # for face in cube[2:]: + # face.set_color([BLUE, RED]) + cube.color_using_background_image("VerticalTempGradient") + + # light_source.next_to(cube, np.array([1, -1, 1]), buff=2) + + cube_3d = cube.copy() + cube_2d = cube_3d.copy().stretch(0, 2) + cube_1d = cube_2d.copy().stretch(0, 1) + cube_0d = cube_1d.copy().stretch(0, 0) + + cube.become(cube_0d) + + self.set_camera_orientation( + phi=70 * DEGREES, + theta=-145 * DEGREES, + ) + self.begin_ambient_camera_rotation(rate=0.05) + + for target in [cube_1d, cube_2d, cube_3d]: + self.play( + Transform(cube, target, run_time=1.5) + ) + self.wait(8) + + +class ShowCubeFormationWithColor(ShowCubeFormation): + CONFIG = { + "color": True, + } + + +class ShowRect(Scene): + CONFIG = { + "height": 1, + "width": 3, + } + + def construct(self): + rect = Rectangle( + height=self.height, + width=self.width, + ) + rect.set_color(YELLOW) + self.play(ShowCreationThenFadeOut(rect)) + + +class ShowSquare(ShowRect): + CONFIG = { + "height": 1, + "width": 1, + } + + +class ShowHLine(Scene): + def construct(self): + line = Line(LEFT, RIGHT) + line.set_color(BLUE) + self.play(ShowCreationThenFadeOut(line)) + + +class ShowCross(Scene): + def construct(self): + cross = Cross(Square()) + cross.set_width(3) + cross.set_height(1, stretch=True) + self.play(ShowCreation(cross)) + + +class TwoBodyEquations(Scene): + def construct(self): + kw = { + "tex_to_color_map": { + "x_1": LIGHT_GREY, + "y_1": LIGHT_GREY, + "x_2": BLUE, + "y_2": BLUE, + "=": WHITE, + } + } + equations = VGroup( + TexMobject( + "{d^2 x_1 \\over dt^2}", + "=", + "{x_2 - x_1 \\over m_1 \\left(", + "(x_2 - x_1)^2 + (y_2 - y_1)^2", + "\\right)^{3/2}", + **kw + ), + TexMobject( + "{d^2 y_1 \\over dt^2}", + "=", + "{y_2 - y_1 \\over m_1 \\left(", + "(x_2 - x_1)^2 + (y_2 - y_1)^2", + "\\right)^{3/2}", + **kw + ), + TexMobject( + "{d^2 x_2 \\over dt^2}", + "=", + "{x_1 - x_2 \\over m_2 \\left(", + "(x_2 - x_1)^2 + (y_2 - y_1)^2", + "\\right)^{3/2}", + **kw + ), + TexMobject( + "{d^2 y_2 \\over dt^2}", + "=", + "{y_1 - y_2 \\over m_2 \\left(", + "(x_2 - x_1)^2 + (y_2 - y_1)^2", + "\\right)^{3/2}", + **kw + ), + ) + + equations.arrange(DOWN, buff=LARGE_BUFF) + equations.set_height(6) + equations.to_edge(LEFT) + + variables = VGroup() + lhss = VGroup() + rhss = VGroup() + for equation in equations: + variable = equation[1] + lhs = equation[:4] + rhs = equation[4:] + variables.add(variable) + lhss.add(lhs) + rhss.add(rhs) + lhss_copy = lhss.copy() + + for variable, lhs in zip(variables, lhss): + variable.save_state() + variable.match_height(lhs) + variable.scale(0.7) + variable.move_to(lhs, LEFT) + + self.play(LaggedStart(*[ + FadeInFrom(v, RIGHT) + for v in variables + ])) + self.wait() + self.play( + LaggedStartMap(Restore, variables), + FadeIn( + lhss_copy, + remover=True, + lag_ratio=0.1, + run_time=2, + ) + ) + self.add(lhss) + self.wait() + self.play(LaggedStartMap( + FadeIn, rhss + )) + self.wait() + self.play( + LaggedStart(*[ + ShowCreationThenFadeAround(lhs[:3]) + for lhs in lhss + ]) + ) + self.wait() + self.play( + LaggedStartMap( + ShowCreationThenFadeAround, + rhss, + ) + ) + self.wait() + + +class LaplacianIntuition(SpecialThreeDScene): + CONFIG = { + "three_d_axes_config": { + "x_min": -5, + "x_max": 5, + "y_min": -5, + "y_max": 5, + }, + "surface_resolution": 32, + } + + def construct(self): + axes = self.get_axes() + axes.scale(0.5, about_point=ORIGIN) + self.set_camera_to_default_position() + self.begin_ambient_camera_rotation() + + def func(x, y): + return np.array([ + x, y, + 2.7 + 0.5 * (np.sin(x) + np.cos(y)) - + 0.025 * (x**2 + y**2) + ]) + + surface_config = { + "u_min": -5, + "u_max": 5, + "v_min": -5, + "v_max": 5, + "resolution": self.surface_resolution, + } + # plane = ParametricSurface( + # lambda u, v: np.array([u, v, 0]), + # **surface_config + # ) + # plane.set_stroke(WHITE, width=0.1) + # plane.set_fill(WHITE, opacity=0.1) + plane = Square( + side_length=10, + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.1, + ) + plane.center() + plane.set_shade_in_3d(True) + + surface = ParametricSurface( + func, **surface_config + ) + surface.set_stroke(BLUE, width=0.1) + surface.set_fill(BLUE, opacity=0.25) + + self.add(axes, plane, surface) + + point = VectorizedPoint(np.array([2, -2, 0])) + dot = Dot() + dot.set_color(GREEN) + dot.add_updater(lambda d: d.move_to(point)) + line = always_redraw(lambda: DashedLine( + point.get_location(), + func(*point.get_location()[:2]), + background_image_file="VerticalTempGradient", + )) + + circle = Circle(radius=0.25) + circle.set_color(YELLOW) + circle.insert_n_curves(20) + circle.add_updater(lambda m: m.move_to(point)) + circle.set_shade_in_3d(True) + surface_circle = always_redraw( + lambda: circle.copy().apply_function( + lambda p: func(*p[:2]) + ).shift( + 0.02 * IN + ).color_using_background_image("VerticalTempGradient") + ) + + self.play(FadeInFromLarge(dot)) + self.play(ShowCreation(line)) + self.play(TransformFromCopy(dot, circle)) + self.play( + Transform( + circle.copy(), + surface_circle.copy().clear_updaters(), + remover=True, + ) + ) + self.add(surface_circle) + + self.wait() + for vect in [4 * LEFT, DOWN, 4 * RIGHT, UP]: + self.play( + point.shift, vect, + run_time=3, + ) + + +class StrogatzMention(PiCreatureScene): + def construct(self): + self.show_book() + self.show_motives() + self.show_pages() + + def show_book(self): + morty = self.pi_creature + book = ImageMobject("InfinitePowers") + book.set_height(5) + book.to_edge(LEFT) + + steve = ImageMobject("Strogatz_by_bricks") + steve.set_height(5) + steve.to_edge(LEFT) + + name = TextMobject("Steven Strogatz") + name.match_width(steve) + name.next_to(steve, DOWN) + + self.think( + "Hmm...many good\\\\lessons here...", + run_time=1 + ) + self.wait() + self.play(FadeInFromDown(steve)) + self.wait() + self.play( + FadeInFrom(book, DOWN), + steve.shift, 4 * RIGHT, + RemovePiCreatureBubble( + morty, target_mode="thinking" + ) + ) + self.wait(3) + self.play( + FadeOut(steve), + FadeOut(morty), + ) + + self.book = book + + def show_motives(self): + motives = VGroup( + TextMobject("1) Scratch and itch"), + TextMobject("2) Make people love math"), + ) + motives.scale(1.5) + motives.arrange( + DOWN, LARGE_BUFF, + aligned_edge=LEFT, + ) + motives.move_to( + Line( + self.book.get_right(), + FRAME_WIDTH * RIGHT / 2 + ) + ) + motives.to_edge(UP) + + for motive in motives: + self.play(FadeInFromDown(motive)) + self.wait(2) + self.play(FadeOut(motives)) + + def show_pages(self): + book = self.book + pages = Group(*[ + ImageMobject("IP_Sample_Page{}".format(i)) + for i in range(1, 4) + ]) + for page in pages: + page.match_height(book) + page.next_to(book, RIGHT) + + last_page = VectorizedPoint() + for page in pages: + self.play( + FadeOut(last_page), + FadeIn(page) + ) + self.wait() + last_page = page + + self.play(FadeOut(last_page)) + + def create_pi_creature(self): + return Mortimer().to_corner(DR) + + +class Thumbnail(Scene): + def construct(self): + image = ImageMobject("HeatSurfaceExampleFlipped") + image.set_height(6.5) + image.to_edge(DOWN, buff=-SMALL_BUFF) + self.add(image) + + equation = TexMobject( + "{\\partial {T} \\over \\partial {t}}", "=", + "\\alpha", "\\nabla^2 {T}", + tex_to_color_map={ + "{t}": YELLOW, + "{T}": RED, + } + ) + equation.scale(2) + equation.to_edge(UP) + + self.add(equation) + + Group(equation, image).shift(1.5 * RIGHT) + + question = TextMobject("What is\\\\this?") + question.scale(2.5) + question.to_edge(LEFT) + arrow = Arrow( + question.get_top(), + equation.get_left(), + buff=0.5, + path_arc=-90 * DEGREES, + ) + arrow.set_stroke(width=5) + + self.add(question, arrow) + + +class ShowNewton(Scene): + def construct(self): + pass + + +class ShowCupOfWater(Scene): + def construct(self): + pass diff --git a/active_projects/diffyq/part2/wordy_scenes.py b/active_projects/diffyq/part2/wordy_scenes.py new file mode 100644 index 00000000..861474ae --- /dev/null +++ b/active_projects/diffyq/part2/wordy_scenes.py @@ -0,0 +1,785 @@ +from manimlib.imports import * + + +class WriteHeatEquationTemplate(Scene): + CONFIG = { + "tex_mobject_config": { + "tex_to_color_map": { + "{T}": WHITE, + "{t}": YELLOW, + "{x}": GREEN, + "{y}": RED, + "{z}": BLUE, + "\\partial": WHITE, + "2": WHITE, + }, + }, + } + + def get_d1_equation(self): + return TexMobject( + "{\\partial {T} \\over \\partial {t}}({x}, {t})", "=", + "\\alpha \\cdot", + "{\\partial^2 {T} \\over \\partial {x}^2} ({x}, {t})", + **self.tex_mobject_config + ) + + def get_d1_equation_without_inputs(self): + return TexMobject( + "{\\partial {T} \\over \\partial {t}}", "=", + "\\alpha \\cdot", + "{\\partial^2 {T} \\over \\partial {x}^2}", + **self.tex_mobject_config + ) + + def get_d3_equation(self): + return TexMobject( + "{\\partial {T} \\over \\partial {t}}", "=", + "\\alpha \\left(", + "{\\partial^2 {T} \\over \\partial {x}^2} + ", + "{\\partial^2 {T} \\over \\partial {y}^2} + ", + "{\\partial^2 {T} \\over \\partial {z}^2}", + "\\right)", + **self.tex_mobject_config + ) + + def get_general_equation(self): + return TexMobject( + "{\\partial {T} \\over \\partial {t}}", "=", + "\\alpha", "\\nabla^2 {T}", + **self.tex_mobject_config, + ) + + def get_d3_equation_with_inputs(self): + return TexMobject( + "{\\partial {T} \\over \\partial {t}}", + "({x}, {y}, {z}, {t})", "=", + "\\alpha \\left(", + "{\\partial^2 {T} \\over \\partial {x}^2}", + "({x}, {y}, {z}, {t}) + ", + "{\\partial^2 {T} \\over \\partial {y}^2}", + "({x}, {y}, {z}, {t}) + ", + "{\\partial^2 {T} \\over \\partial {z}^2}", + "({x}, {y}, {z}, {t})", + "\\right)", + **self.tex_mobject_config + ) + + def get_d1_words(self): + return TextMobject("Heat equation\\\\", "(1 dimension)") + + def get_d3_words(self): + return TextMobject("Heat equation\\\\", "(3 dimensions)") + + def get_d1_group(self): + group = VGroup( + self.get_d1_words(), + self.get_d1_equation(), + ) + group.arrange(DOWN, buff=MED_LARGE_BUFF) + return group + + def get_d3_group(self): + group = VGroup( + self.get_d3_words(), + self.get_d3_equation(), + ) + group.arrange(DOWN, buff=MED_LARGE_BUFF) + return group + + +class HeatEquationIntroTitle(WriteHeatEquationTemplate): + def construct(self): + scale_factor = 1.25 + title = TextMobject("The Heat Equation") + title.scale(scale_factor) + title.to_edge(UP) + + equation = self.get_general_equation() + equation.scale(scale_factor) + equation.next_to(title, DOWN, MED_LARGE_BUFF) + equation.set_color_by_tex("{T}", RED) + + self.play( + FadeInFrom(title, DOWN), + FadeInFrom(equation, UP), + ) + self.wait() + + +class BringTogether(Scene): + def construct(self): + arrows = VGroup(Vector(2 * RIGHT), Vector(2 * LEFT)) + arrows.arrange(RIGHT, buff=2) + words = TextMobject("Bring together")[0] + words.next_to(arrows, DOWN) + words.save_state() + words.space_out_submobjects(1.2) + + self.play( + VFadeIn(words), + Restore(words), + arrows.arrange, RIGHT, {"buff": SMALL_BUFF}, + VFadeIn(arrows), + ) + self.play(FadeOut(words), FadeOut(arrows)) + + +class FourierSeriesIntro(WriteHeatEquationTemplate): + def construct(self): + title_scale_value = 1.5 + + title = TextMobject( + "Fourier ", "Series", + ) + title.scale(title_scale_value) + title.to_edge(UP) + title.generate_target() + + details_coming = TextMobject("Details coming...") + details_coming.next_to(title.get_corner(DR), DOWN) + details_coming.set_color(LIGHT_GREY) + + # physics = TextMobject("Physics") + heat = TextMobject("Heat") + heat.scale(title_scale_value) + physics = self.get_general_equation() + physics.set_color_by_tex("{T}", RED) + arrow1 = Arrow(LEFT, RIGHT) + arrow2 = Arrow(LEFT, RIGHT) + group = VGroup( + heat, arrow1, physics, arrow2, title.target + ) + group.arrange(RIGHT) + # physics.align_to(title.target, UP) + group.to_edge(UP) + + rot_square = Square() + rot_square.fade(1) + rot_square.add_updater(lambda m, dt: m.rotate(dt)) + + def update_heat_colors(heat): + colors = [YELLOW, RED] + vertices = rot_square.get_vertices() + letters = heat.family_members_with_points() + for letter, vertex in zip(letters, vertices): + alpha = (normalize(vertex)[0] + 1) / 2 + i, sa = integer_interpolate(0, len(colors) - 1, alpha) + letter.set_color(interpolate_color( + colors[i], colors[i + 1], alpha, + )) + heat.add_updater(update_heat_colors) + + image = ImageMobject("Joseph Fourier") + image.set_height(5) + image.next_to(title, DOWN, LARGE_BUFF) + image.to_edge(LEFT) + name = TextMobject("Joseph", "Fourier") + name.next_to(image, DOWN) + + bubble = ThoughtBubble( + height=2, + width=2.5, + direction=RIGHT, + ) + bubble.set_fill(opacity=0) + bubble.set_stroke(WHITE) + bubble.set_stroke(BLACK, 5, background=True) + bubble.shift(heat.get_center() - bubble.get_bubble_center()) + bubble[:-1].shift(LEFT + 0.2 * DOWN) + bubble[:-1].rotate(-20 * DEGREES) + for mob in bubble[:-1]: + mob.rotate(20 * DEGREES) + + # self.play(FadeInFromDown(title)) + self.add(title) + self.play( + FadeInFromDown(image), + TransformFromCopy( + title.get_part_by_tex("Fourier"), + name.get_part_by_tex("Fourier"), + path_arc=90 * DEGREES, + ), + FadeIn(name.get_part_by_tex("Joseph")), + ) + self.play(Write(details_coming, run_time=1)) + self.play(LaggedStartMap(FadeOut, details_coming[0], run_time=1)) + self.wait() + self.add(rot_square) + self.play( + FadeInFrom(physics, RIGHT), + GrowArrow(arrow2), + FadeInFrom(heat, RIGHT), + GrowArrow(arrow1), + MoveToTarget(title), + ) + self.play(ShowCreation(bubble)) + self.wait(10) + + +class CompareODEToPDE(Scene): + def construct(self): + pass + + +class TodaysTargetWrapper(Scene): + def construct(self): + pass + + +class TwoGraphTypeTitles(Scene): + def construct(self): + left_title = TextMobject( + "Represent time\\\\with actual time" + ) + left_title.shift(FRAME_WIDTH * LEFT / 4) + right_title = TextMobject( + "Represent time\\\\with an axis" + ) + right_title.shift(FRAME_WIDTH * RIGHT / 4) + + titles = VGroup(left_title, right_title) + for title in titles: + title.scale(1.25) + title.to_edge(UP) + + self.play(FadeInFromDown(right_title)) + self.wait() + self.play(FadeInFromDown(left_title)) + self.wait() + + +class ShowPartialDerivativeSymbols(Scene): + def construct(self): + t2c = { + "{x}": GREEN, + "{t}": YELLOW, + } + d_derivs, del_derivs = VGroup(*[ + VGroup(*[ + TexMobject( + "{" + sym, "T", "\\over", sym, var + "}", + "(", "{x}", ",", "{t}", ")", + ).set_color_by_tex_to_color_map(t2c) + for var in ("{x}", "{t}") + ]) + for sym in ("d", "\\partial") + ]) + dTdx, dTdt = d_derivs + delTdelx, delTdelx = del_derivs + dels = VGroup(*it.chain(*[ + del_deriv.get_parts_by_tex("\\partial") + for del_deriv in del_derivs + ])) + + dTdx.to_edge(UP) + self.play(FadeInFrom(dTdx, DOWN)) + self.wait() + self.play(ShowCreationThenFadeAround(dTdx[3:5])) + self.play(ShowCreationThenFadeAround(dTdx[:2])) + self.wait() + + dTdt.move_to(dTdx) + self.play( + dTdx.next_to, dTdt, RIGHT, {"buff": 1.5}, + dTdx.set_opacity, 0.5, + FadeInFromDown(dTdt) + ) + self.wait() + + for m1, m2 in zip(d_derivs, del_derivs): + m2.move_to(m1) + + pd_words = TextMobject("Partial derivatives") + pd_words.next_to(del_derivs, DOWN, MED_LARGE_BUFF) + + self.play( + Write(pd_words), + dTdx.set_opacity, 1, + run_time=1, + ) + self.wait() + self.play( + ReplacementTransform(d_derivs, del_derivs) + ) + self.play( + LaggedStartMap( + ShowCreationThenFadeAround, + dels, + surrounding_rectangle_config={ + "color": BLUE, + "buff": 0.5 * SMALL_BUFF, + "stroke_width": 2, + } + ) + ) + self.wait() + + num_words = VGroup(*[ + TextMobject( + "Change in $T$\\\\caused by {}", + "$\\partial$", "${}$".format(var), + arg_separator="", + ).set_color_by_tex_to_color_map(t2c) + for var in ("{x}", "{t}") + ]) + num_words.scale(0.8) + for word, deriv in zip(num_words, del_derivs): + num = deriv[:2] + word.move_to(num, UP) + word.to_edge(UP, buff=MED_SMALL_BUFF) + deriv.rect = SurroundingRectangle( + num, + buff=SMALL_BUFF, + stroke_width=2, + color=word[-1].get_color(), + ) + deriv.rect.mob = num + deriv.rect.add_updater(lambda r: r.move_to(r.mob)) + + self.play( + Write(num_words[1]), + VGroup(del_derivs, pd_words).shift, DOWN, + ShowCreation(del_derivs[1].rect), + ) + self.play( + Write(num_words[0]), + ShowCreation(del_derivs[0].rect), + ) + self.wait() + + +class WriteHeatEquation(WriteHeatEquationTemplate): + def construct(self): + title = TextMobject("The Heat Equation") + title.to_edge(UP) + + equation = self.get_d1_equation() + equation.next_to(title, DOWN) + + eq_i = equation.index_of_part_by_tex("=") + dt_part = equation[:eq_i] + dx_part = equation[eq_i + 3:] + dt_rect = SurroundingRectangle(dt_part) + dt_rect.set_stroke(YELLOW, 2) + dx_rect = SurroundingRectangle(dx_part) + dx_rect.set_stroke(GREEN, 2) + + two_outlines = equation.get_parts_by_tex("2").copy() + two_outlines.set_stroke(YELLOW, 2) + two_outlines.set_fill(opacity=0) + + to_be_explained = TextMobject( + "To be explained shortly..." + ) + to_be_explained.scale(0.7) + to_be_explained.next_to(equation, RIGHT, MED_LARGE_BUFF) + to_be_explained.fade(1) + + pde = TextMobject("Partial Differential Equation") + pde.move_to(title) + + del_outlines = equation.get_parts_by_tex("\\partial").copy() + del_outlines.set_stroke(YELLOW, 2) + del_outlines.set_fill(opacity=0) + + self.play( + FadeInFrom(title, 0.5 * DOWN), + FadeInFrom(equation, 0.5 * UP), + ) + self.wait() + self.play(ShowCreation(dt_rect)) + self.wait() + self.play(TransformFromCopy(dt_rect, dx_rect)) + self.play(ShowCreationThenDestruction(two_outlines)) + self.wait() + self.play(Write(to_be_explained, run_time=1)) + self.wait(2) + self.play( + ShowCreationThenDestruction( + del_outlines, + lag_ratio=0.1, + ) + ) + self.play( + FadeOutAndShift(title, UP), + FadeInFrom(pde, DOWN), + FadeOut(dt_rect), + FadeOut(dx_rect), + ) + self.wait() + + +class Show3DEquation(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d3_equation_with_inputs() + equation.set_width(FRAME_WIDTH - 1) + inputs = VGroup(*it.chain(*[ + equation.get_parts_by_tex(s) + for s in ["{x}", "{y}", "{z}", "{t}"] + ])) + inputs.sort() + equation.to_edge(UP) + + self.add(equation) + self.play(LaggedStartMap( + ShowCreationThenFadeAround, inputs, + surrounding_rectangle_config={ + "buff": 0.05, + "stroke_width": 2, + } + )) + self.wait() + + +class Show1DAnd3DEquations(WriteHeatEquationTemplate): + def construct(self): + d1_group = self.get_d1_group() + d3_group = self.get_d3_group() + d1_words, d1_equation = d1_group + d3_words, d3_equation = d3_group + + groups = VGroup(d1_group, d3_group) + for group in groups: + group.arrange(DOWN, buff=MED_LARGE_BUFF) + groups.arrange(RIGHT, buff=1.5) + groups.to_edge(UP) + + d3_rhs = d3_equation[9:-2] + d3_brace = Brace(d3_rhs, DOWN) + nabla_words = TextMobject("Sometimes written as") + nabla_words.match_width(d3_brace) + nabla_words.next_to(d3_brace, DOWN) + nabla_exp = TexMobject( + "\\nabla^2 {T}", + **self.tex_mobject_config, + ) + nabla_exp.next_to(nabla_words, DOWN) + # nabla_group = VGroup(nabla_words, nabla_exp) + + d1_group.save_state() + d1_group.center().to_edge(UP) + + self.play( + Write(d1_words), + FadeInFrom(d1_equation, UP), + run_time=1, + ) + self.wait(2) + self.play( + Restore(d1_group), + FadeInFrom(d3_group, LEFT) + ) + self.wait() + self.play( + GrowFromCenter(d3_brace), + Write(nabla_words), + TransformFromCopy(d3_rhs, nabla_exp), + run_time=1, + ) + self.wait() + + +class D1EquationNoInputs(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation_without_inputs() + equation.to_edge(UP) + # i1 = equation.index_of_part_by_tex("\\partial") + # i2 = equation.index_of_part_by_tex("\\cdot") + # equation[i1:i1 + 2].set_color(RED) + # equation[i2 + 1:i2 + 6].set_color(RED) + equation.set_color_by_tex("{T}", RED) + self.add(equation) + + +class AltHeatRHS(Scene): + def construct(self): + formula = TexMobject( + "{\\alpha \\over 2}", "\\Big(", + "T({x} - 1, {t}) + T({x} + 1, {t})" + "\\Big)", + tex_to_color_map={ + "{x}": GREEN, + "{t}": YELLOW, + } + ) + self.add(formula) + + +class CompareInputsOfGeneralCaseTo1D(WriteHeatEquation): + def construct(self): + three_d_expr, one_d_expr = [ + TexMobject( + "{T}(" + inputs + ", {t})", + **self.tex_mobject_config, + ) + for inputs in ["{x}, {y}, {z}", "{x}"] + ] + for expr in three_d_expr, one_d_expr: + expr.scale(2) + expr.to_edge(UP) + + x, y, z = [ + three_d_expr.get_part_by_tex(letter) + for letter in ["x", "y", "z"] + ] + + self.play(FadeInFromDown(three_d_expr)) + self.play(LaggedStartMap( + ShowCreationThenFadeAround, + VGroup(x, y, z) + )) + self.wait() + low = 3 + high = -3 + self.play( + ReplacementTransform(three_d_expr[:low], one_d_expr[:low]), + ReplacementTransform(three_d_expr[high:], one_d_expr[high:]), + three_d_expr[low:high].scale, 0, + ) + self.wait() + + +class ShowLaplacian(WriteHeatEquation): + def construct(self): + equation = self.get_d3_equation() + equation.to_edge(UP, buff=MED_SMALL_BUFF) + + parts = VGroup() + plusses = VGroup() + for char in "xyz": + index = equation.index_of_part_by_tex( + "{" + char + "}" + ) + part = equation[index - 6:index + 3] + rect = SurroundingRectangle(part) + rect.match_color(equation[index]) + parts.add(part) + part.rect = rect + if char in "yz": + plus = equation[index - 8] + part.plus = plus + plusses.add(plus) + + lp = equation.get_part_by_tex("(") + rp = equation.get_part_by_tex(")") + + for part in parts: + part.rp = rp.copy() + part.rp.next_to(part, RIGHT, SMALL_BUFF) + part.rp.align_to(lp, UP) + rp.become(parts[0].rp) + + # Show new second derivatives + self.add(*equation) + self.remove(*plusses, *parts[1], *parts[2]) + for part in parts[1:]: + self.play( + rp.become, part.rp, + FadeInFrom(part, LEFT), + Write(part.plus), + ShowCreation(part.rect), + ) + self.play( + FadeOut(part.rect), + ) + self.wait() + + # Show laplacian + brace = Brace(parts, DOWN) + laplacian = TexMobject("\\nabla^2", "T") + laplacian.next_to(brace, DOWN) + laplacian_name = TextMobject( + "``Laplacian''" + ) + laplacian_name.next_to(laplacian, DOWN) + + T_parts = VGroup(*[part[3] for part in parts]) + non_T_parts = VGroup(*[ + VGroup(*part[:3], *part[4:]) + for part in parts + ]) + + self.play(GrowFromCenter(brace)) + self.play(Write(laplacian_name)) + self.play( + TransformFromCopy(non_T_parts, laplacian[0]) + ) + self.play( + TransformFromCopy(T_parts, laplacian[1]) + ) + self.wait(3) + + +class AskAboutActuallySolving(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation() + equation.center() + + q1 = TextMobject("Solve for T?") + q1.next_to(equation, UP, LARGE_BUFF) + q2 = TextMobject("What does it \\emph{mean} to solve this?") + q2.next_to(equation, UP, LARGE_BUFF) + formula = TexMobject( + "T({x}, {t}) = \\sin\\big(a{x}\\big) e^{-\\alpha \\cdot a^2 {t}}", + tex_to_color_map={ + "{x}": GREEN, + "{t}": YELLOW, + } + ) + formula.next_to(equation, DOWN, LARGE_BUFF) + q3 = TextMobject("Is this it?") + arrow = Vector(LEFT, color=WHITE) + arrow.next_to(formula, RIGHT) + q3.next_to(arrow, RIGHT) + + self.add(equation) + self.play(FadeInFromDown(q1)) + self.wait() + self.play( + FadeInFromDown(q2), + q1.shift, 1.5 * UP, + ) + self.play(FadeInFrom(formula, UP)) + self.play( + GrowArrow(arrow), + FadeInFrom(q3, LEFT) + ) + self.wait() + + +class PDEPatreonEndscreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Juan Benet", + "Vassili Philippov", + "Burt Humburg", + "Matt Russell", + "Scott Gray", + "soekul", + "Tihan Seale", + "Richard Barthel", + "Ali Yahya", + "dave nicponski", + "Evan Phillips", + "Graham", + "Joseph Kelly", + "Kaustuv DeBiswas", + "LambdaLabs", + "Lukas Biewald", + "Mike Coleman", + "Peter Mcinerney", + "Quantopian", + "Roy Larson", + "Scott Walter, Ph.D.", + "Yana Chernobilsky", + "Yu Jun", + "Jordan Scales", + "D. Sivakumar", + "Lukas -krtek.net- Novy", + "John Shaughnessy", + "Britt Selvitelle", + "David Gow", + "J", + "Jonathan Wilson", + "Joseph John Cox", + "Magnus Dahlström", + "Randy C. Will", + "Ryan Atallah", + "Luc Ritchie", + "1stViewMaths", + "Adrian Robinson", + "Alexis Olson", + "Andreas Benjamin Brössel", + "Andrew Busey", + "Ankalagon", + "Antoine Bruguier", + "Antonio Juarez", + "Arjun Chakroborty", + "Art Ianuzzi", + "Awoo", + "Bernd Sing", + "Boris Veselinovich", + "Brian Staroselsky", + "Chad Hurst", + "Charles Southerland", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Cooper Jones", + "Danger Dai", + "Dave B", + "Dave Kester", + "David B. Hill", + "David Clark", + "DeathByShrimp", + "Delton Ding", + "eaglle", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Giovanni Filippi", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Isaac Jeffrey Lee", + "j eduardo perez", + "Jacob Magnuson", + "Jameel Syed", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Kai-Siang Ang", + "Kanan Gill", + "L0j1k", + "Lee Beck", + "Lee Redden", + "Linh Tran", + "Ludwig Schubert", + "Magister Mugit", + "Mark B Bahu", + "Mark Heising", + "Martin Price", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Bouchard", + "Matthew Cocke", + "Michael Faust", + "Michael Hardel", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nikita Lesnikov", + "Omar Zrien", + "Owen Campbell-Moore", + "Peter Ehrnstrom", + "RedAgent14", + "rehmi post", + "Richard Burgmann", + "Richard Comish", + "Ripta Pasay", + "Rish Kundalia", + "Robert Teed", + "Roobie", + "Ryan Williams", + "Sachit Nagpal", + "Solara570", + "Stevie Metke", + "Tal Einav", + "Ted Suzman", + "Thomas Tarler", + "Tom Fleming", + "Valeriy Skobelev", + "Xavier Bernard", + "Yavor Ivanov", + "Yaw Etse", + "YinYangBalance.Asia", + "Zach Cardwell", + ], + } diff --git a/active_projects/diffyq/part3/discrete_case.py b/active_projects/diffyq/part3/discrete_case.py new file mode 100644 index 00000000..2c8ae08c --- /dev/null +++ b/active_projects/diffyq/part3/discrete_case.py @@ -0,0 +1,303 @@ +from manimlib.imports import * +from active_projects.ode.part2.heat_equation import * + + +class ShowNewRuleAtDiscreteBoundary(DiscreteSetup): + CONFIG = { + "axes_config": { + "x_min": 0, + "stroke_width": 1, + "x_axis_config": { + "include_tip": False, + }, + }, + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + ], + "v_line_class": DashedLine, + "v_line_config": { + + }, + "step_size": 1, + "wait_time": 15, + "alpha": 0.25, + } + + def construct(self): + self.add_axes() + self.set_points() + self.show_boundary_point_influenced_by_neighbor() + self.add_clock() + self.let_evolve() + + def set_points(self): + axes = self.axes + for mob in axes.family_members_with_points(): + if isinstance(mob, Line): + mob.set_stroke(width=1) + + step_size = self.step_size + xs = np.arange( + axes.x_min, + axes.x_max + step_size, + step_size + ) + + dots = self.dots = self.get_dots(axes, xs) + self.v_lines = self.get_v_lines(dots) + self.rod_pieces = self.get_rod_pieces(dots) + + # rod_pieces + + self.add(self.dots) + self.add(self.v_lines) + self.add(self.rod_pieces) + + def show_boundary_point_influenced_by_neighbor(self): + dots = self.dots + ld = dots[0] + ld_in = dots[1] + rd = dots[-1] + rd_in = dots[-2] + v_len = 0.75 + l_arrow = Vector(v_len * LEFT) + l_arrow.move_to(ld.get_left(), RIGHT) + r_arrow = Vector(v_len * RIGHT) + r_arrow.move_to(rd.get_right(), LEFT) + arrows = VGroup(l_arrow, r_arrow) + q_marks = VGroup(*[ + TexMobject("?").scale(1.5).next_to( + arrow, arrow.get_vector() + ) + for arrow in arrows + ]) + + arrows.set_color(YELLOW) + q_marks.set_color(YELLOW) + + blocking_rects = VGroup(*[ + BackgroundRectangle(VGroup( + *dots[i:-i], + *self.rod_pieces[i:-i] + )) + for i in [1, 2] + ]) + for rect in blocking_rects: + rect.stretch(1.1, dim=1, about_edge=UP) + + self.play(FadeIn(blocking_rects[0])) + self.play( + LaggedStartMap(ShowCreation, arrows), + LaggedStart(*[ + FadeInFrom(q_mark, -arrow.get_vector()) + for q_mark, arrow in zip(q_marks, arrows) + ]), + run_time=1.5 + ) + self.wait() + + # Point to inward neighbor + new_arrows = VGroup(*[ + Arrow( + d1.get_center(), + VGroup(d1, d2).get_center(), + buff=0, + ).match_style(l_arrow) + for d1, d2 in [(ld, ld_in), (rd, rd_in)] + ]) + new_arrows.match_style(arrows) + + l_brace = Brace(VGroup(ld, ld_in), DOWN) + r_brace = Brace(VGroup(rd, rd_in), DOWN) + braces = VGroup(l_brace, r_brace) + for brace in braces: + brace.align_to( + self.axes.x_axis.get_center(), UP + ) + brace.shift(SMALL_BUFF * DOWN) + brace.add(brace.get_tex("\\Delta x")) + + self.play( + ReplacementTransform(arrows, new_arrows), + FadeOut(q_marks), + ReplacementTransform(*blocking_rects) + ) + self.wait() + self.play(FadeInFrom(braces, UP)) + self.wait() + self.play( + FadeOut(new_arrows), + FadeOut(blocking_rects[1]), + FadeOut(braces), + ) + + def add_clock(self): + super().add_clock() + self.time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + VGroup( + self.clock, + self.time_label + ).shift(2 * LEFT) + + def let_evolve(self): + dots = self.dots + dots.add_updater(self.update_dots) + + wait_time = self.wait_time + self.play( + ClockPassesTime( + self.clock, + run_time=wait_time, + hours_passed=wait_time, + ), + ) + + # + + def get_dots(self, axes, xs): + dots = VGroup(*[ + Dot(axes.c2p(x, self.temp_func(x, 0))) + for x in xs + ]) + + max_width = 0.8 * self.step_size + for dot in dots: + dot.add_updater(self.update_dot_color) + if dot.get_width() > max_width: + dot.set_width(max_width) + + return dots + + def get_v_lines(self, dots): + return always_redraw(lambda: VGroup(*[ + self.get_v_line(dot) + for dot in dots + ])) + + def get_v_line(self, dot): + x_axis = self.axes.x_axis + bottom = dot.get_bottom() + x = x_axis.p2n(bottom) + proj_point = x_axis.n2p(x) + return self.v_line_class( + proj_point, bottom, + **self.v_line_config, + ) + + def get_rod_pieces(self, dots): + axis = self.axes.x_axis + factor = 1 - np.exp(-(0.8 / self.step_size)**2) + width = factor * self.step_size + + pieces = VGroup() + for dot in dots: + piece = Line(ORIGIN, width * RIGHT) + piece.set_stroke(width=5) + piece.move_to(dot) + piece.set_y(axis.get_center()[1]) + piece.dot = dot + piece.add_updater( + lambda p: p.match_color(p.dot) + ) + pieces.add(piece) + return pieces + + def update_dot_color(self, dot): + y = self.axes.y_axis.p2n(dot.get_center()) + dot.set_color(self.y_to_color(y)) + + def update_dots(self, dots, dt): + for ds in zip(dots, dots[1:], dots[2:]): + points = [d.get_center() for d in ds] + x0, x1, x2 = [p[0] for p in points] + dx = x1 - x0 + y0, y1, y2 = [p[1] for p in points] + + self.update_dot( + dot=ds[1], + dt=dt, + mean_diff=0.5 * (y2 - 2 * y1 + y0) / dx + ) + if ds[0] is dots[0]: + self.update_dot( + dot=ds[0], + dt=dt, + mean_diff=(y1 - y0) / dx + ) + elif ds[-1] is dots[-1]: + self.update_dot( + dot=ds[-1], + dt=dt, + mean_diff=(y1 - y2) / dx + ) + + def update_dot(self, dot, dt, mean_diff): + dot.shift(mean_diff * self.alpha * dt * UP) + + +class DiscreteEvolutionPoint25(ShowNewRuleAtDiscreteBoundary): + CONFIG = { + "step_size": 0.25, + "alpha": 0.5, + "wait_time": 30, + } + + def construct(self): + self.add_axes() + self.set_points() + self.add_clock() + self.let_evolve() + + +class DiscreteEvolutionPoint1(DiscreteEvolutionPoint25): + CONFIG = { + "step_size": 0.1, + "v_line_config": { + "stroke_width": 1, + }, + "wait_time": 30, + } + + +class FlatEdgesForDiscreteEvolution(DiscreteEvolutionPoint1): + CONFIG = { + "wait_time": 20, + "step_size": 0.1, + } + + def let_evolve(self): + lines = VGroup(*[ + Line(LEFT, RIGHT) + for x in range(2) + ]) + lines.set_width(1.5) + lines.set_stroke(WHITE, 5, opacity=0.5) + lines.add_updater(self.update_lines) + + turn_animation_into_updater( + ShowCreation(lines, run_time=2) + ) + self.add(lines) + + super().let_evolve() + + def update_lines(self, lines): + dots = self.dots + for line, dot in zip(lines, [dots[0], dots[-1]]): + line.move_to(dot) + + +class FlatEdgesForDiscreteEvolutionTinySteps(FlatEdgesForDiscreteEvolution): + CONFIG = { + "step_size": 0.025, + "wait_time": 10, + "v_line_class": Line, + "v_line_config": { + "stroke_opacity": 0.5, + } + } diff --git a/active_projects/diffyq/part3/pi_creature_scenes.py b/active_projects/diffyq/part3/pi_creature_scenes.py new file mode 100644 index 00000000..61b8f7c0 --- /dev/null +++ b/active_projects/diffyq/part3/pi_creature_scenes.py @@ -0,0 +1,175 @@ +from manimlib.imports import * +from active_projects.ode.part2.wordy_scenes import * + + +class IveHeardOfThis(TeacherStudentsScene): + def construct(self): + point = VectorizedPoint() + point.move_to(3 * RIGHT + 2 * UP) + self.student_says( + "I've heard\\\\", "of this!", + student_index=1, + target_mode="hooray", + bubble_kwargs={ + "height": 3, + "width": 3, + "direction": RIGHT, + }, + run_time=1, + ) + self.change_student_modes( + "thinking", "hooray", "thinking", + look_at_arg=point, + added_anims=[self.teacher.change, "happy"] + ) + self.wait(3) + self.student_says( + "But who\\\\", "cares?", + student_index=1, + target_mode="maybe", + bubble_kwargs={ + "direction": RIGHT, + "width": 3, + "height": 3, + }, + run_time=1, + ) + self.change_student_modes( + "pondering", "maybe", "pondering", + look_at_arg=point, + added_anims=[self.teacher.change, "guilty"] + ) + self.wait(5) + + +class InFouriersShoes(PiCreatureScene, WriteHeatEquationTemplate): + def construct(self): + randy = self.pi_creature + fourier = ImageMobject("Joseph Fourier") + fourier.set_height(4) + fourier.next_to(randy, RIGHT, LARGE_BUFF) + fourier.align_to(randy, DOWN) + + equation = self.get_d1_equation() + equation.next_to(fourier, UP, MED_LARGE_BUFF) + + decades = list(range(1740, 2040, 20)) + time_line = NumberLine( + x_min=decades[0], + x_max=decades[-1], + tick_frequency=1, + tick_size=0.05, + longer_tick_multiple=4, + unit_size=0.2, + numbers_with_elongated_ticks=decades, + numbers_to_show=decades, + decimal_number_config={ + "group_with_commas": False, + }, + stroke_width=2, + ) + time_line.add_numbers() + time_line.move_to(ORIGIN, RIGHT) + time_line.to_edge(UP) + triangle = ArrowTip(start_angle=-90 * DEGREES) + triangle.set_height(0.25) + triangle.move_to(time_line.n2p(2019), DOWN) + triangle.set_color(WHITE) + + self.play(FadeInFrom(fourier, 2 * LEFT)) + self.play(randy.change, "pondering") + self.wait() + self.play( + DrawBorderThenFill(triangle, run_time=1), + FadeInFromDown(equation), + FadeIn(time_line), + ) + self.play( + Animation(triangle), + ApplyMethod( + time_line.shift, + time_line.n2p(2019) - time_line.n2p(1822), + run_time=5 + ), + ) + self.wait() + + +class SineCurveIsUnrealistic(TeacherStudentsScene): + def construct(self): + self.student_says( + "But that would\\\\never happen!", + student_index=1, + bubble_kwargs={ + "direction": RIGHT, + "height": 3, + "width": 4, + }, + target_mode="angry" + ) + self.change_student_modes( + "guilty", "angry", "hesitant", + added_anims=[ + self.teacher.change, "tease" + ] + ) + self.wait(3) + self.play( + RemovePiCreatureBubble(self.students[1]), + self.teacher.change, "raise_right_hand" + ) + self.change_all_student_modes( + "pondering", + look_at_arg=3 * UP, + ) + self.wait(5) + + +class IfOnly(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "If only!", + target_mode="angry" + ) + self.change_all_student_modes( + "confused", + look_at_arg=self.screen + ) + self.wait(3) + + +class SoWeGotNowhere(TeacherStudentsScene): + def construct(self): + self.student_says( + "So we've gotten\\\\nowhere!", + target_mode="angry", + added_anims=[ + self.teacher.change, "guilty" + ] + ) + self.change_all_student_modes("angry") + self.wait() + text = TexMobject( + "&\\text{Actually,}\\\\", + "&\\sin\\left({x}\\right)" + "e^{-\\alpha {t}}\\\\", + "&\\text{isn't far off.}", + tex_to_color_map={ + "{x}": GREEN, + "{t}": YELLOW, + } + ) + text.scale(0.8) + self.teacher_says( + text, + content_introduction_class=FadeIn, + bubble_kwargs={ + "width": 4, + "height": 3.5, + } + ) + self.change_all_student_modes( + "pondering", + look_at_arg=self.screen + ) + self.wait(3) diff --git a/active_projects/diffyq/part3/staging.py b/active_projects/diffyq/part3/staging.py new file mode 100644 index 00000000..e7cc4871 --- /dev/null +++ b/active_projects/diffyq/part3/staging.py @@ -0,0 +1,1230 @@ +from manimlib.imports import * + +from active_projects.ode.part2.fourier_series import FourierOfTrebleClef + + +class FourierNameIntro(Scene): + def construct(self): + self.show_two_titles() + self.transition_to_image() + self.show_paper() + + def show_two_titles(self): + lt = TextMobject("Fourier", "Series") + rt = TextMobject("Fourier", "Transform") + lt_variants = VGroup( + TextMobject("Complex", "Fourier Series"), + TextMobject("Discrete", "Fourier Series"), + ) + rt_variants = VGroup( + TextMobject("Discrete", "Fourier Transform"), + TextMobject("Fast", "Fourier Transform"), + TextMobject("Quantum", "Fourier Transform"), + ) + + titles = VGroup(lt, rt) + titles.scale(1.5) + for title, vect in (lt, LEFT), (rt, RIGHT): + title.move_to(vect * FRAME_WIDTH / 4) + title.to_edge(UP) + + for title, variants in (lt, lt_variants), (rt, rt_variants): + title.save_state() + title.target = title.copy() + title.target.scale(1 / 1.5, about_edge=RIGHT) + for variant in variants: + variant.move_to(title.target, UR) + variant[0].set_color(YELLOW) + + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.set_stroke(WHITE, 2) + + self.play( + FadeInFrom(lt, RIGHT), + ShowCreation(v_line) + ) + self.play( + FadeInFrom(rt, LEFT), + ) + # Edit in images of circle animations + # and clips from FT video + + # for title, variants in (rt, rt_variants), (lt, lt_variants): + for title, variants in [(rt, rt_variants)]: + # Maybe do it for left variant, maybe not... + self.play( + MoveToTarget(title), + FadeInFrom(variants[0][0], LEFT) + ) + for v1, v2 in zip(variants, variants[1:]): + self.play( + FadeOutAndShift(v1[0], UP), + FadeInFrom(v2[0], DOWN), + run_time=0.5, + ) + self.wait(0.5) + self.play( + Restore(title), + FadeOut(variants[-1][0]) + ) + self.wait() + + self.titles = titles + self.v_line = v_line + + def transition_to_image(self): + titles = self.titles + v_line = self.v_line + + image = ImageMobject("Joseph Fourier") + image.set_height(5) + image.to_edge(LEFT) + + frame = Rectangle() + frame.replace(image, stretch=True) + + name = TextMobject("Joseph", "Fourier") + fourier_part = name.get_part_by_tex("Fourier") + fourier_part.set_color(YELLOW) + F_sym = fourier_part[0] + name.match_width(image) + name.next_to(image, DOWN) + + self.play( + ReplacementTransform(v_line, frame), + FadeIn(image), + FadeIn(name[0]), + *[ + ReplacementTransform( + title[0].deepcopy(), + name[1] + ) + for title in titles + ], + titles.scale, 0.65, + titles.arrange, DOWN, + titles.next_to, image, UP, + ) + self.wait() + + big_F = F_sym.copy() + big_F.set_fill(opacity=0) + big_F.set_stroke(WHITE, 2) + big_F.set_height(3) + big_F.move_to(midpoint( + image.get_right(), + RIGHT_SIDE, + )) + big_F.shift(DOWN) + equivalence = VGroup( + fourier_part.copy().scale(1.25), + TexMobject("\\Leftrightarrow").scale(1.5), + TextMobject("Break down into\\\\pure frequencies"), + ) + equivalence.arrange(RIGHT) + equivalence.move_to(big_F) + equivalence.to_edge(UP) + + self.play( + FadeIn(big_F), + TransformFromCopy(fourier_part, equivalence[0]), + Write(equivalence[1:]), + ) + self.wait(3) + self.play(FadeOut(VGroup(big_F, equivalence))) + + self.image = image + self.name = name + + def show_paper(self): + image = self.image + paper = ImageMobject("Fourier paper") + paper.match_height(image) + paper.next_to(image, RIGHT, MED_LARGE_BUFF) + + date = TexMobject("1822") + date.next_to(paper, DOWN) + date_rect = SurroundingRectangle(date) + date_rect.scale(0.3) + date_rect.set_color(RED) + date_rect.shift(1.37 * UP + 0.08 * LEFT) + date_arrow = Arrow( + date_rect.get_bottom(), + date.get_top(), + buff=SMALL_BUFF, + color=date_rect.get_color(), + ) + + heat_rect = SurroundingRectangle( + TextMobject("CHALEUR") + ) + heat_rect.set_color(RED) + heat_rect.scale(0.6) + heat_rect.move_to( + paper.get_top() + + 1.22 * DOWN + 0.37 * RIGHT + ) + heat_word = TextMobject("Heat") + heat_word.scale(1.5) + heat_word.next_to(paper, UP) + heat_word.shift(paper.get_width() * RIGHT) + heat_arrow = Arrow( + heat_rect.get_top(), + heat_word.get_left(), + buff=0.1, + path_arc=-60 * DEGREES, + color=heat_rect.get_color(), + ) + + self.play(FadeInFrom(paper, LEFT)) + self.play( + ShowCreation(date_rect), + ) + self.play( + GrowFromPoint(date, date_arrow.get_start()), + ShowCreation(date_arrow), + ) + self.wait(3) + + # Insert animation of circles/sine waves + # approximating a square wave + + self.play( + ShowCreation(heat_rect), + ) + self.play( + GrowFromPoint(heat_word, heat_arrow.get_start()), + ShowCreation(heat_arrow), + ) + self.wait(3) + + +class ManyCousinsOfFourierThings(Scene): + def construct(self): + series_variants = VGroup( + TextMobject("Complex", "Fourier Series"), + TextMobject("Discrete", "Fourier Series"), + ) + transform_variants = VGroup( + TextMobject("Discrete", "Fourier Transform"), + TextMobject("Fast", "Fourier Transform"), + TextMobject("Quantum", "Fourier Transform"), + ) + groups = VGroup(series_variants, transform_variants) + for group, vect in zip(groups, [LEFT, RIGHT]): + group.scale(0.7) + group.arrange(DOWN, aligned_edge=LEFT) + group.move_to( + vect * FRAME_WIDTH / 4 + ) + group.set_color(YELLOW) + + self.play(*[ + LaggedStartMap(FadeIn, group) + for group in groups + ]) + self.play(*[ + LaggedStartMap(FadeOut, group) + for group in groups + ]) + + +class FourierSeriesIllustraiton(Scene): + CONFIG = { + "n_range": range(1, 31, 2), + "axes_config": { + "number_line_config": { + "include_tip": False, + }, + "x_axis_config": { + "tick_frequency": 1 / 4, + "unit_size": 4, + }, + "x_min": 0, + "x_max": 1, + "y_min": -1, + "y_max": 1, + } + } + + def construct(self): + n_range = self.n_range + + axes1 = Axes(**self.axes_config) + axes1.x_axis.add_numbers( + 0.5, 1, + number_config={"num_decimal_places": 1} + ) + axes2 = axes1.copy() + target_func_graph = self.get_target_func_graph(axes2) + axes2.add(target_func_graph) + + arrow = Arrow(LEFT, RIGHT, color=WHITE) + group = VGroup(axes1, arrow, axes2) + group.arrange(RIGHT, buff=LARGE_BUFF) + group.shift(2 * UP) + group.shift_onto_screen() + + sine_graphs = VGroup(*[ + axes1.get_graph(self.generate_nth_func(n)) + for n in n_range + ]) + sine_graphs.set_stroke(width=3) + sine_graphs.set_color_by_gradient( + BLUE, GREEN, RED, YELLOW, PINK, + BLUE, GREEN, RED, YELLOW, PINK, + ) + + partial_sums = VGroup(*[ + axes1.get_graph(self.generate_kth_partial_sum_func(k + 1)) + for k in range(len(n_range)) + ]) + partial_sums.match_style(sine_graphs) + + sum_tex = self.get_sum_tex() + sum_tex.next_to(axes1, DOWN, LARGE_BUFF) + sum_tex.shift(RIGHT) + eq = TexMobject("=") + target_func_tex = self.get_target_func_tex() + target_func_tex.next_to(axes2, DOWN) + target_func_tex.match_y(sum_tex) + eq.move_to(midpoint( + target_func_tex.get_left(), + sum_tex.get_right() + )) + + range_words = TextMobject( + "For $0 \\le x \\le 1$" + ) + range_words.next_to( + VGroup(sum_tex, target_func_tex), + DOWN, + ) + + rects = it.chain( + [ + SurroundingRectangle(piece) + for piece in self.get_sum_tex_pieces(sum_tex) + ], + it.cycle([None]) + ) + + self.add(axes1, arrow, axes2) + self.add(target_func_graph) + self.add(sum_tex, eq, target_func_tex) + self.add(range_words) + + curr_partial_sum = axes1.get_graph(lambda x: 0) + curr_partial_sum.set_stroke(width=1) + for sine_graph, partial_sum, rect in zip(sine_graphs, partial_sums, rects): + anims1 = [ + ShowCreation(sine_graph) + ] + partial_sum.set_stroke(BLACK, 4, background=True) + anims2 = [ + curr_partial_sum.set_stroke, + {"width": 1, "opacity": 0.5}, + curr_partial_sum.set_stroke, + {"width": 0, "background": True}, + ReplacementTransform( + sine_graph, partial_sum, + remover=True + ), + ] + if rect: + rect.match_style(sine_graph) + anims1.append(ShowCreation(rect)) + anims2.append(FadeOut(rect)) + self.play(*anims1) + self.play(*anims2) + curr_partial_sum = partial_sum + + def get_sum_tex(self): + return TexMobject( + "\\frac{4}{\\pi} \\left(", + "\\frac{\\cos(\\pi x)}{1}", + "-\\frac{\\cos(3\\pi x)}{3}", + "+\\frac{\\cos(5\\pi x)}{5}", + "- \\cdots \\right)" + ).scale(0.75) + + def get_sum_tex_pieces(self, sum_tex): + return sum_tex[1:4] + + def get_target_func_tex(self): + step_tex = TexMobject( + """ + 1 \\quad \\text{if $x < 0.5$} \\\\ + 0 \\quad \\text{if $x = 0.5$} \\\\ + -1 \\quad \\text{if $x > 0.5$} \\\\ + """ + ) + lb = Brace(step_tex, LEFT, buff=SMALL_BUFF) + step_tex.add(lb) + return step_tex + + def get_target_func_graph(self, axes): + step_func = axes.get_graph( + lambda x: (1 if x < 0.5 else -1), + discontinuities=[0.5], + color=YELLOW, + stroke_width=3, + ) + dot = Dot(axes.c2p(0.5, 0), color=step_func.get_color()) + dot.scale(0.5) + step_func.add(dot) + return step_func + + # def generate_nth_func(self, n): + # return lambda x: (4 / n / PI) * np.sin(TAU * n * x) + + def generate_nth_func(self, n): + return lambda x: np.prod([ + (4 / PI), + (1 / n) * (-1)**((n - 1) / 2), + np.cos(PI * n * x) + ]) + + def generate_kth_partial_sum_func(self, k): + return lambda x: np.sum([ + self.generate_nth_func(n)(x) + for n in self.n_range[:k] + ]) + + +class FourierSeriesOfLineIllustration(FourierSeriesIllustraiton): + CONFIG = { + "n_range": range(1, 31, 2), + "axes_config": { + "y_axis_config": { + "unit_size": 2, + "tick_frequency": 0.25, + "numbers_with_elongated_ticks": [-1, 1], + } + } + } + + def get_sum_tex(self): + return TexMobject( + "\\frac{8}{\\pi^2} \\left(", + "\\frac{\\cos(\\pi x)}{1^2}", + "+\\frac{\\cos(3\\pi x)}{3^2}", + "+\\frac{\\cos(5\\pi x)}{5^2}", + "+ \\cdots \\right)" + ).scale(0.75) + + # def get_sum_tex_pieces(self, sum_tex): + # return sum_tex[1:4] + + def get_target_func_tex(self): + result = TexMobject("1 - 2x") + result.scale(1.5) + point = VectorizedPoint() + point.next_to(result, RIGHT, 1.5 * LARGE_BUFF) + # result.add(point) + return result + + def get_target_func_graph(self, axes): + return axes.get_graph( + lambda x: 1 - 2 * x, + color=YELLOW, + stroke_width=3, + ) + + # def generate_nth_func(self, n): + # return lambda x: (4 / n / PI) * np.sin(TAU * n * x) + + def generate_nth_func(self, n): + return lambda x: np.prod([ + (8 / PI**2), + (1 / n**2), + np.cos(PI * n * x) + ]) + + +class CircleAnimationOfF(FourierOfTrebleClef): + CONFIG = { + "height": 3, + "n_circles": 200, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + path = VMobject() + shape = TextMobject("F") + for sp in shape.family_members_with_points(): + path.append_points(sp.points) + return path + + +class ExponentialDecay(PiCreatureScene): + def construct(self): + k = 0.2 + mk_tex = "-0.2" + mk_tex_color = GREEN + t2c = {mk_tex: mk_tex_color} + + # Pi creature + randy = self.pi_creature + randy.flip() + randy.set_height(2.5) + randy.move_to(3 * RIGHT) + randy.to_edge(DOWN) + bubble = ThoughtBubble( + direction=LEFT, + height=3.5, + width=3, + ) + bubble.pin_to(randy) + bubble.set_fill(DARKER_GREY) + exp = TexMobject( + "Ce^{", mk_tex, "t}", + tex_to_color_map=t2c, + ) + exp.move_to(bubble.get_bubble_center()) + + # Setup axes + axes = Axes( + x_min=0, + x_max=13, + y_min=-4, + y_max=4, + ) + axes.set_stroke(width=2) + axes.set_color(LIGHT_GREY) + axes.scale(0.9) + axes.to_edge(LEFT, buff=LARGE_BUFF) + axes.x_axis.add_numbers() + axes.y_axis.add_numbers() + axes.y_axis.add_numbers(0) + axes.x_axis.add( + TextMobject("Time").next_to( + axes.x_axis.get_end(), DR, + ) + ) + axes.y_axis.add( + TexMobject("f").next_to( + axes.y_axis.get_corner(UR), RIGHT, + ).set_color(YELLOW) + ) + axes.x_axis.set_opacity(0) + + # Value trackers + y_tracker = ValueTracker(3) + x_tracker = ValueTracker(0) + dydt_tracker = ValueTracker() + dxdt_tracker = ValueTracker(0) + self.add( + y_tracker, x_tracker, + dydt_tracker, dxdt_tracker, + ) + + get_y = y_tracker.get_value + get_x = x_tracker.get_value + get_dydt = dydt_tracker.get_value + get_dxdt = dxdt_tracker.get_value + + dydt_tracker.add_updater(lambda m: m.set_value( + - k * get_y() + )) + y_tracker.add_updater(lambda m, dt: m.increment_value( + dt * get_dydt() + )) + x_tracker.add_updater(lambda m, dt: m.increment_value( + dt * get_dxdt() + )) + + # Tip/decimal + tip = ArrowTip(color=YELLOW) + tip.set_width(0.25) + tip.add_updater(lambda m: m.move_to( + axes.c2p(get_x(), get_y()), LEFT + )) + decimal = DecimalNumber() + decimal.add_updater(lambda d: d.set_value(get_y())) + decimal.add_updater(lambda d: d.next_to( + tip, RIGHT, + SMALL_BUFF, + )) + + # Rate of change arrow + arrow = Vector( + DOWN, color=RED, + max_stroke_width_to_length_ratio=50, + max_tip_length_to_length_ratio=0.2, + ) + arrow.set_stroke(width=4) + arrow.add_updater(lambda m: m.scale( + 2.5 * abs(get_dydt()) / m.get_length() + )) + arrow.add_updater(lambda m: m.move_to( + tip.get_left(), UP + )) + + # Graph + graph = TracedPath(tip.get_left) + + # Equation + ode = TexMobject( + "{d{f} \\over dt}(t)", + "=", mk_tex, "\\cdot {f}(t)", + tex_to_color_map={ + "{f}": YELLOW, + "=": WHITE, + mk_tex: mk_tex_color + } + ) + ode.to_edge(UP) + dfdt = ode[:3] + ft = ode[-2:] + + self.add(axes) + self.add(tip) + self.add(decimal) + self.add(arrow) + self.add(randy) + self.add(ode) + + # Show rate of change dependent on itself + rect = SurroundingRectangle(dfdt) + self.play(ShowCreation(rect)) + self.wait() + self.play( + Transform( + rect, + SurroundingRectangle(ft) + ) + ) + self.wait(3) + + # Show graph over time + self.play( + DrawBorderThenFill(bubble), + Write(exp), + FadeOut(rect), + randy.change, "thinking", + ) + axes.x_axis.set_opacity(1) + self.play( + y_tracker.set_value, 3, + ShowCreation(axes.x_axis), + ) + dxdt_tracker.set_value(1) + self.add(graph) + randy.add_updater(lambda r: r.look_at(tip)) + self.wait(4) + + # Show derivative of exponential + eq = TexMobject("=") + eq.next_to(ode.get_part_by_tex("="), DOWN, LARGE_BUFF) + exp.generate_target() + exp.target.next_to(eq, LEFT) + d_dt = TexMobject("{d \\over dt}") + d_dt.next_to(exp.target, LEFT) + const = TexMobject(mk_tex) + const.set_color(mk_tex_color) + dot = TexMobject("\\cdot") + const.next_to(eq, RIGHT) + dot.next_to(const, RIGHT, 2 * SMALL_BUFF) + exp_copy = exp.copy() + exp_copy.next_to(dot, RIGHT, 2 * SMALL_BUFF) + VGroup(const, dot, eq).align_to(exp_copy, DOWN) + + self.play( + MoveToTarget(exp), + FadeOut(bubble), + FadeIn(d_dt), + FadeIn(eq), + ) + self.wait(2) + self.play( + ApplyMethod( + exp[1].copy().replace, + const[0], + ) + ) + self.wait() + rect = SurroundingRectangle(exp) + rect.set_stroke(BLUE, 2) + self.play(FadeIn(rect)) + self.play( + Write(dot), + TransformFromCopy(exp, exp_copy), + rect.move_to, exp_copy + ) + self.play(FadeOut(rect)) + self.wait(5) + + +class InvestmentGrowth(Scene): + CONFIG = { + "output_tex": "{M}", + "output_color": GREEN, + "initial_value": 1, + "initial_value_tex": "{M_0}", + "k": 0.05, + "k_tex": "0.05", + "total_time": 43, + "time_rate": 3, + } + + def construct(self): + # Axes + axes = Axes( + x_min=0, + x_max=self.total_time, + y_min=0, + y_max=6, + x_axis_config={ + "unit_size": 0.3, + "tick_size": 0.05, + "numbers_with_elongated_ticks": range( + 0, self.total_time, 5 + ) + } + ) + axes.to_corner(DL, buff=LARGE_BUFF) + + time_label = TextMobject("Time") + time_label.next_to( + axes.x_axis.get_right(), + UP, MED_LARGE_BUFF + ) + time_label.shift_onto_screen() + axes.x_axis.add(time_label) + money_label = TexMobject(self.output_tex) + money_label.set_color(self.output_color) + money_label.next_to( + axes.y_axis.get_top(), + UP, + ) + axes.y_axis.add(money_label) + + # Graph + graph = axes.get_graph( + lambda x: self.initial_value * np.exp(self.k * x) + ) + graph.set_color(self.output_color) + full_graph = graph.copy() + time_tracker = self.get_time_tracker() + graph.add_updater(lambda m: m.pointwise_become_partial( + full_graph, 0, + np.clip( + time_tracker.get_value() / self.total_time, + 0, 1, + ) + )) + + # Equation + tex_kwargs = { + "tex_to_color_map": { + self.output_tex: self.output_color, + self.initial_value_tex: BLUE, + } + } + ode = TexMobject( + "{d", + "\\over dt}", + self.output_tex, + "(t)", + "=", self.k_tex, + "\\cdot", self.output_tex, "(t)", + **tex_kwargs + ) + ode.to_edge(UP) + exp = TexMobject( + self.output_tex, + "(t) =", self.initial_value_tex, + "e^{", self.k_tex, "t}", + **tex_kwargs + ) + exp.next_to(ode, DOWN, LARGE_BUFF) + + M0_part = exp.get_part_by_tex(self.initial_value_tex) + exp_part = exp[-3:] + M0_label = M0_part.copy() + M0_label.next_to( + axes.c2p(0, self.initial_value), + LEFT + ) + M0_part.set_opacity(0) + exp_part.save_state() + exp_part.align_to(M0_part, LEFT) + + self.add(axes) + self.add(graph) + self.add(time_tracker) + + self.play(FadeInFromDown(ode)) + self.wait(6) + self.play(FadeInFrom(exp, UP)) + self.wait(2) + self.play( + Restore(exp_part), + M0_part.set_opacity, 1, + ) + self.play(TransformFromCopy( + M0_part, M0_label + )) + self.wait(5) + + def get_time_tracker(self): + time_tracker = ValueTracker(0) + time_tracker.add_updater( + lambda m, dt: m.increment_value( + self.time_rate * dt + ) + ) + return time_tracker + + +class GrowingPileOfMoney(InvestmentGrowth): + CONFIG = { + "total_time": 60 + } + + def construct(self): + initial_count = 5 + k = self.k + total_time = self.total_time + + time_tracker = self.get_time_tracker() + + final_count = initial_count * np.exp(k * total_time) + dollar_signs = VGroup(*[ + TexMobject("\\$") + for x in range(int(final_count)) + ]) + dollar_signs.set_color(GREEN) + for ds in dollar_signs: + ds.shift( + 3 * np.random.random(3) + ) + dollar_signs.center() + dollar_signs.sort(get_norm) + dollar_signs.set_stroke(BLACK, 3, background=True) + + def update_dollar_signs(group): + t = time_tracker.get_value() + count = initial_count * np.exp(k * t) + alpha = count / final_count + n, sa = integer_interpolate(0, len(dollar_signs), alpha) + group.set_opacity(1) + group[n:].set_opacity(0) + group[n].set_opacity(sa) + + dollar_signs.add_updater(update_dollar_signs) + + self.add(time_tracker) + self.add(dollar_signs) + self.wait(20) + + +class CarbonDecayCurve(InvestmentGrowth): + CONFIG = { + "output_tex": "{^{14}C}", + "output_color": GOLD, + "initial_value": 4, + "initial_value_tex": "{^{14}C_0}", + "k": -0.1, + "k_tex": "-k", + "time_rate": 6, + } + + +class CarbonDecayingInMammoth(Scene): + def construct(self): + mammoth = SVGMobject("Mammoth") + mammoth.set_color( + interpolate_color(GREY_BROWN, WHITE, 0.25) + ) + mammoth.set_height(4) + body = mammoth[9] + + atoms = VGroup(*[ + self.get_atom(body) + for n in range(600) + ]) + + p_decay = 0.2 + + def update_atoms(group, dt): + for atom in group: + if np.random.random() < dt * p_decay: + group.remove(atom) + return group + atoms.add_updater(update_atoms) + + self.add(mammoth) + self.add(atoms) + self.wait(20) + + def get_atom(self, body): + atom = Dot(color=GOLD) + atom.set_height(0.05) + + dl = body.get_corner(DL) + ur = body.get_corner(UR) + + wn = 0 + while wn == 0: + point = np.array([ + interpolate(dl[0], ur[0], np.random.random()), + interpolate(dl[1], ur[1], np.random.random()), + 0 + ]) + wn = get_winding_number([ + body.point_from_proportion(a) - point + for a in np.linspace(0, 1, 300) + ]) + wn = int(np.round(wn)) + + atom.move_to(point) + return atom + + +class BoundaryConditionInterlude(Scene): + def construct(self): + background = FullScreenFadeRectangle( + fill_color=DARK_GREY + ) + storyline = self.get_main_storyline() + storyline.generate_target() + v_shift = 2 * DOWN + storyline.target.shift(v_shift) + im_to_im = storyline[1].get_center() - storyline[0].get_center() + + bc_image = self.get_labeled_image( + "Boundary\\\\conditions", + "boundary_condition_thumbnail" + ) + bc_image.next_to(storyline[1], UP) + new_arrow0 = Arrow( + storyline.arrows[0].get_start() + v_shift, + bc_image.get_left() + SMALL_BUFF * LEFT, + path_arc=-90 * DEGREES, + buff=0, + ) + new_mid_arrow = Arrow( + bc_image.get_bottom(), + storyline[1].get_top() + v_shift, + buff=SMALL_BUFF, + path_arc=60 * DEGREES, + ) + + # BC detour + self.add(background) + self.add(storyline[0]) + for im1, im2, arrow in zip(storyline, storyline[1:], storyline.arrows): + self.add(im2, im1) + self.play( + FadeInFrom(im2, -im_to_im), + ShowCreation(arrow), + ) + self.wait() + self.play( + GrowFromCenter(bc_image), + MoveToTarget(storyline), + Transform( + storyline.arrows[0], + new_arrow0, + ), + MaintainPositionRelativeTo( + storyline.arrows[1], + storyline, + ), + ) + self.play(ShowCreation(new_mid_arrow)) + self.wait() + + # From BC to next step + rect1 = bc_image[2].copy() + rect2 = storyline[1][2].copy() + rect3 = storyline[2][2].copy() + for rect in rect1, rect2, rect3: + rect.set_stroke(YELLOW, 3) + + self.play(FadeIn(rect1)) + kw = {"path_arc": 60 * DEGREES} + self.play( + LaggedStart( + Transform(rect1, rect2, **kw), + # TransformFromCopy(rect1, rect3, **kw), + lag_ratio=0.4, + ) + ) + self.play( + FadeOut(rect1), + # FadeOut(rect3), + ) + + # Reorganize images + im1, im3, im4 = storyline + im2 = bc_image + l_group = Group(im1, im2) + r_group = Group(im3, im4) + for group in l_group, r_group: + group.generate_target() + group.target.arrange(DOWN, buff=LARGE_BUFF) + group.target.center() + + l_group.target.to_edge(LEFT) + r_group.target.move_to( + FRAME_WIDTH * RIGHT / 4 + ) + brace = Brace(r_group.target, LEFT) + nv_text = brace.get_text("Next\\\\video") + nv_text.scale(1.5, about_edge=RIGHT) + nv_text.set_color(YELLOW) + brace.set_color(YELLOW) + + arrows = VGroup( + storyline.arrows, + new_mid_arrow, + ) + + self.play( + LaggedStart( + MoveToTarget(l_group), + MoveToTarget(r_group), + lag_ratio=0.3, + ), + FadeOut(arrows), + ) + self.play( + GrowFromCenter(brace), + FadeInFrom(nv_text, RIGHT) + ) + self.wait() + + def get_main_storyline(self): + images = Group( + self.get_sine_curve_image(), + self.get_linearity_image(), + self.get_fourier_series_image(), + ) + for image in images: + image.set_height(3) + images.arrange(RIGHT, buff=1) + images.set_width(FRAME_WIDTH - 1) + + arrows = VGroup() + for im1, im2 in zip(images, images[1:]): + arrow = Arrow( + im1.get_top(), + im2.get_top(), + color=WHITE, + buff=MED_SMALL_BUFF, + path_arc=-120 * DEGREES, + rectangular_stem_width=0.025, + ) + arrow.scale(0.7, about_edge=DOWN) + arrows.add(arrow) + images.arrows = arrows + + return images + + def get_sine_curve_image(self): + return self.get_labeled_image( + "Sine curves", + "sine_curve_temp_graph", + ) + + def get_linearity_image(self): + return self.get_labeled_image( + "Linearity", + "linearity_thumbnail", + ) + + def get_fourier_series_image(self): + return self.get_labeled_image( + "Fourier series", + "fourier_series_thumbnail", + ) + + def get_labeled_image(self, text, image_file): + rect = ScreenRectangle(height=2) + border = rect.copy() + rect.set_fill(BLACK, 1) + rect.set_stroke(WHITE, 0) + border.set_stroke(WHITE, 2) + + text_mob = TextMobject(text) + text_mob.set_stroke(BLACK, 5, background=True) + text_mob.next_to(rect.get_top(), DOWN, SMALL_BUFF) + + image = ImageMobject(image_file) + image.replace(rect, dim_to_match=1) + image.scale(0.8, about_edge=DOWN) + + return Group(rect, image, border, text_mob) + + +class GiantCross(Scene): + def construct(self): + rect = FullScreenFadeRectangle() + cross = Cross(rect) + cross.set_stroke(RED, 25) + + words = TextMobject("This wouldn't\\\\happen!") + words.scale(2) + words.set_color(RED) + words.to_edge(UP) + + self.play( + FadeInFromDown(words), + ShowCreation(cross), + ) + self.wait() + + +class EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "1stViewMaths", + "Adrian Robinson", + "Alexis Olson", + "Andreas Benjamin Brössel", + "Andrew Busey", + "Ankalagon", + "Antoine Bruguier", + "Antonio Juarez", + "Arjun Chakroborty", + "Art Ianuzzi", + "Awoo", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Bernd Sing", + "Boris Veselinovich", + "Brian Staroselsky", + "Charles Southerland", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Cooper Jones", + "Danger Dai", + "Daniel Pang", + "Dave B", + "Dave Kester", + "David B. Hill", + "David Clark", + "Delton Ding", + "eaglle", + "Empirasign", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Fernando Via Canel", + "Giovanni Filippi", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Isaac Jeffrey Lee", + "j eduardo perez", + "Jacob Hartmann", + "Jacob Magnuson", + "Jameel Syed", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "John C. Vesey", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Kai-Siang Ang", + "Kanan Gill", + "Kartik\\\\Cating-Subramanian", + "L0j1k", + "Lee Redden", + "Linh Tran", + "Ludwig Schubert", + "Magister Mugit", + "Mark B Bahu", + "Martin Price", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Bouchard", + "Matthew Cocke", + "Michael Faust", + "Michael Hardel", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nikita Lesnikov", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick", + "Peter Ehrnstrom", + "RedAgent14", + "rehmi post", + "Richard Comish", + "Ripta Pasay", + "Rish Kundalia", + "Robert Teed", + "Roobie", + "Ryan Williams", + "Sebastian Garcia", + "Solara570", + "Stephan Arlinghaus", + "Steven Siddals", + "Stevie Metke", + "Tal Einav", + "Ted Suzman", + "Thomas Tarler", + "Tianyu Ge", + "Tom Fleming", + "Valeriy Skobelev", + "Xuanji Li", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Zach Cardwell", + "Luc Ritchie", + "Britt Selvitelle", + "David Gow", + "J", + "Jonathan Wilson", + "Joseph John Cox", + "Magnus Dahlström", + "Randy C. Will", + "Ryan Atallah", + "Lukas -krtek.net- Novy", + "Jordan Scales", + "Ali Yahya", + "Arthur Zey", + "Atul S", + "dave nicponski", + "Evan Phillips", + "Joseph Kelly", + "Kaustuv DeBiswas", + "Lambda AI Hardware", + "Lukas Biewald", + "Mark Heising", + "Mike Coleman", + "Nicholas Cahill", + "Peter Mcinerney", + "Quantopian", + "Roy Larson", + "Scott Walter, Ph.D.", + "Yana Chernobilsky", + "Yu Jun", + "D. Sivakumar", + "Richard Barthel", + "Burt Humburg", + "Matt Russell", + "Scott Gray", + "soekul", + "Tihan Seale", + "Juan Benet", + "Vassili Philippov", + "Kurt Dicus", + ], + } diff --git a/active_projects/diffyq/part3/temperature_graphs.py b/active_projects/diffyq/part3/temperature_graphs.py new file mode 100644 index 00000000..2ed445b5 --- /dev/null +++ b/active_projects/diffyq/part3/temperature_graphs.py @@ -0,0 +1,2830 @@ +from scipy import integrate + +from manimlib.imports import * +from active_projects.ode.part2.heat_equation import * + + +class TemperatureGraphScene(SpecialThreeDScene): + CONFIG = { + "axes_config": { + "x_min": 0, + "x_max": TAU, + "y_min": 0, + "y_max": 10, + "z_min": -3, + "z_max": 3, + "x_axis_config": { + "tick_frequency": TAU / 8, + "include_tip": False, + }, + "num_axis_pieces": 1, + }, + "default_graph_style": { + "stroke_width": 2, + "stroke_color": WHITE, + "background_image_file": "VerticalTempGradient", + }, + "default_surface_config": { + "fill_opacity": 0.1, + "checkerboard_colors": [LIGHT_GREY], + "stroke_width": 0.5, + "stroke_color": WHITE, + "stroke_opacity": 0.5, + }, + "temp_text": "Temperature", + } + + def get_three_d_axes(self, include_labels=True, include_numbers=False, **kwargs): + config = dict(self.axes_config) + config.update(kwargs) + axes = ThreeDAxes(**config) + axes.set_stroke(width=2) + + if include_numbers: + self.add_axes_numbers(axes) + + if include_labels: + self.add_axes_labels(axes) + + # Adjust axis orientation + axes.x_axis.rotate( + 90 * DEGREES, RIGHT, + about_point=axes.c2p(0, 0, 0), + ) + axes.y_axis.rotate( + 90 * DEGREES, UP, + about_point=axes.c2p(0, 0, 0), + ) + + # Add xy-plane + input_plane = self.get_surface( + axes, lambda x, t: 0 + ) + input_plane.set_style( + fill_opacity=0.5, + fill_color=BLUE_B, + stroke_width=0.5, + stroke_color=WHITE, + ) + + axes.input_plane = input_plane + + return axes + + def add_axes_numbers(self, axes): + x_axis = axes.x_axis + y_axis = axes.y_axis + tex_vals = [ + ("\\pi \\over 2", TAU / 4), + ("\\pi", TAU / 2), + ("3\\pi \\over 2", 3 * TAU / 4), + ("2\\pi", TAU) + ] + x_labels = VGroup() + for tex, val in tex_vals: + label = TexMobject(tex) + label.scale(0.5) + label.next_to(x_axis.n2p(val), DOWN) + x_labels.add(label) + x_axis.add(x_labels) + x_axis.numbers = x_labels + + y_axis.add_numbers() + for number in y_axis.numbers: + number.rotate(90 * DEGREES) + return axes + + def add_axes_labels(self, axes): + x_label = TexMobject("x") + x_label.next_to(axes.x_axis.get_end(), RIGHT) + axes.x_axis.label = x_label + + t_label = TextMobject("Time") + t_label.rotate(90 * DEGREES, OUT) + t_label.next_to(axes.y_axis.get_end(), UP) + axes.y_axis.label = t_label + + temp_label = TextMobject(self.temp_text) + temp_label.rotate(90 * DEGREES, RIGHT) + temp_label.next_to(axes.z_axis.get_zenith(), RIGHT) + axes.z_axis.label = temp_label + for axis in axes: + axis.add(axis.label) + return axes + + def get_time_slice_graph(self, axes, func, t, **kwargs): + config = dict() + config.update(self.default_graph_style) + config.update({ + "t_min": axes.x_min, + "t_max": axes.x_max, + }) + config.update(kwargs) + return ParametricFunction( + lambda x: axes.c2p( + x, t, func(x, t) + ), + **config, + ) + + def get_initial_state_graph(self, axes, func, **kwargs): + return self.get_time_slice_graph( + axes, + lambda x, t: func(x), + t=0, + **kwargs + ) + + def get_surface(self, axes, func, **kwargs): + config = { + "u_min": axes.x_min, + "u_max": axes.x_max, + "v_min": axes.y_min, + "v_max": axes.y_max, + "resolution": ( + (axes.x_max - axes.x_min) // axes.x_axis.tick_frequency, + (axes.y_max - axes.y_min) // axes.y_axis.tick_frequency, + ), + } + config.update(self.default_surface_config) + config.update(kwargs) + return ParametricSurface( + lambda x, t: axes.c2p( + x, t, func(x, t) + ), + **config + ) + + def orient_three_d_mobject(self, mobject, + phi=85 * DEGREES, + theta=-80 * DEGREES): + mobject.rotate(-90 * DEGREES - theta, OUT) + mobject.rotate(phi, LEFT) + return mobject + + def get_rod_length(self): + return self.axes_config["x_max"] + + def get_const_time_plane(self, axes): + t_tracker = ValueTracker(0) + plane = Polygon( + *[ + axes.c2p(x, 0, z) + for x, z in [ + (axes.x_min, axes.z_min), + (axes.x_max, axes.z_min), + (axes.x_max, axes.z_max), + (axes.x_min, axes.z_max), + ] + ], + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.2 + ) + plane.add_updater(lambda m: m.move_to( + axes.c2p( + axes.x_min, + t_tracker.get_value(), + axes.z_min, + ), + IN + LEFT, + )) + plane.t_tracker = t_tracker + return plane + + +class SimpleCosExpGraph(TemperatureGraphScene): + def construct(self): + axes = self.get_three_d_axes() + cos_graph = self.get_cos_graph(axes) + cos_exp_surface = self.get_cos_exp_surface(axes) + + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + ) + self.camera.frame_center.shift(3 * RIGHT) + self.begin_ambient_camera_rotation(rate=0.01) + + self.add(axes) + self.play(ShowCreation(cos_graph)) + self.play(UpdateFromAlphaFunc( + cos_exp_surface, + lambda m, a: m.become( + self.get_cos_exp_surface(axes, v_max=a * 10) + ), + run_time=3 + )) + + self.add(cos_graph.copy()) + + t_tracker = ValueTracker(0) + get_t = t_tracker.get_value + cos_graph.add_updater( + lambda m: m.become(self.get_time_slice_graph( + axes, + lambda x: self.cos_exp(x, get_t()), + t=get_t() + )) + ) + + plane = Rectangle( + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.1, + ) + plane.rotate(90 * DEGREES, RIGHT) + plane.match_width(axes.x_axis) + plane.match_depth(axes.z_axis, stretch=True) + plane.move_to(axes.c2p(0, 0, 0), LEFT) + + self.add(plane, cos_graph) + self.play( + ApplyMethod( + t_tracker.set_value, 10, + run_time=10, + rate_func=linear, + ), + ApplyMethod( + plane.shift, 10 * UP, + run_time=10, + rate_func=linear, + ), + VFadeIn(plane), + ) + self.wait(10) + + # + def cos_exp(self, x, t, A=2, omega=1.5, k=0.1): + return A * np.cos(omega * x) * np.exp(-k * (omega**2) * t) + + def get_cos_graph(self, axes, **config): + return self.get_initial_state_graph( + axes, + lambda x: self.cos_exp(x, 0), + **config + ) + + def get_cos_exp_surface(self, axes, **config): + return self.get_surface( + axes, + lambda x, t: self.cos_exp(x, t), + **config + ) + + +class AddMultipleSolutions(SimpleCosExpGraph): + CONFIG = { + "axes_config": { + "x_axis_config": { + "unit_size": 0.7, + }, + } + } + + def construct(self): + axes1, axes2, axes3 = all_axes = VGroup(*[ + self.get_three_d_axes( + include_labels=False, + ) + for x in range(3) + ]) + all_axes.scale(0.5) + self.orient_three_d_mobject(all_axes) + + As = [1.5, 1.5] + omegas = [1.5, 2.5] + ks = [0.1, 0.1] + quads = [ + (axes1, [As[0]], [omegas[0]], [ks[0]]), + (axes2, [As[1]], [omegas[1]], [ks[1]]), + (axes3, As, omegas, ks), + ] + + for axes, As, omegas, ks in quads: + graph = self.get_initial_state_graph( + axes, + lambda x: np.sum([ + self.cos_exp(x, 0, A, omega, k) + for A, omega, k in zip(As, omegas, ks) + ]) + ) + surface = self.get_surface( + axes, + lambda x, t: np.sum([ + self.cos_exp(x, t, A, omega, k) + for A, omega, k in zip(As, omegas, ks) + ]) + ) + surface.sort(lambda p: -p[2]) + + axes.add(surface, graph) + axes.graph = graph + axes.surface = surface + + self.set_camera_orientation(distance=100) + plus = TexMobject("+").scale(2) + equals = TexMobject("=").scale(2) + group = VGroup( + axes1, plus, axes2, equals, axes3, + ) + group.arrange(RIGHT, buff=SMALL_BUFF) + + for axes in all_axes: + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.scale(2) + checkmark.next_to(axes, UP) + checkmark.shift(0.7 * DOWN) + axes.checkmark = checkmark + + self.add(axes1, axes2) + self.play( + LaggedStart( + Write(axes1.surface), + Write(axes2.surface), + ), + LaggedStart( + FadeInFrom(axes1.checkmark, DOWN), + FadeInFrom(axes2.checkmark, DOWN), + ), + lag_ratio=0.2, + run_time=1, + ) + self.wait() + self.play(Write(plus)) + self.play( + Transform( + axes1.copy().set_fill(opacity=0), + axes3 + ), + Transform( + axes2.copy().set_fill(opacity=0), + axes3 + ), + FadeInFrom(equals, LEFT) + ) + self.play( + FadeInFrom(axes3.checkmark, DOWN), + ) + self.wait() + + +class BreakDownAFunction(SimpleCosExpGraph): + CONFIG = { + "axes_config": { + "z_axis_config": { + "unit_size": 0.75, + "include_tip": False, + }, + "z_min": -2, + "y_max": 20, + }, + "n_low_axes": 4, + "k": 0.2, + } + + def construct(self): + self.set_camera_orientation(distance=100) + self.set_axes() + self.setup_graphs() + self.show_break_down() + self.show_solutions_for_waves() + + def set_axes(self): + top_axes = self.get_three_d_axes() + top_axes.z_axis.label.next_to( + top_axes.z_axis.get_end(), OUT, SMALL_BUFF + ) + top_axes.y_axis.set_opacity(0) + self.orient_three_d_mobject(top_axes) + top_axes.y_axis.label.rotate(-10 * DEGREES, UP) + top_axes.scale(0.75) + top_axes.center() + top_axes.to_edge(UP) + + low_axes = self.get_three_d_axes( + z_min=-3, + z_axis_config={"unit_size": 1} + ) + low_axes.y_axis.set_opacity(0) + for axis in low_axes: + axis.label.fade(1) + # low_axes.add(low_axes.input_plane) + # low_axes.input_plane.set_opacity(0) + + self.orient_three_d_mobject(low_axes) + low_axes_group = VGroup(*[ + low_axes.deepcopy() + for x in range(self.n_low_axes) + ]) + low_axes_group.arrange( + RIGHT, buff=low_axes.get_width() / 3 + ) + low_axes_group.set_width(FRAME_WIDTH - 2.5) + low_axes_group.next_to(top_axes, DOWN, LARGE_BUFF) + low_axes_group.to_edge(LEFT) + + self.top_axes = top_axes + self.low_axes_group = low_axes_group + + def setup_graphs(self): + top_axes = self.top_axes + low_axes_group = self.low_axes_group + + top_graph = self.get_initial_state_graph( + top_axes, + self.initial_func, + discontinuities=self.get_initial_func_discontinuities(), + color=YELLOW, + ) + top_graph.set_stroke(width=4) + + fourier_terms = self.get_fourier_cosine_terms( + self.initial_func + ) + + low_graphs = VGroup(*[ + self.get_initial_state_graph( + axes, + lambda x: A * np.cos(n * x / 2) + ) + for n, axes, A in zip( + it.count(), + low_axes_group, + fourier_terms + ) + ]) + k = self.k + low_surfaces = VGroup(*[ + self.get_surface( + axes, + lambda x, t: np.prod([ + A, + np.cos(n * x / 2), + np.exp(-k * (n / 2)**2 * t) + ]) + ) + for n, axes, A in zip( + it.count(), + low_axes_group, + fourier_terms + ) + ]) + top_surface = self.get_surface( + top_axes, + lambda x, t: np.sum([ + np.prod([ + A, + np.cos(n * x / 2), + np.exp(-k * (n / 2)**2 * t) + ]) + for n, A in zip( + it.count(), + fourier_terms + ) + ]) + ) + + self.top_graph = top_graph + self.low_graphs = low_graphs + self.low_surfaces = low_surfaces + self.top_surface = top_surface + + def show_break_down(self): + top_axes = self.top_axes + low_axes_group = self.low_axes_group + top_graph = self.top_graph + low_graphs = self.low_graphs + + plusses = VGroup(*[ + TexMobject("+").next_to( + axes.x_axis.get_end(), + RIGHT, MED_SMALL_BUFF + ) + for axes in low_axes_group + ]) + dots = TexMobject("\\cdots") + dots.next_to(plusses, RIGHT, MED_SMALL_BUFF) + + arrow = Arrow( + dots.get_right(), + top_graph.get_end() + 1.4 * DOWN + 1.7 * RIGHT, + path_arc=90 * DEGREES, + ) + + top_words = TextMobject("Arbitrary\\\\function") + top_words.next_to(top_axes, LEFT, MED_LARGE_BUFF) + top_words.set_color(YELLOW) + top_arrow = Arrow( + top_words.get_right(), + top_graph.point_from_proportion(0.3) + ) + + low_words = TextMobject("Sine curves") + low_words.set_color(BLUE) + low_words.next_to(low_axes_group, DOWN, MED_LARGE_BUFF) + + self.add(top_axes) + self.play(ShowCreation(top_graph)) + self.play( + FadeInFrom(top_words, RIGHT), + ShowCreation(top_arrow) + ) + self.wait() + self.play( + LaggedStartMap(FadeIn, low_axes_group), + FadeInFrom(low_words, UP), + LaggedStartMap(FadeInFromDown, [*plusses, dots]), + *[ + TransformFromCopy(top_graph, low_graph) + for low_graph in low_graphs + ], + ) + self.play(ShowCreation(arrow)) + self.wait() + + def show_solutions_for_waves(self): + low_axes_group = self.low_axes_group + top_axes = self.top_axes + low_graphs = self.low_graphs + low_surfaces = self.low_surfaces + top_surface = self.top_surface + top_graph = self.top_graph + + for surface in [top_surface, *low_surfaces]: + surface.sort(lambda p: -p[2]) + + anims1 = [] + anims2 = [ + ApplyMethod( + top_axes.y_axis.set_opacity, 1, + ), + ] + for axes, surface, graph in zip(low_axes_group, low_surfaces, low_graphs): + axes.y_axis.set_opacity(1) + axes.y_axis.label.fade(1) + anims1 += [ + ShowCreation(axes.y_axis), + Write(surface, run_time=2), + ] + anims2.append(AnimationGroup( + TransformFromCopy(graph, top_graph.copy()), + Transform( + surface.copy().set_fill(opacity=0), + top_surface, + ) + )) + + self.play(*anims1) + self.wait() + self.play(LaggedStart(*anims2, run_time=2)) + self.wait() + + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + low_checkmarks = VGroup(*[ + checkmark.copy().next_to( + surface.get_top(), UP, SMALL_BUFF + ) + for surface in low_surfaces + ]) + top_checkmark = checkmark.copy() + top_checkmark.scale(1.5) + top_checkmark.move_to(top_axes.get_corner(UR)) + + self.play(LaggedStartMap(FadeInFromDown, low_checkmarks)) + self.wait() + self.play(*[ + TransformFromCopy(low_checkmark, top_checkmark.copy()) + for low_checkmark in low_checkmarks + ]) + self.wait() + + # + def initial_func(self, x): + # return 3 * np.exp(-(x - PI)**2) + + x1 = TAU / 4 - 1 + x2 = TAU / 4 + 1 + x3 = 3 * TAU / 4 - 1.6 + x4 = 3 * TAU / 4 + 0.3 + + T0 = -2 + T1 = 2 + T2 = 1 + + if x < x1: + return T0 + elif x < x2: + alpha = inverse_interpolate(x1, x2, x) + return bezier([T0, T0, T1, T1])(alpha) + elif x < x3: + return T1 + elif x < x4: + alpha = inverse_interpolate(x3, x4, x) + return bezier([T1, T1, T2, T2])(alpha) + else: + return T2 + + def get_initial_func_discontinuities(self): + # return [TAU / 4, 3 * TAU / 4] + return [] + + def get_fourier_cosine_terms(self, func, n_terms=40): + result = [ + integrate.quad( + lambda x: (1 / PI) * func(x) * np.cos(n * x / 2), + 0, TAU + )[0] + for n in range(n_terms) + ] + result[0] = result[0] / 2 + return result + + +class OceanOfPossibilities(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_min": 0, + "z_max": 4, + }, + "k": 0.2, + "default_surface_config": { + # "resolution": (32, 20), + # "resolution": (8, 5), + } + } + + def construct(self): + self.setup_camera() + self.setup_axes() + self.setup_surface() + self.show_solution() + self.reference_boundary_conditions() + self.reference_initial_condition() + self.ambiently_change_solution() + + def setup_camera(self): + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + ) + self.begin_ambient_camera_rotation(rate=0.01) + + def setup_axes(self): + axes = self.get_three_d_axes(include_numbers=True) + axes.add(axes.input_plane) + axes.scale(0.8) + axes.center() + axes.shift(OUT + RIGHT) + + self.add(axes) + self.axes = axes + + def setup_surface(self): + axes = self.axes + k = self.k + + # Parameters for surface function + initial_As = [2] + [ + 0.8 * random.choice([-1, 1]) / n + for n in range(1, 20) + ] + A_trackers = Group(*[ + ValueTracker(A) + for A in initial_As + ]) + + def get_As(): + return [At.get_value() for At in A_trackers] + + omegas = [n / 2 for n in range(0, 10)] + + def func(x, t): + return np.sum([ + np.prod([ + A * np.cos(omega * x), + np.exp(-k * omega**2 * t) + ]) + for A, omega in zip(get_As(), omegas) + ]) + + # Surface and graph + surface = always_redraw( + lambda: self.get_surface(axes, func) + ) + t_tracker = ValueTracker(0) + graph = always_redraw( + lambda: self.get_time_slice_graph( + axes, func, t_tracker.get_value(), + ) + ) + + surface.suspend_updating() + graph.suspend_updating() + + self.surface_func = func + self.surface = surface + self.graph = graph + self.t_tracker = t_tracker + self.A_trackers = A_trackers + self.omegas = omegas + + def show_solution(self): + axes = self.axes + surface = self.surface + graph = self.graph + t_tracker = self.t_tracker + get_t = t_tracker.get_value + + opacity_tracker = ValueTracker(0) + plane = always_redraw(lambda: Polygon( + *[ + axes.c2p(x, get_t(), T) + for x, T in [ + (0, 0), (TAU, 0), (TAU, 4), (0, 4) + ] + ], + stroke_width=0, + fill_color=WHITE, + fill_opacity=opacity_tracker.get_value(), + )) + + self.add(surface, plane, graph) + graph.resume_updating() + self.play( + opacity_tracker.set_value, 0.2, + ApplyMethod( + t_tracker.set_value, 1, + rate_func=linear + ), + run_time=1 + ) + self.play( + ApplyMethod( + t_tracker.set_value, 10, + rate_func=linear, + run_time=9 + ) + ) + self.wait() + + self.plane = plane + + def reference_boundary_conditions(self): + axes = self.axes + t_numbers = axes.y_axis.numbers + + lines = VGroup(*[ + Line( + axes.c2p(x, 0, 0), + axes.c2p(x, axes.y_max, 0), + stroke_width=3, + stroke_color=MAROON_B, + ) + for x in [0, axes.x_max] + ]) + surface_boundary_lines = always_redraw(lambda: VGroup(*[ + ParametricFunction( + lambda t: axes.c2p( + x, t, + self.surface_func(x, t) + ), + t_max=axes.y_max + ).match_style(self.graph) + for x in [0, axes.x_max] + ])) + # surface_boundary_lines.suspend_updating() + words = VGroup() + for line in lines: + word = TextMobject("Boundary") + word.set_stroke(BLACK, 3, background=True) + word.scale(1.5) + word.match_color(line) + word.rotate(90 * DEGREES, RIGHT) + word.rotate(90 * DEGREES, OUT) + word.next_to(line, OUT, SMALL_BUFF) + words.add(word) + + self.stop_ambient_camera_rotation() + self.move_camera( + theta=-45 * DEGREES, + added_anims=[ + LaggedStartMap(ShowCreation, lines), + LaggedStartMap( + FadeInFrom, words, + lambda m: (m, IN) + ), + FadeOut(t_numbers), + ] + ) + self.play( + LaggedStart(*[ + TransformFromCopy(l1, l2) + for l1, l2 in zip(lines, surface_boundary_lines) + ]) + ) + self.add(surface_boundary_lines) + self.wait() + self.move_camera( + theta=-70 * DEGREES, + ) + + self.surface_boundary_lines = surface_boundary_lines + + def reference_initial_condition(self): + plane = self.plane + t_tracker = self.t_tracker + + self.play( + t_tracker.set_value, 0, + run_time=2 + ) + plane.clear_updaters() + self.play(FadeOut(plane)) + + def ambiently_change_solution(self): + A_trackers = self.A_trackers + + def generate_A_updater(A, rate): + def update(m, dt): + m.total_time += dt + m.set_value( + 2 * A * np.sin(rate * m.total_time + PI / 6) + ) + return update + + rates = [0, 0.2] + [ + 0.5 + 0.5 * np.random.random() + for x in range(len(A_trackers) - 2) + ] + + for tracker, rate in zip(A_trackers, rates): + tracker.total_time = 0 + tracker.add_updater(generate_A_updater( + tracker.get_value(), + rate + )) + + self.add(*A_trackers) + self.surface_boundary_lines.resume_updating() + self.surface.resume_updating() + self.graph.resume_updating() + self.begin_ambient_camera_rotation(rate=0.01) + self.wait(30) + + +class AnalyzeSineCurve(TemperatureGraphScene): + CONFIG = { + "origin_point": 3 * LEFT, + "axes_config": { + "z_min": -1.5, + "z_max": 1.5, + "z_axis_config": { + "unit_size": 2, + "tick_frequency": 0.5, + } + }, + "tex_to_color_map": { + "{x}": GREEN, + "T": YELLOW, + "=": WHITE, + "0": WHITE, + "\\Delta t": WHITE, + "\\sin": WHITE, + "{t}": PINK, + } + } + + def construct(self): + self.setup_axes() + self.ask_about_sine_curve() + self.show_sine_wave_on_axes() + self.reference_curvature() + self.show_derivatives() + self.show_curvature_matching_height() + self.show_time_step_scalings() + self.smooth_evolution() + + def setup_axes(self): + axes = self.get_three_d_axes() + axes.rotate(90 * DEGREES, LEFT) + axes.shift(self.origin_point - axes.c2p(0, 0, 0)) + y_axis = axes.y_axis + y_axis.fade(1) + z_axis = axes.z_axis + z_axis.label.next_to(z_axis.get_end(), UP, SMALL_BUFF) + + self.add_axes_numbers(axes) + y_axis.remove(y_axis.numbers) + axes.z_axis.add_numbers( + *range(-1, 2), + direction=LEFT, + ) + + self.axes = axes + + def ask_about_sine_curve(self): + curve = FunctionGraph( + lambda t: np.sin(t), + x_min=0, + x_max=TAU, + ) + curve.move_to(DR) + curve.set_width(5) + curve.set_color(YELLOW) + question = TextMobject("What's so special?") + question.scale(1.5) + question.to_edge(UP) + question.shift(2 * LEFT) + arrow = Arrow( + question.get_bottom(), + curve.point_from_proportion(0.25) + ) + + self.play( + ShowCreation(curve), + Write(question, run_time=1), + GrowArrow(arrow), + ) + self.wait() + + self.quick_sine_curve = curve + self.question_group = VGroup(question, arrow) + + def show_sine_wave_on_axes(self): + axes = self.axes + graph = self.get_initial_state_graph( + axes, lambda x: np.sin(x) + ) + graph.set_stroke(width=4) + graph_label = TexMobject( + "T({x}, 0) = \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + graph_label.next_to( + graph.point_from_proportion(0.25), UR, + buff=SMALL_BUFF, + ) + + v_line, x_tracker = self.get_v_line_with_x_tracker(graph) + + xs = VGroup( + *graph_label.get_parts_by_tex("x"), + axes.x_axis.label, + ) + + self.play( + Write(axes), + self.quick_sine_curve.become, graph, + FadeOutAndShift(self.question_group, UP), + ) + self.play( + FadeInFromDown(graph_label), + FadeIn(graph), + ) + self.remove(self.quick_sine_curve) + self.add(v_line) + self.play( + ApplyMethod( + x_tracker.set_value, TAU, + rate_func=lambda t: smooth(t, 3), + run_time=5, + ), + LaggedStartMap( + ShowCreationThenFadeAround, xs, + run_time=3, + lag_ratio=0.2, + ) + ) + self.remove(v_line, x_tracker) + self.wait() + + self.graph = graph + self.graph_label = graph_label + self.v_line = v_line + self.x_tracker = x_tracker + + def reference_curvature(self): + curve_segment, curve_x_tracker = \ + self.get_curve_segment_with_x_tracker(self.graph) + + self.add(curve_segment) + self.play( + curve_x_tracker.set_value, TAU, + run_time=5, + rate_func=lambda t: smooth(t, 3), + ) + self.play(FadeOut(curve_segment)) + + self.curve_segment = curve_segment + self.curve_x_tracker = curve_x_tracker + + def show_derivatives(self): + deriv1 = TexMobject( + "{\\partial T \\over \\partial {x}}({x}, 0)", + "= \\cos\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + deriv2 = TexMobject( + "{\\partial^2 T \\over \\partial {x}^2}({x}, 0)", + "= -\\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + + deriv1.to_corner(UR) + deriv2.next_to( + deriv1, DOWN, + buff=0.75, + aligned_edge=LEFT, + ) + VGroup(deriv1, deriv2).shift(1.4 * RIGHT) + + self.play( + Animation(Group(*self.get_mobjects())), + FadeInFrom(deriv1, LEFT), + self.camera.frame_center.shift, 2 * RIGHT, + ) + self.wait() + self.play( + FadeInFrom(deriv2, UP) + ) + self.wait() + + self.deriv1 = deriv1 + self.deriv2 = deriv2 + + def show_curvature_matching_height(self): + axes = self.axes + graph = self.graph + curve_segment = self.curve_segment + curve_x_tracker = self.curve_x_tracker + + d2_graph = self.get_initial_state_graph( + axes, lambda x: -np.sin(x), + ) + dashed_d2_graph = DashedVMobject(d2_graph, num_dashes=50) + dashed_d2_graph.color_using_background_image(None) + dashed_d2_graph.set_stroke(RED, 2) + + vector, x_tracker = self.get_v_line_with_x_tracker( + d2_graph, + line_creator=lambda p1, p2: Arrow( + p1, p2, color=RED, buff=0 + ) + ) + + lil_vectors = self.get_many_lil_vectors(graph) + lil_vector = always_redraw( + lambda: self.get_lil_vector( + graph, x_tracker.get_value() + ) + ) + + d2_rect = SurroundingRectangle( + self.deriv2[-5:], + color=RED, + ) + self.play(ShowCreation(d2_rect)) + self.add(vector) + self.add(lil_vector) + self.add(curve_segment) + curve_x_tracker.set_value(0) + self.play( + ShowCreation(dashed_d2_graph), + x_tracker.set_value, TAU, + curve_x_tracker.set_value, TAU, + ShowIncreasingSubsets(lil_vectors[1:]), + run_time=8, + rate_func=linear, + ) + self.remove(vector) + self.remove(lil_vector) + self.add(lil_vectors) + self.play( + FadeOut(curve_segment), + FadeOut(d2_rect), + ) + + self.lil_vectors = lil_vectors + self.dashed_d2_graph = dashed_d2_graph + + def show_time_step_scalings(self): + axes = self.axes + graph_label = self.graph_label + dashed_d2_graph = self.dashed_d2_graph + lil_vectors = self.lil_vectors + graph = self.graph + + factor = 0.9 + + new_label = TexMobject( + "T({x}, \\Delta t) = c \\cdot \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + final_label = TexMobject( + "T({x}, {t}) = (\\text{something}) \\cdot \\sin\\left({x}\\right)", + tex_to_color_map=self.tex_to_color_map, + ) + for label in (new_label, final_label): + label.shift( + graph_label.get_part_by_tex("=").get_center() - + label.get_part_by_tex("=").get_center() + ) + final_label.shift(1.5 * LEFT) + + h_lines = VGroup( + DashedLine(axes.c2p(0, 0, 1), axes.c2p(TAU, 0, 1)), + DashedLine(axes.c2p(0, 0, -1), axes.c2p(TAU, 0, -1)), + ) + + lil_vectors.add_updater(lambda m: m.become( + self.get_many_lil_vectors(graph) + )) + + i = 4 + self.play( + ReplacementTransform( + graph_label[:i], new_label[:i], + ), + ReplacementTransform( + graph_label[i + 1:i + 3], + new_label[i + 1:i + 3], + ), + FadeOutAndShift(graph_label[i], UP), + FadeInFrom(new_label[i], DOWN), + ) + self.play( + ReplacementTransform( + graph_label[i + 3:], + new_label[i + 4:] + ), + FadeInFromDown(new_label[i + 3]) + ) + self.play( + FadeOut(dashed_d2_graph), + FadeIn(h_lines), + ) + self.play( + graph.stretch, factor, 1, + h_lines.stretch, factor, 1, + ) + self.wait() + + # Repeat + last_coef = None + last_exp = None + delta_T = new_label.get_part_by_tex("\\Delta t") + c = new_label.get_part_by_tex("c")[0] + prefix = new_label[:4] + prefix.generate_target() + for x in range(5): + coef = Integer(x + 2) + exp = coef.copy().scale(0.7) + coef.next_to( + delta_T, LEFT, SMALL_BUFF, + aligned_edge=DOWN, + ) + exp.move_to(c.get_corner(UR), DL) + anims1 = [FadeInFrom(coef, 0.25 * DOWN)] + anims2 = [FadeInFrom(exp, 0.25 * DOWN)] + if last_coef: + anims1.append( + FadeOutAndShift(last_coef, 0.25 * UP) + ) + anims2.append( + FadeOutAndShift(last_exp, 0.25 * UP) + ) + last_coef = coef + last_exp = exp + prefix.target.next_to(coef, LEFT, SMALL_BUFF) + prefix.target.match_y(prefix) + anims1.append(MoveToTarget(prefix)) + + self.play(*anims1) + self.play( + graph.stretch, factor, 1, + h_lines.stretch, factor, 1, + *anims2, + ) + self.play( + ReplacementTransform( + new_label[:4], + final_label[:4], + ), + ReplacementTransform( + VGroup(last_coef, delta_T), + final_label.get_part_by_tex("{t}"), + ), + ReplacementTransform( + last_exp, + final_label.get_part_by_tex("something"), + ), + FadeOut(new_label.get_part_by_tex("\\cdot"), UP), + ReplacementTransform( + new_label[-4:], + final_label[-4:], + ), + ReplacementTransform( + new_label.get_part_by_tex("="), + final_label.get_part_by_tex("="), + ), + ReplacementTransform( + new_label.get_part_by_tex(")"), + final_label.get_part_by_tex(")"), + ), + ) + final_label.add_background_rectangle(opacity=1) + self.add(final_label) + self.wait() + + group = VGroup(graph, h_lines) + group.add_updater(lambda m, dt: m.stretch( + (1 - 0.1 * dt), 1 + )) + self.add(group) + self.wait(10) + + def smooth_evolution(self): + pass + + # + def get_rod(self, temp_func): + pass + + def get_v_line_with_x_tracker(self, graph, line_creator=DashedLine): + axes = self.axes + x_min = axes.x_axis.p2n(graph.get_start()) + x_max = axes.x_axis.p2n(graph.get_end()) + x_tracker = ValueTracker(x_min) + get_x = x_tracker.get_value + v_line = always_redraw(lambda: line_creator( + axes.c2p(get_x(), 0, 0), + graph.point_from_proportion( + inverse_interpolate( + x_min, x_max, get_x() + ) + ), + )) + return v_line, x_tracker + + def get_curve_segment_with_x_tracker(self, graph, delta_x=0.5): + axes = self.axes + x_min = axes.x_axis.p2n(graph.get_start()) + x_max = axes.x_axis.p2n(graph.get_end()) + x_tracker = ValueTracker(x_min) + get_x = x_tracker.get_value + + def x2a(x): + return inverse_interpolate(x_min, x_max, x) + + curve = VMobject( + stroke_color=WHITE, + stroke_width=5 + ) + curve.add_updater(lambda m: m.pointwise_become_partial( + graph, + max(x2a(get_x() - delta_x), 0), + min(x2a(get_x() + delta_x), 1), + )) + return curve, x_tracker + + def get_lil_vector(self, graph, x): + x_axis = self.axes.x_axis + point = graph.point_from_proportion(x / TAU) + x_axis_point = x_axis.n2p(x_axis.p2n(point)) + return Arrow( + point, + interpolate( + point, x_axis_point, 0.5, + ), + buff=0, + color=RED + ) + + def get_many_lil_vectors(self, graph, n=13): + return VGroup(*[ + self.get_lil_vector(graph, x) + for x in np.linspace(0, TAU, n) + ]) + + +class SineWaveScaledByExp(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_min": -1.5, + "z_max": 1.5, + "z_axis_config": { + "unit_size": 2, + "tick_frequency": 0.5, + "label_direction": LEFT, + }, + "y_axis_config": { + "label_direction": RIGHT, + }, + }, + "k": 0.3, + } + + def construct(self): + self.setup_axes() + self.setup_camera() + self.show_sine_wave() + self.show_decay_surface() + self.linger_at_end() + + def setup_axes(self): + axes = self.get_three_d_axes() + + # Add number labels + self.add_axes_numbers(axes) + for axis in [axes.x_axis, axes.y_axis]: + axis.numbers.rotate( + 90 * DEGREES, + axis=axis.get_vector(), + about_point=axis.point_from_proportion(0.5) + ) + axis.numbers.set_shade_in_3d(True) + axes.z_axis.add_numbers(*range(-1, 2)) + for number in axes.z_axis.numbers: + number.rotate(90 * DEGREES, RIGHT) + + axes.z_axis.label.next_to( + axes.z_axis.get_end(), OUT, + ) + + # Input plane + axes.input_plane.set_opacity(0.25) + self.add(axes.input_plane) + + # Shift into place + # axes.shift(5 * LEFT) + self.axes = axes + self.add(axes) + + def setup_camera(self): + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-80 * DEGREES, + distance=50, + ) + self.camera.set_frame_center( + 2 * RIGHT, + ) + + def show_sine_wave(self): + time_tracker = ValueTracker(0) + graph = always_redraw( + lambda: self.get_time_slice_graph( + self.axes, + self.sin_exp, + t=time_tracker.get_value(), + ) + ) + graph.suspend_updating() + + graph_label = TexMobject("\\sin(x)") + graph_label.set_color(BLUE) + graph_label.rotate(90 * DEGREES, RIGHT) + graph_label.next_to( + graph.point_from_proportion(0.25), + OUT, + SMALL_BUFF, + ) + + self.play( + ShowCreation(graph), + FadeInFrom(graph_label, IN) + ) + self.wait() + graph.resume_updating() + + self.time_tracker = time_tracker + self.graph = graph + + def show_decay_surface(self): + time_tracker = self.time_tracker + axes = self.axes + + plane = Rectangle() + plane.rotate(90 * DEGREES, RIGHT) + plane.set_stroke(width=0) + plane.set_fill(WHITE, 0.2) + plane.match_depth(axes.z_axis) + plane.match_width(axes.x_axis, stretch=True) + plane.add_updater( + lambda p: p.move_to(axes.c2p( + 0, + time_tracker.get_value(), + 0, + ), LEFT) + ) + + time_slices = VGroup(*[ + self.get_time_slice_graph( + self.axes, + self.sin_exp, + t=t, + ) + for t in range(0, 10) + ]) + surface_t_tracker = ValueTracker(0) + surface = always_redraw( + lambda: self.get_surface( + self.axes, + self.sin_exp, + v_max=surface_t_tracker.get_value(), + ).set_stroke(opacity=0) + ) + + exp_graph = ParametricFunction( + lambda t: axes.c2p( + PI / 2, + t, + self.sin_exp(PI / 2, t) + ), + t_min=axes.y_min, + t_max=axes.y_max, + ) + exp_graph.set_stroke(RED, 3) + exp_graph.set_shade_in_3d(True) + + exp_label = TexMobject("e^{-\\alpha t}") + exp_label.scale(1.5) + exp_label.set_color(RED) + exp_label.rotate(90 * DEGREES, RIGHT) + exp_label.rotate(90 * DEGREES, OUT) + exp_label.next_to( + exp_graph.point_from_proportion(0.3), + OUT + UP, + ) + + self.move_camera( + theta=-25 * DEGREES, + ) + self.add(surface) + self.add(plane) + self.play( + surface_t_tracker.set_value, axes.y_max, + time_tracker.set_value, axes.y_max, + ShowIncreasingSubsets( + time_slices, + int_func=np.ceil, + ), + run_time=5, + rate_func=linear, + ) + surface.clear_updaters() + + self.play( + ShowCreation(exp_graph), + FadeOut(plane), + FadeInFrom(exp_label, IN), + time_slices.set_stroke, {"width": 1}, + ) + + def linger_at_end(self): + self.wait() + self.begin_ambient_camera_rotation(rate=-0.02) + self.wait(20) + + # + def sin_exp(self, x, t): + return np.sin(x) * np.exp(-self.k * t) + + +class BoundaryConditionReference(ShowEvolvingTempGraphWithArrows): + def construct(self): + self.setup_axes() + self.setup_graph() + + rod = self.get_rod(0, 10) + self.color_rod_by_graph(rod) + + boundary_points = [ + rod.get_right(), + rod.get_left(), + ] + boundary_dots = VGroup(*[ + Dot(point, radius=0.2) + for point in boundary_points + ]) + boundary_arrows = VGroup(*[ + Vector(2 * DOWN).next_to(dot, UP) + for dot in boundary_dots + ]) + boundary_arrows.set_stroke(YELLOW, 10) + + words = TextMobject( + "Different rules\\\\", + "at the boundary", + ) + words.scale(1.5) + words.to_edge(UP) + + # self.add(self.axes) + # self.add(self.graph) + self.add(rod) + self.play(FadeInFromDown(words)) + self.play( + LaggedStartMap(GrowArrow, boundary_arrows), + LaggedStartMap(GrowFromCenter, boundary_dots), + lag_ratio=0.3, + run_time=1, + ) + self.wait() + + +class SimulateRealSineCurve(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "axes_config": { + "x_min": 0, + "x_max": TAU, + "x_axis_config": { + "unit_size": 1.5, + "include_tip": False, + "tick_frequency": PI / 4, + }, + "y_min": -1.5, + "y_max": 1.5, + "y_axis_config": { + "tick_frequency": 0.5, + "unit_size": 2, + }, + }, + "graph_x_min": 0, + "graph_x_max": TAU, + "arrow_xs": np.linspace(0, TAU, 13), + "rod_opacity": 0.5, + "wait_time": 30, + "alpha": 0.5, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + self.add_arrows() + self.initialize_updaters() + self.let_play() + + def add_labels_to_axes(self): + x_axis = self.axes.x_axis + x_axis.add(*[ + TexMobject(tex).scale(0.5).next_to( + x_axis.n2p(n), + DOWN, + buff=MED_SMALL_BUFF + ) + for tex, n in [ + ("\\tau \\over 4", TAU / 4), + ("\\tau \\over 2", TAU / 2), + ("3 \\tau \\over 4", 3 * TAU / 4), + ("\\tau", TAU), + ] + ]) + + def add_axes(self): + super().add_axes() + self.add_labels_to_axes() + + def add_rod(self): + super().add_rod() + self.rod.set_opacity(self.rod_opacity) + self.rod.set_stroke(width=0) + + def initial_function(self, x): + return np.sin(x) + + def y_to_color(self, y): + return temperature_to_color(0.8 * y) + + +class StraightLine3DGraph(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_min": 0, + "z_max": 10, + "z_axis_config": { + 'unit_size': 0.5, + } + }, + "c": 1.2, + } + + def construct(self): + axes = self.get_three_d_axes() + axes.add(axes.input_plane) + axes.move_to(2 * IN + UP, IN) + surface = self.get_surface( + axes, self.func, + ) + initial_graph = self.get_initial_state_graph( + axes, lambda x: self.func(x, 0) + ) + + plane = self.get_const_time_plane(axes) + initial_graph.add_updater( + lambda m: m.move_to(plane, IN) + ) + + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-100 * DEGREES, + ) + self.begin_ambient_camera_rotation() + + self.add(axes) + self.add(initial_graph) + self.play( + TransformFromCopy(initial_graph, surface) + ) + self.add(surface, initial_graph) + self.wait() + + self.play( + FadeIn(plane), + ApplyMethod( + plane.t_tracker.set_value, 10, + rate_func=linear, + run_time=10, + ) + ) + self.play(FadeOut(plane)) + self.wait(15) + + def func(self, x, t): + return self.c * x + + +class SimulateLinearGraph(SimulateRealSineCurve): + CONFIG = { + "axes_config": { + "y_min": 0, + "y_max": 3, + "y_axis_config": { + "tick_frequency": 0.5, + "unit_size": 2, + }, + }, + "arrow_scale_factor": 2, + "alpha": 1, + "wait_time": 40, + "step_size": 0.02, + } + + # def let_play(self): + # pass + + def add_labels_to_axes(self): + pass + + def y_to_color(self, y): + return temperature_to_color(0.8 * (y - 1.5)) + + def initial_function(self, x): + axes = self.axes + y_max = axes.y_max + x_max = axes.x_max + slope = y_max / x_max + return slope * x + + +class EmphasizeBoundaryPoints(SimulateLinearGraph): + CONFIG = { + "wait_time": 30, + } + + def let_play(self): + rod = self.rod + self.graph.update(0.01) + self.arrows.update() + + to_update = VGroup( + self.graph, + self.arrows, + self.time_label, + ) + for mob in to_update: + mob.suspend_updating() + + dots = VGroup( + Dot(rod.get_left()), + Dot(rod.get_right()), + ) + for dot in dots: + dot.set_height(0.2) + dot.set_color(YELLOW) + + words = TextMobject( + "Different rules\\\\" + "at the boundary" + ) + words.next_to(rod, UP) + + arrows = VGroup(*[ + Arrow( + words.get_corner(corner), + dot.get_top(), + path_arc=u * 60 * DEGREES, + ) + for corner, dot, u in zip( + [LEFT, RIGHT], dots, [1, -1] + ) + ]) + + self.add(words) + self.play( + LaggedStartMap( + FadeInFromLarge, dots, + scale_factor=5, + ), + LaggedStartMap( + ShowCreation, arrows, + ), + lag_ratio=0.4 + ) + self.wait() + + for mob in to_update: + mob.resume_updating() + + super().let_play() + + +class FlatEdgesContinuousEvolution(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "wait_time": 30, + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + ], + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + self.initialize_updaters() + self.add_boundary_lines() + self.let_play() + + def add_boundary_lines(self): + + lines = VGroup(*[ + Line(LEFT, RIGHT) + for x in range(2) + ]) + lines.set_width(1.5) + lines.set_stroke(WHITE, 5, opacity=0.5) + lines.add_updater(self.update_lines) + + turn_animation_into_updater( + ShowCreation(lines, run_time=2) + ) + self.add(lines) + + def update_lines(self, lines): + graph = self.graph + lines[0].move_to(graph.get_start()) + lines[1].move_to(graph.get_end()) + + def initial_function(self, x): + return ShowEvolvingTempGraphWithArrows.initial_function(self, x) + + +class MoreAccurateTempFlowWithArrows(ShowEvolvingTempGraphWithArrows): + CONFIG = { + "arrow_scale_factor": 1, + "max_magnitude": 1, + "freq_amplitude_pairs": [ + (1, 0.5), + (2, 1), + (3, 0.5), + (4, 0.3), + ], + } + + def setup_axes(self): + super().setup_axes() + y_axis = self.axes.y_axis + y_axis.remove(y_axis.label) + + +class SlopeToHeatFlow(FlatEdgesContinuousEvolution): + CONFIG = { + "wait_time": 20, + "xs": [1.75, 5.25, 7.8], + "axes_config": { + "x_min": 0, + "x_max": 10, + "x_axis_config": { + "include_tip": False, + } + }, + "n_arrows": 10, + "random_seed": 1, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_rod() + # + self.show_slope_labels() + self.show_heat_flow() + + def show_slope_labels(self): + axes = self.axes + # axes.x_axis.add_numbers() + graph = self.graph + xs = self.xs + + lines = VGroup(*[ + TangentLine( + graph, + inverse_interpolate( + axes.x_min, + axes.x_max, + x + ), + length=2, + stroke_opacity=0.75, + ) + for x in xs + ]) + + slope_words = VGroup() + for line in lines: + slope = line.get_slope() + word = TextMobject( + "Slope =", + "${:.2}$".format(slope) + ) + if slope > 0: + word[1].set_color(GREEN) + else: + word[1].set_color(RED) + + word.set_width(line.get_length()) + word.next_to(ORIGIN, UP, SMALL_BUFF) + word.rotate( + line.get_angle(), + about_point=ORIGIN, + ) + word.shift(line.get_center()) + slope_words.add(word) + + self.play( + LaggedStartMap(ShowCreation, lines), + LaggedStartMap(Write, slope_words), + lag_ratio=0.5, + run_time=1, + ) + self.wait() + + self.lines = lines + self.slope_words = slope_words + + def show_heat_flow(self): + axes = self.axes + xs = self.xs + slope_words = self.slope_words + tan_lines = self.lines + + slopes = map(Line.get_slope, self.lines) + flow_rates = [-s for s in slopes] + + points = list(map(axes.x_axis.n2p, xs)) + v_lines = VGroup(*[ + Line( + line.get_center(), point, + stroke_width=1, + ) + for point, line in zip(points, tan_lines) + ]) + + flow_words = VGroup(*[ + TextMobject("Heat flow").next_to( + point, DOWN + ).scale(0.8) + for point in points + ]) + flow_mobjects = VGroup(*[ + self.get_flow(x, flow_rate) + for x, flow_rate in zip(xs, flow_rates) + ]) + + self.add(flow_mobjects) + self.wait() + self.play( + LaggedStart(*[ + TransformFromCopy( + sw[0], fw[0] + ) + for sw, fw in zip(slope_words, flow_words) + ]), + LaggedStartMap(ShowCreation, v_lines), + lag_ratio=0.4, + run_time=2, + ) + self.wait(self.wait_time) + + def get_flow(self, x, flow_rate): + return VGroup(*[ + self.get_single_flow_arrow( + x, flow_rate, + t_offset=to + ) + for to in np.linspace(0, 5, self.n_arrows) + ]) + + def get_single_flow_arrow(self, x, flow_rate, t_offset): + axes = self.axes + point = axes.x_axis.n2p(x) + + h_shift = 0.5 + v_shift = 0.4 * np.random.random() + 0.1 + + arrow = Vector(0.5 * RIGHT * np.sign(flow_rate)) + arrow.move_to(point) + arrow.shift(v_shift * UP) + lp = arrow.get_center() + h_shift * LEFT + rp = arrow.get_center() + h_shift * RIGHT + lc = self.rod_point_to_color(lp) + rc = self.rod_point_to_color(rp) + + run_time = 3 / flow_rate + animation = UpdateFromAlphaFunc( + arrow, + lambda m, a: m.move_to( + interpolate(lp, rp, a) + ).set_color( + interpolate_color(lc, rc, a) + ).set_opacity(there_and_back(a)), + run_time=run_time, + ) + result = cycle_animation(animation) + animation.total_time += t_offset + return result + + +class CloserLookAtStraightLine(SimulateLinearGraph): + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + # self.initialize_updaters() + # + self.show_t_eq_0_state() + self.show_t_gt_0_state() + + def show_t_eq_0_state(self): + t_eq = TexMobject("t", "=", "0") + t_eq.next_to(self.time_label, DOWN) + + circles = VGroup(*[ + Circle( + radius=0.25, + stroke_color=YELLOW, + ) + for x in range(2) + ]) + circles.add_updater(self.attach_to_endpoints) + + self.play(Write(t_eq)) + self.play(ShowCreation(circles)) + self.wait() + self.play(FadeOut(circles)) + + self.circles = circles + self.t_eq = t_eq + + def show_t_gt_0_state(self): + # circles = self.circles + t_eq = self.t_eq + + t_ineq = TexMobject("t", ">", "0") + t_ineq.move_to(t_eq) + + slope_lines = VGroup(*[ + Line(LEFT, RIGHT) + for x in range(2) + ]) + slope_lines.set_opacity(0.5) + slope_lines.add_updater(self.attach_to_endpoints) + + self.remove(t_eq) + self.add(t_ineq) + + self.initialize_updaters() + self.run_clock(0.1) + for mob in self.mobjects: + mob.suspend_updating() + self.wait() + + self.add(slope_lines) + self.add(self.clock, self.time_label, t_ineq) + self.play(ShowCreation(slope_lines)) + for mob in self.mobjects: + mob.resume_updating() + + self.run_clock(self.wait_time) + + # + def attach_to_endpoints(self, mobs): + points = [ + self.graph.get_start(), + self.graph.get_end(), + ] + for mob, point in zip(mobs, points): + mob.move_to(point) + return mobs + + +class ManipulateSinExpSurface(TemperatureGraphScene): + CONFIG = { + "axes_config": { + "z_max": 1.25, + "z_min": -1.25, + "z_axis_config": { + "unit_size": 2.5, + "tick_frequency": 0.5, + }, + "x_axis_config": { + # "unit_size": 1.5, + "unit_size": 1.0, + "tick_frequency": 1, + }, + "x_max": 10, + "y_max": 15, + }, + "alpha": 0.2, + "tex_mobject_config": { + "tex_to_color_map": { + "{x}": GREEN, + "{t}": YELLOW, + "\\omega": MAROON_B, + "^2": WHITE, + }, + }, + "graph_config": {}, + "initial_phi": -90 * DEGREES, + "initial_omega": 1, + } + + def construct(self): + self.setup_axes() + self.add_sine_wave() + self.shift_sine_to_cosine() + self.show_derivatives_of_cos() + self.show_cos_exp_surface() + self.change_frequency() + self.talk_through_omega() + self.show_cos_omega_derivatives() + self.show_rebalanced_exp() + + def setup_axes(self): + axes = self.get_three_d_axes(include_numbers=True) + axes.x_axis.remove(axes.x_axis.numbers) + L = TexMobject("L") + L.rotate(90 * DEGREES, RIGHT) + L.next_to(axes.x_axis.get_end(), IN) + axes.x_axis.label = L + axes.x_axis.add(L) + + axes.shift(5 * LEFT + 0.5 * IN) + axes.z_axis.label[0].remove( + *axes.z_axis.label[0][1:] + ) + axes.z_axis.label.next_to( + axes.z_axis.get_end(), OUT + ) + axes.z_axis.add_numbers( + *np.arange(-1, 1.5, 0.5), + direction=LEFT, + number_config={ + "num_decimal_places": 1, + } + ) + for number in axes.z_axis.numbers: + number.rotate(90 * DEGREES, RIGHT) + + self.set_camera_orientation( + phi=90 * DEGREES, + theta=-90 * DEGREES, + distance=100, + ) + + self.add(axes) + self.remove(axes.y_axis) + self.axes = axes + + def add_sine_wave(self): + self.initialize_parameter_trackers() + graph = self.get_graph() + sin_label = TexMobject( + "\\sin\\left({x}\\right)", + **self.tex_mobject_config, + ) + sin_label.shift(2 * LEFT + 2.75 * UP) + + self.add_fixed_in_frame_mobjects(sin_label) + graph.suspend_updating() + self.play( + ShowCreation(graph), + Write(sin_label), + ) + graph.resume_updating() + self.wait() + + self.sin_label = sin_label + self.graph = graph + + def shift_sine_to_cosine(self): + graph = self.graph + sin_label = self.sin_label + + sin_cross = Cross(sin_label) + sin_cross.add_updater( + lambda m: m.move_to(sin_label) + ) + cos_label = TexMobject( + "\\cos\\left({x}\\right)", + **self.tex_mobject_config, + ) + cos_label.move_to(sin_label, LEFT) + cos_label.shift(LEFT) + # cos_label.shift( + # axes.c2p(0, 0) - axes.c2p(PI / 2, 0), + # ) + + left_tangent = Line(ORIGIN, RIGHT) + left_tangent.set_stroke(WHITE, 5) + + self.add_fixed_in_frame_mobjects(cos_label) + self.play( + ApplyMethod( + self.phi_tracker.set_value, 0, + ), + FadeOutAndShift(sin_label, LEFT), + FadeInFrom(cos_label, RIGHT), + run_time=2, + ) + left_tangent.move_to(graph.get_start(), LEFT) + self.play(ShowCreation(left_tangent)) + self.play(FadeOut(left_tangent)) + + self.cos_label = cos_label + + def show_derivatives_of_cos(self): + cos_label = self.cos_label + cos_exp_label = TexMobject( + "\\cos\\left({x}\\right)", + "e^{-\\alpha {t}}", + **self.tex_mobject_config + ) + cos_exp_label.move_to(cos_label, LEFT) + + ddx_group, ddx_exp_group = [ + self.get_ddx_computation_group( + label, + *[ + TexMobject( + s + "\\left({x}\\right)" + exp, + **self.tex_mobject_config, + ) + for s in ["-\\sin", "-\\cos"] + ] + ) + for label, exp in [ + (cos_label, ""), + (cos_exp_label, "e^{-\\alpha {t}}"), + ] + ] + + self.add_fixed_in_frame_mobjects(ddx_group) + self.play(FadeIn(ddx_group)) + self.wait() + + # Cos exp + transforms = [ + ReplacementTransform( + cos_label, cos_exp_label, + ), + ReplacementTransform( + ddx_group, ddx_exp_group, + ), + ] + for trans in transforms: + trans.begin() + self.add_fixed_in_frame_mobjects(trans.mobject) + self.play(*transforms) + self.add_fixed_in_frame_mobjects( + cos_exp_label, ddx_exp_group + ) + self.remove_fixed_in_frame_mobjects( + cos_label, + *[ + trans.mobject + for trans in transforms + ] + ) + + self.cos_exp_label = cos_exp_label + self.ddx_exp_group = ddx_exp_group + + def show_cos_exp_surface(self): + axes = self.axes + + surface = self.get_cos_exp_surface() + self.add(surface) + surface.max_t_tracker.set_value(0) + self.move_camera( + phi=85 * DEGREES, + theta=-80 * DEGREES, + added_anims=[ + ApplyMethod( + surface.max_t_tracker.set_value, + axes.y_max, + run_time=4, + ), + Write(axes.y_axis), + ] + ) + self.wait() + + self.surface = surface + + def change_frequency(self): + cos_exp_label = self.cos_exp_label + ddx_exp_group = self.ddx_exp_group + omega_tracker = self.omega_tracker + + cos_omega = TexMobject( + "\\cos\\left(", + "\\omega", "\\cdot", "{x}", + "\\right)", + **self.tex_mobject_config + ) + cos_omega.move_to(cos_exp_label, LEFT) + + omega = cos_omega.get_part_by_tex("\\omega") + brace = Brace(omega, UP, buff=SMALL_BUFF) + omega_decimal = always_redraw( + lambda: DecimalNumber( + omega_tracker.get_value(), + color=omega.get_color(), + ).next_to(brace, UP, SMALL_BUFF) + ) + + self.add_fixed_in_frame_mobjects( + cos_omega, + brace, + omega_decimal, + ) + self.play( + self.camera.phi_tracker.set_value, 90 * DEGREES, + self.camera.theta_tracker.set_value, -90 * DEGREES, + FadeOut(self.surface), + FadeOut(self.axes.y_axis), + FadeOut(cos_exp_label), + FadeOut(ddx_exp_group), + FadeIn(cos_omega), + GrowFromCenter(brace), + FadeInFromDown(omega_decimal) + ) + for n in [2, 6, 1, 4]: + freq = n * PI / self.axes.x_max + self.play( + omega_tracker.set_value, freq, + run_time=2 + ) + self.wait() + self.wait() + + self.cos_omega = cos_omega + self.omega_brace = brace + + def talk_through_omega(self): + axes = self.axes + + x_tracker = ValueTracker(0) + get_x = x_tracker.get_value + v_line = always_redraw(lambda: DashedLine( + axes.c2p(get_x(), 0, 0), + axes.c2p(get_x(), 0, self.func(get_x(), 0)), + )) + + x = self.cos_omega.get_part_by_tex("{x}") + brace = Brace(x, DOWN) + x_decimal = always_redraw( + lambda: DecimalNumber( + get_x(), + color=x.get_color() + ).next_to(brace, DOWN, SMALL_BUFF) + ) + + self.add(v_line) + self.add_fixed_in_frame_mobjects(brace, x_decimal) + self.play( + x_tracker.set_value, 5, + run_time=5, + rate_func=linear, + ) + self.play( + FadeOut(v_line), + FadeOut(brace), + FadeOut(x_decimal), + ) + self.remove_fixed_in_frame_mobjects( + brace, x_decimal + ) + + def show_cos_omega_derivatives(self): + cos_omega = self.cos_omega + ddx_omega_group = self.get_ddx_computation_group( + cos_omega, + *[ + TexMobject( + s + "\\left(\\omega \\cdot {x}\\right)", + **self.tex_mobject_config, + ) + for s in ["-\\omega \\sin", "-\\omega^2 \\cos"] + ] + ) + + omega_squared = ddx_omega_group[-1][1:3] + rect = SurroundingRectangle(omega_squared) + + self.add_fixed_in_frame_mobjects(ddx_omega_group) + self.play(FadeIn(ddx_omega_group)) + self.wait() + self.add_fixed_in_frame_mobjects(rect) + self.play(ShowCreationThenFadeOut(rect)) + self.wait() + + self.ddx_omega_group = ddx_omega_group + + def show_rebalanced_exp(self): + cos_omega = self.cos_omega + ddx_omega_group = self.ddx_omega_group + + cos_exp = TexMobject( + "\\cos\\left(", + "\\omega", "\\cdot", "{x}", + "\\right)", + "e^{-\\alpha \\omega^2 {t}}", + **self.tex_mobject_config + ) + cos_exp.move_to(cos_omega, DL) + + self.add_fixed_in_frame_mobjects(cos_exp) + self.play( + FadeOut(cos_omega), + FadeOut(ddx_omega_group), + FadeIn(cos_exp), + ) + self.remove_fixed_in_frame_mobjects( + cos_omega, + ddx_omega_group, + ) + + self.play( + FadeIn(self.surface), + FadeIn(self.axes.y_axis), + VGroup( + cos_exp, + self.omega_brace, + ).shift, 4 * RIGHT, + self.camera.phi_tracker.set_value, 80 * DEGREES, + self.camera.theta_tracker.set_value, -80 * DEGREES, + ) + self.wait() + self.play( + self.omega_tracker.set_value, TAU / 10, + run_time=6, + ) + + # + def initialize_parameter_trackers(self): + self.phi_tracker = ValueTracker( + self.initial_phi + ) + self.omega_tracker = ValueTracker( + self.initial_omega + ) + self.t_tracker = ValueTracker(0) + + def get_graph(self): + return always_redraw( + lambda: self.get_time_slice_graph( + self.axes, self.func, + t=self.t_tracker.get_value(), + **self.graph_config + ) + ) + + def get_cos_exp_surface(self): + max_t_tracker = ValueTracker(self.axes.y_max) + surface = always_redraw( + lambda: self.get_surface( + self.axes, + self.func, + v_max=max_t_tracker.get_value(), + ) + ) + surface.max_t_tracker = max_t_tracker + return surface + + def func(self, x, t): + phi = self.phi_tracker.get_value() + omega = self.omega_tracker.get_value() + alpha = self.alpha + return op.mul( + np.cos(omega * (x + phi)), + np.exp(-alpha * (omega**2) * t) + ) + + def get_ddx_computation_group(self, f, df, ddf): + arrows = VGroup(*[ + Vector(0.5 * RIGHT) for x in range(2) + ]) + group = VGroup( + arrows[0], df, arrows[1], ddf + ) + group.arrange(RIGHT) + group.next_to(f, RIGHT) + + for arrow in arrows: + label = TexMobject( + "\\partial \\over \\partial {x}", + **self.tex_mobject_config, + ) + label.scale(0.5) + label.next_to(arrow, UP, SMALL_BUFF) + arrow.add(label) + + group.arrows = arrows + group.funcs = VGroup(df, ddf) + + return group + + +class ShowFreq1CosExpDecay(ManipulateSinExpSurface): + CONFIG = { + "freq": 1, + "alpha": 0.2, + } + + def construct(self): + self.setup_axes() + self.add_back_y_axis() + self.initialize_parameter_trackers() + self.phi_tracker.set_value(0) + self.omega_tracker.set_value( + self.freq * TAU / 10, + ) + # + self.show_decay() + + def add_back_y_axis(self): + axes = self.axes + self.add(axes.y_axis) + self.remove(axes.y_axis.numbers) + self.remove(axes.y_axis.label) + + def show_decay(self): + axes = self.axes + t_tracker = self.t_tracker + t_max = self.axes.y_max + + graph = always_redraw( + lambda: self.get_time_slice_graph( + axes, + self.func, + t=t_tracker.get_value(), + stroke_width=5, + ) + ) + + surface = self.get_surface( + self.axes, self.func, + ) + plane = self.get_const_time_plane(axes) + + self.add(surface, plane, graph) + + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-85 * DEGREES, + ) + self.begin_ambient_camera_rotation(rate=0.01) + self.play( + t_tracker.set_value, t_max, + plane.t_tracker.set_value, t_max, + run_time=t_max, + rate_func=linear, + ) + + +class ShowFreq2CosExpDecay(ShowFreq1CosExpDecay): + CONFIG = { + "freq": 2, + } + + +class ShowFreq4CosExpDecay(ShowFreq1CosExpDecay): + CONFIG = { + "freq": 4, + } + + +class ShowHarmonics(SimulateRealSineCurve): + CONFIG = { + "rod_opacity": 0.75, + "initial_omega": 1.27, + "default_n_rod_pieces": 32, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_rod() + self.rod.add_updater(self.color_rod_by_graph) + # + self.show_formula() + + def add_graph(self): + omega_tracker = ValueTracker(self.initial_omega) + get_omega = omega_tracker.get_value + graph = always_redraw( + lambda: self.axes.get_graph( + lambda x: np.cos(get_omega() * x), + x_min=self.graph_x_min, + x_max=self.graph_x_max, + step_size=self.step_size, + discontinuities=[5], + ).color_using_background_image("VerticalTempGradient") + ) + + self.add(graph) + self.graph = graph + self.omega_tracker = omega_tracker + + def show_formula(self): + rod = self.rod + graph = self.graph + axes = self.axes + omega_tracker = self.omega_tracker + L = TAU + + formula = TexMobject( + "=", "\\cos\\left(", + "2(\\pi / L)", "\\cdot", "{x}", + "\\right)", + tex_to_color_map={ + "{x}": GREEN, + } + ) + formula.next_to( + self.axes.y_axis.label, RIGHT, SMALL_BUFF + ) + omega_part = formula.get_part_by_tex("\\pi") + omega_brace = Brace(omega_part, DOWN) + omega = TexMobject("\\omega") + omega.set_color(MAROON_B) + omega.next_to(omega_brace, DOWN, SMALL_BUFF) + formula.remove(omega_part) + + pi_over_L = TexMobject("(\\pi / L)") + pi_over_L.move_to(omega_part) + pi_over_L.match_color(omega) + + self.add(formula) + self.add(omega_brace) + self.add(omega) + + self.remove(graph) + self.play(GrowFromEdge(rod, LEFT)) + self.play( + ShowCreationThenFadeAround(axes.x_axis.label) + ) + self.wait() + self.play(FadeIn(graph)) + self.play(FadeInFromDown(pi_over_L)) + self.play( + omega_tracker.set_value, PI / L, + run_time=2 + ) + self.wait() + + # Show x + x_tracker = ValueTracker(0) + tip = ArrowTip( + start_angle=-90 * DEGREES, + color=WHITE, + ) + tip.add_updater(lambda m: m.move_to( + axes.x_axis.n2p(x_tracker.get_value()), + DOWN, + )) + x_sym = TexMobject("x") + x_sym.set_color(GREEN) + x_sym.add_background_rectangle(buff=SMALL_BUFF) + x_sym.add_updater(lambda m: m.next_to(tip, UP, SMALL_BUFF)) + + self.play( + Write(tip), + Write(x_sym), + ) + self.play( + x_tracker.set_value, L, + run_time=3, + ) + self.wait() + self.play(TransformFromCopy( + x_sym, formula.get_part_by_tex("{x}") + )) + self.play( + FadeOut(tip), + FadeOut(x_sym), + ) + + # Harmonics + pi_over_L.generate_target() + n_sym = Integer(2) + n_sym.match_color(pi_over_L) + group = VGroup(n_sym, pi_over_L.target) + group.arrange(RIGHT, buff=SMALL_BUFF) + group.move_to(pi_over_L) + + self.play( + MoveToTarget(pi_over_L), + FadeInFromDown(n_sym), + ApplyMethod( + omega_tracker.set_value, 2 * PI / L, + run_time=2, + ) + ) + self.wait() + for n in [*range(3, 9), 0]: + new_n_sym = Integer(n) + new_n_sym.move_to(n_sym, DR) + new_n_sym.match_style(n_sym) + self.play( + FadeOutAndShift(n_sym, UP), + FadeInFrom(new_n_sym, DOWN), + omega_tracker.set_value, n * PI / L, + ) + self.wait() + n_sym = new_n_sym + + # + def add_labels_to_axes(self): + x_axis = self.axes.x_axis + L = TexMobject("L") + L.next_to(x_axis.get_end(), DOWN) + x_axis.add(L) + x_axis.label = L + + +class ShowHarmonicSurfaces(ManipulateSinExpSurface): + CONFIG = { + "alpha": 0.2, + "initial_phi": 0, + "initial_omega": PI / 10, + "n_iterations": 8, + "default_surface_config": { + "resolution": (40, 30), + "surface_piece_config": { + "stroke_width": 0.5, + } + } + } + + def construct(self): + self.setup_axes() + self.initialize_parameter_trackers() + self.add_surface() + self.add_graph() + self.show_all_harmonic_surfaces() + + def setup_axes(self): + super().setup_axes() + self.add(self.axes.y_axis) + self.set_camera_orientation( + phi=82 * DEGREES, + theta=-80 * DEGREES, + ) + + def add_surface(self): + self.surface = self.get_cos_exp_surface() + self.add(self.surface) + + def add_graph(self): + self.graph = self.get_graph() + self.add(self.graph) + + def show_all_harmonic_surfaces(self): + omega_tracker = self.omega_tracker + formula = self.get_formula(str(1)) + L = self.axes.x_max + + self.begin_ambient_camera_rotation(rate=0.01) + self.add_fixed_in_frame_mobjects(formula) + self.wait(2) + for n in range(2, self.n_iterations): + if n > 5: + n_str = "n" + else: + n_str = str(n) + new_formula = self.get_formula(n_str) + self.play( + Transform(formula, new_formula), + ApplyMethod( + omega_tracker.set_value, + n * PI / L + ), + ) + self.wait(3) + + # + def get_formula(self, n_str): + n_str = "{" + n_str + "}" + result = TexMobject( + "\\cos\\left(", + n_str, "(", "\\pi / L", ")", "{x}" + "\\right)" + "e^{-\\alpha (", n_str, "\\pi / L", ")^2", + "{t}}", + tex_to_color_map={ + "{x}": GREEN, + "{t}": YELLOW, + "\\pi / L": MAROON_B, + n_str: MAROON_B, + } + ) + result.to_edge(UP) + return result + + +class Thumbnail(ShowHarmonicSurfaces): + CONFIG = { + "default_surface_config": { + "resolution": (40, 30), + # "resolution": (10, 10), + }, + "graph_config": { + "stroke_width": 8, + }, + } + + def construct(self): + self.setup_axes() + self.initialize_parameter_trackers() + self.add_surface() + self.add_graph() + # + self.omega_tracker.set_value(3 * PI / 10) + self.set_camera_orientation( + theta=-70 * DEGREES, + ) + + axes = self.axes + for axis in [axes.y_axis, axes.z_axis]: + axis.numbers.set_opacity(0) + axis.remove(*axis.numbers) + axes.x_axis.label.set_opacity(0) + axes.z_axis.label.set_opacity(0) + + for n in range(2, 16, 2): + new_graph = self.get_time_slice_graph( + axes, self.func, t=n, + **self.graph_config + ) + new_graph.set_shade_in_3d(True) + new_graph.set_stroke( + width=8 / np.sqrt(n), + # opacity=1 / n**(1 / 4), + ) + self.add(new_graph) + + words = TextMobject( + "Sine waves + Linearity + Fourier = Solution" + ) + words.set_width(FRAME_WIDTH - 1) + words.to_edge(DOWN) + words.shift(2 * DOWN) + self.add_fixed_in_frame_mobjects(words) + + self.camera.frame_center.shift(DOWN) + self.update_mobjects(0) + self.surface.set_stroke(width=0.1) + self.surface.set_fill(opacity=0.2) diff --git a/active_projects/diffyq/part3/wordy_scenes.py b/active_projects/diffyq/part3/wordy_scenes.py new file mode 100644 index 00000000..e1160ff8 --- /dev/null +++ b/active_projects/diffyq/part3/wordy_scenes.py @@ -0,0 +1,919 @@ +from manimlib.imports import * +from active_projects.ode.part2.wordy_scenes import * + + +class ThreeMainObservations(Scene): + def construct(self): + fourier = ImageMobject("Joseph Fourier") + name = TextMobject("Joseph Fourier") + name.match_width(fourier) + name.next_to(fourier, DOWN, SMALL_BUFF) + fourier.add(name) + fourier.set_height(5) + fourier.to_corner(DR) + fourier.shift(LEFT) + bubble = ThoughtBubble( + direction=RIGHT, + height=3, + width=4, + ) + bubble.move_tip_to(fourier.get_corner(UL) + 0.5 * DR) + + observations = VGroup( + TextMobject( + "1)", + "Sine = Nice", + ), + TextMobject( + "2)", + "Linearity" + ), + TextMobject( + "3)", + "Fourier series" + ), + ) + # heart = SuitSymbol("hearts") + # heart.replace(observations[0][2]) + # observations[0][2].become(heart) + # observations[0][1].add(happiness) + # observations[2][2].align_to( + # observations[2][1], LEFT, + # ) + + observations.arrange( + DOWN, + aligned_edge=LEFT, + buff=2 * LARGE_BUFF, + ) + observations.set_height(FRAME_HEIGHT - 2) + observations.to_corner(UL, buff=LARGE_BUFF) + + self.add(fourier) + self.play(ShowCreation(bubble)) + self.wait() + self.play(LaggedStart(*[ + TransformFromCopy(bubble, observation[0]) + for observation in observations + ], lag_ratio=0.2)) + self.play( + FadeOut(fourier), + FadeOut(bubble), + ) + self.wait() + for obs in observations: + self.play(FadeInFrom(obs[1], LEFT)) + self.wait() + + +class LastChapterWrapper(Scene): + def construct(self): + full_rect = FullScreenFadeRectangle( + fill_color=DARK_GREY, + fill_opacity=1, + ) + rect = ScreenRectangle(height=6) + rect.set_stroke(WHITE, 2) + rect.set_fill(BLACK, 1) + title = TextMobject("Last chapter") + title.scale(2) + title.to_edge(UP) + rect.next_to(title, DOWN) + + self.add(full_rect) + self.play( + FadeIn(rect), + Write(title, run_time=2), + ) + self.wait() + + +class ThreeConstraints(WriteHeatEquationTemplate): + def construct(self): + self.cross_out_solving() + self.show_three_conditions() + + def cross_out_solving(self): + equation = self.get_d1_equation() + words = TextMobject("Solve this equation") + words.to_edge(UP) + equation.next_to(words, DOWN) + cross = Cross(words) + + self.add(words, equation) + self.wait() + self.play(ShowCreation(cross)) + self.wait() + + self.equation = equation + self.to_remove = VGroup(words, cross) + + def show_three_conditions(self): + equation = self.equation + to_remove = self.to_remove + + title = TexMobject( + "\\text{Constraints }" + "T({x}, {t})" + "\\text{ must satisfy:}", + **self.tex_mobject_config + ) + title.to_edge(UP) + + items = VGroup( + TextMobject("1)", "The PDE"), + TextMobject("2)", "Boundary condition"), + TextMobject("3)", "Initial condition"), + ) + items.scale(0.7) + items.arrange(RIGHT, buff=LARGE_BUFF) + items.set_width(FRAME_WIDTH - 2) + items.next_to(title, DOWN, LARGE_BUFF) + items[1].set_color(MAROON_B) + items[2].set_color(RED) + + bc_paren = TextMobject("(Explained soon)") + bc_paren.scale(0.7) + bc_paren.next_to(items[1], DOWN) + + self.play( + FadeInFromDown(title), + FadeOutAndShift(to_remove, UP), + equation.scale, 0.6, + equation.next_to, items[0], DOWN, + equation.shift_onto_screen, + LaggedStartMap(FadeIn, [ + items[0], + items[1][0], + items[2][0], + ]) + ) + self.wait() + self.play(Write(items[1][1])) + bc_paren.match_y(equation) + self.play(FadeInFrom(bc_paren, UP)) + self.wait(2) + self.play(Write(items[2][1])) + self.wait(2) + + self.title = title + self.items = items + self.pde = equation + self.bc_paren = bc_paren + + +class RectAroundEquation(WriteHeatEquationTemplate): + def construct(self): + eq = self.get_d1_equation() + self.play(ShowCreationThenFadeAround(eq)) + + +class BorderRect(Scene): + def construct(self): + rect = FullScreenFadeRectangle() + rect.set_stroke(WHITE, 3) + rect.set_fill(opacity=0) + self.add(rect) + + +class SeekIdealized(Scene): + def construct(self): + phrases = VGroup(*[ + TextMobject( + "Seek", text, "problems", + tex_to_color_map={ + "realistic": GREEN, + "{idealized}": YELLOW, + "over-idealized": YELLOW, + "general": BLUE, + } + ) + for text in [ + "realistic", + "{idealized}", + "over-idealized", + "general", + ] + ]) + phrases.scale(2) + words = VGroup() + for phrase in phrases: + phrase.center() + word = phrase[1] + words.add(word) + phrase.remove(word) + arrow = Vector(DOWN) + arrow.set_stroke(WHITE, 6) + arrow.next_to(words[3], UP) + low_arrow = arrow.copy() + low_arrow.next_to(words[3], DOWN) + + solutions = TextMobject("solutions") + solutions.scale(2) + solutions.move_to(phrases[3][1], UL) + models = TextMobject("models") + models.scale(2) + models.next_to( + words[0], RIGHT, buff=0.35, + aligned_edge=DOWN + ) + + phrases.center() + phrase = phrases[0] + + self.add(phrase) + self.add(words[0]) + self.wait() + words[0].save_state() + self.play( + words[0].to_edge, DOWN, + words[0].set_opacity, 0.5, + Transform(phrase, phrases[1]), + FadeInFrom(words[1], UP) + ) + self.wait() + # self.play( + # words[1].move_to, words[2], RIGHT, + # FadeIn(words[2]), + # Transform(phrase, phrases[2]) + # ) + # self.wait() + self.play( + words[1].next_to, arrow, UP, + ShowCreation(arrow), + MaintainPositionRelativeTo( + phrase, words[1] + ), + FadeInFrom(solutions, LEFT), + FadeIn(words[3]), + ) + self.wait() + + words[0].generate_target() + words[0].target.next_to(low_arrow, DOWN) + words[0].target.set_opacity(1) + models.shift( + words[0].target.get_center() - + words[0].saved_state.get_center() + ) + self.play( + MoveToTarget(words[0]), + ShowCreation(low_arrow), + FadeInFrom(models, LEFT) + ) + self.wait() + + +class SecondDerivativeOfSine(Scene): + def construct(self): + equation = TexMobject( + "{d^2 \\over d{x}^2}", + "\\cos\\left({x}\\right) =", + "-\\cos\\left({x}\\right)", + tex_to_color_map={ + "{x}": GREEN, + } + ) + + self.add(equation) + + +class EquationAboveSineAnalysis(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation() + equation.to_edge(UP) + equation.shift(2 * LEFT) + eq_index = equation.index_of_part_by_tex("=") + lhs = equation[:eq_index] + eq = equation[eq_index] + rhs = equation[eq_index + 1:] + t_terms = equation.get_parts_by_tex("{t}")[1:] + t_terms.save_state() + zeros = VGroup(*[ + TexMobject("0").replace(t, dim_to_match=1) + for t in t_terms + ]) + zeros.align_to(t_terms, DOWN) + new_rhs = TexMobject( + "=", "-\\alpha \\cdot {T}", "({x}, 0)", + **self.tex_mobject_config + ) + # new_rhs.move_to(equation.get_right()) + # new_rhs.next_to(equation, DOWN, MED_LARGE_BUFF) + # new_rhs.align_to(eq, LEFT) + new_rhs.next_to(equation, RIGHT) + new_rhs.shift(SMALL_BUFF * DOWN) + + self.add(equation) + self.play(ShowCreationThenFadeAround(rhs)) + self.wait() + self.play( + FadeOutAndShift(t_terms, UP), + FadeInFrom(zeros, DOWN), + ) + t_terms.fade(1) + self.wait() + self.play( + # VGroup(equation, zeros).next_to, + # new_rhs, LEFT, + FadeIn(new_rhs), + ) + self.wait() + self.play( + VGroup( + lhs[6:], + eq, + rhs, + new_rhs[0], + new_rhs[-3:], + zeros, + ).fade, 0.5, + ) + self.play(ShowCreationThenFadeAround(lhs[:6])) + self.play(ShowCreationThenFadeAround(new_rhs[1:-3])) + self.wait() + + +class ExpVideoWrapper(Scene): + def construct(self): + self.add(FullScreenFadeRectangle( + fill_color=DARKER_GREY, + fill_opacity=1, + )) + + screen = ImageMobject("eoc_chapter5_thumbnail") + screen.set_height(6) + rect = SurroundingRectangle(screen, buff=0) + rect.set_stroke(WHITE, 2) + screen.add(rect) + title = TextMobject("Need a refresher?") + title.scale(1.5) + title.to_edge(UP) + screen.next_to(title, DOWN) + + screen.center() + self.play( + # FadeInFrom(title, LEFT), + FadeInFrom(screen, DOWN), + ) + self.wait() + + +class ShowSinExpDerivatives(WriteHeatEquationTemplate): + CONFIG = { + "tex_mobject_config": { + "tex_to_color_map": { + "{0}": WHITE, + "\\partial": WHITE, + "=": WHITE, + } + } + } + + def construct(self): + pde = self.get_d1_equation_without_inputs() + pde.to_edge(UP) + pde.generate_target() + + new_rhs = TexMobject( + "=- \\alpha \\cdot T", + **self.tex_mobject_config, + ) + new_rhs.next_to(pde, RIGHT) + new_rhs.align_to(pde.get_part_by_tex("alpha"), DOWN) + + equation1 = TexMobject( + "T({x}, {0}) = \\sin\\left({x}\\right)", + **self.tex_mobject_config + ) + equation2 = TexMobject( + "T({x}, {t}) = \\sin\\left({x}\\right)", + "e^{-\\alpha{t}}", + **self.tex_mobject_config + ) + for eq in equation1, equation2: + eq.next_to(pde, DOWN, MED_LARGE_BUFF) + + eq2_part1 = equation2[:len(equation1)] + eq2_part2 = equation2[len(equation1):] + + # Rectangles + exp_rect = SurroundingRectangle(eq2_part2) + exp_rect.set_stroke(RED, 3) + sin_rect = SurroundingRectangle( + eq2_part1[-3:] + ) + sin_rect.set_color(BLUE) + + VGroup(pde.target, new_rhs).center().to_edge(UP) + + # Show proposed solution + self.add(pde) + self.add(equation1) + self.wait() + self.play( + MoveToTarget(pde), + FadeInFrom(new_rhs, LEFT) + ) + self.wait() + self.play( + ReplacementTransform(equation1, eq2_part1), + FadeIn(eq2_part2), + ) + self.play(ShowCreation(exp_rect)) + self.wait() + self.play(FadeOut(exp_rect)) + + # Take partial derivatives wrt x + q_mark = TexMobject("?") + q_mark.next_to(pde.get_part_by_tex("="), UP) + q_mark.set_color(RED) + + arrow1 = Vector(3 * DOWN + 1 * RIGHT, color=WHITE) + arrow1.scale(1.2 / arrow1.get_length()) + arrow1.next_to( + eq2_part2.get_corner(DL), + DOWN, MED_LARGE_BUFF + ) + ddx_label1 = TexMobject( + "\\partial \\over \\partial {x}", + **self.tex_mobject_config, + ) + ddx_label1.scale(0.7) + ddx_label1.next_to( + arrow1.point_from_proportion(0.8), + UR, SMALL_BUFF + ) + + pde_ddx = VGroup( + *pde.get_parts_by_tex("\\partial")[2:], + pde.get_parts_by_tex("\\over")[1], + pde.get_part_by_tex("{x}"), + ) + pde_ddx_rect = SurroundingRectangle(pde_ddx) + pde_ddx_rect.set_color(GREEN) + + eq2_part2_rect = SurroundingRectangle(eq2_part2) + + dx = TexMobject( + "\\cos\\left({x}\\right)", "e^{-\\alpha {t}}", + **self.tex_mobject_config + ) + ddx = TexMobject( + "-\\sin\\left({x}\\right)", "e^{-\\alpha {t}}", + **self.tex_mobject_config + ) + dx.next_to(arrow1, DOWN) + dx.align_to(eq2_part2, RIGHT) + x_shift = arrow1.get_end()[0] - arrow1.get_start()[0] + x_shift *= 2 + dx.shift(x_shift * RIGHT) + arrow2 = arrow1.copy() + arrow2.next_to(dx, DOWN) + arrow2.shift(MED_SMALL_BUFF * RIGHT) + dx_arrows = VGroup(arrow1, arrow2) + + ddx_label2 = ddx_label1.copy() + ddx_label2.shift( + arrow2.get_center() - arrow1.get_center() + ) + ddx.next_to(arrow2, DOWN) + ddx.align_to(eq2_part2, RIGHT) + ddx.shift(2 * x_shift * RIGHT) + + rhs = equation2[-6:] + + self.play( + FadeInFromDown(q_mark) + ) + self.play( + ShowCreation(pde_ddx_rect) + ) + self.wait() + self.play( + LaggedStart( + GrowArrow(arrow1), + GrowArrow(arrow2), + ), + TransformFromCopy( + pde_ddx[0], ddx_label1 + ), + TransformFromCopy( + pde_ddx[0], ddx_label2 + ), + ) + self.wait() + self.play( + TransformFromCopy(rhs, dx) + ) + self.wait() + self.play( + FadeIn(eq2_part2_rect) + ) + self.play( + Transform( + eq2_part2_rect, + SurroundingRectangle(dx[-3:]) + ) + ) + self.play( + FadeOut(eq2_part2_rect) + ) + self.wait() + self.play( + TransformFromCopy(dx, ddx) + ) + self.play( + FadeIn( + SurroundingRectangle(ddx).match_style( + pde_ddx_rect + ) + ) + ) + self.wait() + + # Take partial derivative wrt t + pde_ddt = pde[:pde.index_of_part_by_tex("=") - 1] + pde_ddt_rect = SurroundingRectangle(pde_ddt) + + dt_arrow = Arrow( + arrow1.get_start(), + arrow2.get_end() + RIGHT, + buff=0 + ) + dt_arrow.flip(UP) + dt_arrow.next_to(dx_arrows, LEFT, MED_LARGE_BUFF) + + dt_label = TexMobject( + "\\partial \\over \\partial {t}", + **self.tex_mobject_config, + ) + dt_label.scale(1) + dt_label.next_to( + dt_arrow.get_center(), UL, + SMALL_BUFF, + ) + + rhs_copy = rhs.copy() + rhs_copy.next_to(dt_arrow.get_end(), DOWN) + rhs_copy.shift(MED_LARGE_BUFF * LEFT) + rhs_copy.match_y(ddx) + + minus_alpha_in_exp = rhs_copy[-3][1:].copy() + minus_alpha_in_exp.set_color(RED) + minus_alpha = TexMobject("-\\alpha") + minus_alpha.next_to(rhs_copy, LEFT) + minus_alpha.align_to(rhs_copy[0][0], DOWN) + dot = TexMobject("\\cdot") + dot.move_to(midpoint( + minus_alpha.get_right(), + rhs_copy.get_left(), + )) + + self.play( + TransformFromCopy( + pde_ddx_rect, + pde_ddt_rect, + ) + ) + self.play( + GrowArrow(dt_arrow), + TransformFromCopy( + pde_ddt, + dt_label, + ) + ) + self.wait() + self.play(TransformFromCopy(rhs, rhs_copy)) + self.play(FadeIn(minus_alpha_in_exp)) + self.play( + ApplyMethod( + minus_alpha_in_exp.replace, minus_alpha, + path_arc=TAU / 4 + ), + FadeIn(dot), + ) + self.play( + FadeIn(minus_alpha), + FadeOut(minus_alpha_in_exp), + ) + self.wait() + rhs_copy.add(minus_alpha, dot) + self.play( + FadeIn(SurroundingRectangle(rhs_copy)) + ) + self.wait() + + # + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.move_to(q_mark, DOWN) + self.play( + FadeInFromDown(checkmark), + FadeOutAndShift(q_mark, UP) + ) + self.wait() + + +class DerivativesOfLinearFunction(WriteHeatEquationTemplate): + CONFIG = { + "tex_mobject_config": { + "tex_to_color_map": { + "{c}": WHITE, + } + } + } + + def construct(self): + func = TexMobject( + "T({x}, {t}) = {c} \\cdot {x}", + **self.tex_mobject_config + ) + dx_T = TexMobject("{c}", **self.tex_mobject_config) + ddx_T = TexMobject("0") + dt_T = TexMobject("0") + + for mob in func, dx_T, ddx_T, dt_T: + mob.scale(1.5) + + func.generate_target() + + arrows = VGroup(*[ + Vector(1.5 * RIGHT, color=WHITE) + for x in range(3) + ]) + dx_arrows = arrows[:2] + dt_arrow = arrows[2] + dt_arrow.rotate(-TAU / 4) + dx_group = VGroup( + func.target, + dx_arrows[0], + dx_T, + dx_arrows[1], + ddx_T, + ) + dx_group.arrange(RIGHT) + for arrow, char, vect in zip(arrows, "xxt", [UP, UP, RIGHT]): + label = TexMobject( + "\\partial \\over \\partial {%s}" % char, + **self.tex_mobject_config + ) + label.scale(0.7) + label.next_to(arrow.get_center(), vect) + arrow.add(label) + + dt_arrow.shift( + func.target[-3:].get_bottom() + MED_SMALL_BUFF * DOWN - + dt_arrow.get_start(), + ) + dt_T.next_to(dt_arrow.get_end(), DOWN) + + self.play(FadeInFromDown(func)) + self.wait() + self.play( + MoveToTarget(func), + LaggedStartMap(Write, dx_arrows), + run_time=1, + ) + self.play( + TransformFromCopy(func[-3:], dx_T), + path_arc=-TAU / 4, + ) + self.play( + TransformFromCopy(dx_T, ddx_T), + path_arc=-TAU / 4, + ) + self.wait() + + # dt + self.play(Write(dt_arrow)) + self.play( + TransformFromCopy(func[-3:], dt_T) + ) + self.wait() + + +class FlatAtBoundaryWords(Scene): + def construct(self): + words = self.get_bc_words() + self.play(Write(words)) + self.wait() + + def get_bc_words(self): + return TextMobject( + "Flat at boundary\\\\" + "for all", "${t}$", "$> 0$", + ) + + +class WriteOutBoundaryCondition(FlatAtBoundaryWords, ThreeConstraints, MovingCameraScene): + def construct(self): + self.force_skipping() + ThreeConstraints.construct(self) + self.revert_to_original_skipping_status() + + self.add_ic() + self.write_bc_words() + self.write_bc_equation() + + def add_ic(self): + image = ImageMobject("temp_initial_condition_example") + image.set_width(3) + border = SurroundingRectangle(image, buff=SMALL_BUFF) + border.shift(SMALL_BUFF * UP) + border.set_stroke(WHITE, 2) + group = Group(image, border) + group.next_to(self.items[2], DOWN) + self.add(group) + + def write_bc_words(self): + bc_paren = self.bc_paren + bc_words = self.get_bc_words() + bc_words.match_width(self.items[1][1]) + bc_words.move_to(bc_paren, UP) + bc_words.set_color_by_tex("{t}", YELLOW) + + self.play(ShowCreationThenFadeAround( + VGroup(self.items[0], self.pde) + )) + self.play( + FadeOutAndShift(bc_paren, UP), + FadeInFrom(bc_words, DOWN), + ) + self.wait() + + self.bc_words = bc_words + + def write_bc_equation(self): + bc_words = self.bc_words + + equation = TexMobject( + "{\\partial {T} \\over \\partial {x}}(0, {t}) = ", + "{\\partial {T} \\over \\partial {x}}(L, {t}) = ", + "0", + **self.tex_mobject_config, + ) + equation.next_to(bc_words, DOWN, MED_LARGE_BUFF) + + self.play( + self.camera_frame.shift, 0.8 * DOWN, + ) + self.play(FadeInFrom(equation, UP)) + self.wait() + + +class HeatEquationFrame(WriteHeatEquationTemplate): + def construct(self): + equation = self.get_d1_equation() + equation.to_edge(UP, buff=MED_SMALL_BUFF) + + ddx = equation[-11:] + dt = equation[:11] + + full_rect = FullScreenFadeRectangle( + fill_color=DARK_GREY, + fill_opacity=1, + ) + smaller_rect = ScreenRectangle( + height=6, + fill_color=BLACK, + fill_opacity=1, + stroke_color=WHITE, + stroke_width=2, + ) + smaller_rect.next_to(equation, DOWN) + + self.add(full_rect) + self.add(smaller_rect) + self.add(equation) + self.wait() + self.play(ShowCreationThenFadeAround( + ddx, + surrounding_rectangle_config={ + "stroke_color": GREEN, + } + )) + self.wait() + self.play(ShowCreationThenFadeAround(dt)) + self.wait() + + +class CompareFreqDecays1to2(Scene): + CONFIG = { + "freqs": [1, 2] + } + + def construct(self): + background = FullScreenFadeRectangle( + fill_color=DARKER_GREY, + fill_opacity=1, + ) + + screens = VGroup(*[ + ScreenRectangle( + height=4, + fill_color=BLACK, + fill_opacity=1, + stroke_width=1, + stroke_color=WHITE, + ) + for x in range(2) + ]) + screens.arrange(RIGHT) + screens.set_width(FRAME_WIDTH - 1) + + formulas = VGroup(*[ + self.get_formula(freq) + for freq in self.freqs + ]) + for formula, screen in zip(formulas, screens): + formula.next_to(screen, UP) + + self.add(background) + self.add(screens) + self.add(formulas) + self.wait() + + def get_formula(self, freq): + f_str = str(freq) + return TexMobject( + "\\cos\\left(%s \\cdot {x}\\right)" % f_str, + "e^{-\\alpha \\cdot %s^2 \\cdot {t}}" % f_str, + tex_to_color_map={ + "{x}": GREEN, + "{t}": YELLOW, + f_str: MAROON_B, + } + ) + + +class CompareFreqDecays1to4(CompareFreqDecays1to2): + CONFIG = { + "freqs": [1, 4], + } + + +class CompareFreqDecays2to4(CompareFreqDecays1to2): + CONFIG = { + "freqs": [2, 4], + } + + +class WorryAboutGenerality(TeacherStudentsScene, WriteHeatEquationTemplate): + def construct(self): + eq = self.get_d1_equation() + diffyq = self.get_diffyq_set() + is_in = TexMobject("\\in") + is_in.scale(2) + + group = VGroup(eq, is_in, diffyq) + group.arrange(RIGHT, buff=MED_LARGE_BUFF) + group.to_edge(UP) + + arrow = Vector(DOWN) + arrow.set_stroke(WHITE, 5) + arrow.next_to(eq, DOWN) + themes = TextMobject("Frequent themes") + themes.scale(1.5) + themes.next_to(arrow, DOWN) + + self.play( + self.get_student_changes( + "sad", "tired", "pleading" + ), + self.teacher.change, "raise_right_hand", + FadeInFromDown(eq) + ) + self.play(Write(group[1:])) + self.wait(2) + self.play( + ShowCreation(arrow), + self.get_student_changes(*3 * ["pondering"]), + ) + self.play( + FadeInFrom(themes, UP), + self.get_student_changes(*3 * ["thinking"]), + self.teacher.change, "happy" + ) + self.wait(4) + + + # def get_d1_equation(self): + # result = super().get_d1_equation() + # lp, rp = parens = TexMobject("(", ")") + # parens.match_height(result) + # lp.next_to(result, LEFT, SMALL_BUFF) + # rp.next_to(result, RIGHT, SMALL_BUFF) + # result.add_to_back(lp) + # result.add(rp) + # return result + + def get_diffyq_set(self): + words = TextMobject( + "Differential\\\\equations" + ) + words.scale(1.5) + words.set_color(BLUE) + lb = Brace(words, LEFT) + rb = Brace(words, RIGHT) + return VGroup(lb, words, rb) diff --git a/active_projects/diffyq/part4/fourier_series_scenes.py b/active_projects/diffyq/part4/fourier_series_scenes.py new file mode 100644 index 00000000..d2ea6fee --- /dev/null +++ b/active_projects/diffyq/part4/fourier_series_scenes.py @@ -0,0 +1,242 @@ +from manimlib.imports import * + +from active_projects.ode.part2.fourier_series import FourierOfTrebleClef + + +class ComplexFourierSeriesExample(FourierOfTrebleClef): + CONFIG = { + "file_name": "EighthNote", + "run_time": 10, + "n_vectors": 200, + "n_cycles": 2, + "max_circle_stroke_width": 0.75, + "drawing_height": 5, + "center_point": DOWN, + "top_row_y": 3, + "top_row_label_y": 2, + "top_row_x_spacing": 1.75, + "top_row_copy_scale_factor": 0.9, + "start_drawn": False, + } + + def construct(self): + self.add_vectors_circles_path() + self.add_top_row(self.vectors, self.circles) + self.write_title() + self.highlight_vectors_one_by_one() + self.change_shape() + + def write_title(self): + title = TextMobject("Complex\\\\Fourier series") + title.scale(1.5) + title.to_edge(LEFT) + title.match_y(self.path) + + self.wait(5) + self.play(FadeInFromDown(title)) + self.wait(2) + self.title = title + + def highlight_vectors_one_by_one(self): + # Don't know why these vectors can't get copied. + # That seems like a problem that will come up again. + labels = self.top_row[-1] + next_anims = [] + for vector, circle, label in zip(self.vectors, self.circles, labels): + # v_color = vector.get_color() + c_color = circle.get_color() + c_stroke_width = circle.get_stroke_width() + + rect = SurroundingRectangle(label, color=PINK) + self.play( + # vector.set_color, PINK, + circle.set_stroke, RED, 3, + FadeIn(rect), + *next_anims + ) + self.wait() + next_anims = [ + # vector.set_color, v_color, + circle.set_stroke, c_color, c_stroke_width, + FadeOut(rect), + ] + self.play(*next_anims) + + def change_shape(self): + # path_mob = TexMobject("\\pi") + path_mob = SVGMobject("Nail_And_Gear") + new_path = path_mob.family_members_with_points()[0] + new_path.set_height(4) + new_path.move_to(self.path, DOWN) + new_path.shift(0.5 * UP) + new_coefs = self.get_coefficients_of_path(new_path) + new_vectors = self.get_rotating_vectors( + coefficients=new_coefs + ) + new_drawn_path = self.get_drawn_path(new_vectors) + + self.vector_clock.set_value(0) + self.vector_clock.suspend_updating(0) + + vectors = self.vectors + anims = [] + + for vect, new_vect in zip(vectors, new_vectors): + new_vect.update() + new_vect.clear_updaters() + + line = Line(stroke_width=0) + line.put_start_and_end_on(*vect.get_start_and_end()) + anims.append(ApplyMethod( + line.put_start_and_end_on, + *new_vect.get_start_and_end() + )) + vect.freq = new_vect.freq + vect.phase = new_vect.phase + vect.coefficient = new_vect.coefficient + + vect.line = line + vect.add_updater( + lambda v: v.put_start_and_end_on( + *v.line.get_start_and_end() + ) + ) + anims += [ + FadeOut(self.drawn_path) + ] + + self.play(*anims, run_time=3) + self.vector_clock.resume_updating() + for vect in self.vectors: + vect.remove_updater(vect.updaters[-1]) + + self.add(new_drawn_path) + for n in range(self.n_cycles): + self.run_one_cycle() + + # + def get_path(self): + path = super().get_path() + path.set_height(self.drawing_height) + path.to_edge(DOWN) + return path + + def add_top_row(self, vectors, circles, max_freq=3): + self.top_row = self.get_top_row( + vectors, circles, max_freq + ) + self.add(self.top_row) + + def get_top_row(self, vectors, circles, max_freq=3): + vector_copies = VGroup() + circle_copies = VGroup() + for vector, circle in zip(vectors, circles): + if vector.freq > max_freq: + break + vcopy = vector.copy() + vcopy.clear_updaters() + ccopy = circle.copy() + ccopy.clear_updaters() + ccopy.original = circle + vcopy.original = vector + + vcopy.center_point = np.array([ + vector.freq * self.top_row_x_spacing, + self.top_row_y, + 0 + ]) + ccopy.center_point = vcopy.center_point + vcopy.add_updater(self.update_top_row_vector_copy) + ccopy.add_updater(self.update_top_row_circle_copy) + vector_copies.add(vcopy) + circle_copies.add(ccopy) + + dots = VGroup(*[ + TexMobject("\\dots").next_to( + circle_copies, direction, + MED_LARGE_BUFF, + ) + for direction in [LEFT, RIGHT] + ]) + labels = self.get_top_row_labels(vector_copies) + return VGroup( + vector_copies, + circle_copies, + dots, + labels, + ) + + def update_top_row_vector_copy(self, vcopy): + vcopy.become(vcopy.original) + vcopy.scale(self.top_row_copy_scale_factor) + vcopy.shift(vcopy.center_point - vcopy.get_start()) + return vcopy + + def update_top_row_circle_copy(self, ccopy): + ccopy.become(ccopy.original) + ccopy.scale(self.top_row_copy_scale_factor) + ccopy.move_to(ccopy.center_point) + return ccopy + + def get_top_row_labels(self, vector_copies): + labels = VGroup() + for vector_copy in vector_copies: + freq = vector_copy.freq + label = Integer(freq) + label.move_to(np.array([ + freq * self.top_row_x_spacing, + self.top_row_label_y, + 0 + ])) + labels.add(label) + return labels + + +class ComplexFourierSeriesExampleEnd(ExternallyAnimatedScene): + pass + + +class FourierSeriesExampleWithRectForZoom(ComplexFourierSeriesExample): + CONFIG = { + "n_vectors": 100, + "slow_factor": 0.01, + "rect_scale_factor": 0.15, + "parametric_function_step_size": 0.0001, + "start_drawn": True, + } + + def construct(self): + self.add_vectors_circles_path() + self.circles.set_stroke(opacity=0.5) + rect = self.get_rect() + rect.set_height(self.rect_scale_factor * FRAME_HEIGHT) + rect.add_updater(lambda m: m.move_to( + center_of_mass([ + v.get_end() + for v in self.vectors + ]) + )) + self.add(rect) + self.run_one_cycle() + + def get_rect(self): + return ScreenRectangle( + color=WHITE, + stroke_width=2, + ) + + +class ZoomedInFourierSeriesExample(FourierSeriesExampleWithRectForZoom, MovingCameraScene): + CONFIG = { + "vector_config": { + "max_tip_length_to_length_ratio": 0.15, + "tip_length": 0.05, + } + } + + def setup(self): + ComplexFourierSeriesExample.setup(self) + MovingCameraScene.setup(self) + + def get_rect(self): + return self.camera_frame diff --git a/active_projects/diffyq/part4/pi_creature_scenes.py b/active_projects/diffyq/part4/pi_creature_scenes.py new file mode 100644 index 00000000..69ef9d32 --- /dev/null +++ b/active_projects/diffyq/part4/pi_creature_scenes.py @@ -0,0 +1,22 @@ +from manimlib.imports import * + + +class WhyWouldYouCare(TeacherStudentsScene): + def construct(self): + self.student_says( + "Who cares!", + target_mode="sassy", + student_index=2, + added_anims=[self.teacher.change, "guilty"], + ) + self.wait() + self.play( + RemovePiCreatureBubble(self.students[2]), + self.teacher.change, "raise_right_hand", + self.get_student_changes( + "pondering", "erm", "thinking", + look_at_arg=self.screen, + ) + ) + self.look_at(self.screen) + self.wait(5) diff --git a/active_projects/diffyq/part4/staging.py b/active_projects/diffyq/part4/staging.py new file mode 100644 index 00000000..0e16ca07 --- /dev/null +++ b/active_projects/diffyq/part4/staging.py @@ -0,0 +1,345 @@ +from manimlib.imports import * +from active_projects.ode.part3.temperature_graphs import TemperatureGraphScene +from active_projects.ode.part2.wordy_scenes import WriteHeatEquationTemplate + + +class RelationToOtherVideos(Scene): + CONFIG = { + "camera_config": { + "background_color": DARK_GREY, + }, + } + + def construct(self): + # Show three videos + videos = self.get_video_thumbnails() + brace = Brace(videos, UP) + text = TextMobject("Heat equation") + text.scale(2) + text.next_to(brace, UP) + + self.play( + LaggedStartMap( + FadeInFrom, videos, + lambda m: (m, LEFT), + lag_ratio=0.4, + run_time=2, + ), + GrowFromCenter(brace), + FadeInFromDown(text), + ) + self.wait() + group = Group(text, brace, videos) + + # Show Fourier thinking + fourier = ImageMobject("Joseph Fourier") + fourier.set_height(4) + fourier.to_edge(RIGHT) + group.generate_target() + group.target.to_edge(DOWN) + fourier.align_to(group.target[0], DOWN) + bubble = ThoughtBubble( + direction=RIGHT, + width=3, + height=2, + fill_opacity=0.5, + stroke_color=WHITE, + ) + bubble[-1].shift(0.25 * DOWN + 0.5 * LEFT) + bubble[:-1].rotate(20 * DEGREES) + for mob in bubble[:-1]: + mob.rotate(-20 * DEGREES) + bubble.move_tip_to( + fourier.get_corner(UL) + DOWN + ) + bubble.to_edge(UP, buff=SMALL_BUFF) + + self.play( + MoveToTarget(group), + FadeInFrom(fourier, LEFT) + ) + self.play(Write(bubble, run_time=1)) + self.wait() + + # Discount first two + first_two = videos[:2] + first_two.generate_target() + first_two.target.scale(0.5) + first_two.target.to_corner(DL) + new_brace = Brace(first_two.target, UP) + + self.play( + # fourier.scale, 0.8, + fourier.match_x, new_brace, + fourier.to_edge, UP, + MoveToTarget(first_two), + Transform(brace, new_brace), + text.scale, 0.7, + text.next_to, new_brace, UP, + FadeOutAndShift(bubble, LEFT), + ) + self.play( + videos[2].scale, 1.7, + videos[2].to_corner, UR, + ) + self.wait() + + # + def get_video_thumbnails(self): + thumbnails = Group( + ImageMobject("diffyq_part2_thumbnail"), + ImageMobject("diffyq_part3_thumbnail"), + ImageMobject("diffyq_part4_thumbnail"), + ) + for thumbnail in thumbnails: + thumbnail.set_height(4) + thumbnail.add(SurroundingRectangle( + thumbnail, + color=WHITE, + stroke_width=2, + buff=0 + )) + thumbnails.arrange(RIGHT, buff=LARGE_BUFF) + thumbnails.set_width(FRAME_WIDTH - 1) + return thumbnails + + +class ShowLinearity(WriteHeatEquationTemplate, TemperatureGraphScene): + CONFIG = { + "temp_text": "Temp", + "alpha": 0.1, + "axes_config": { + "z_max": 2, + "z_min": -2, + "z_axis_config": { + "tick_frequency": 0.5, + "unit_size": 1.5, + }, + }, + "default_surface_config": { + "resolution": (16, 16) + # "resolution": (4, 4) + }, + "freqs": [2, 5], + } + + def setup(self): + TemperatureGraphScene.setup(self) + WriteHeatEquationTemplate.setup(self) + + def construct(self): + self.init_camera() + self.add_three_graphs() + self.show_words() + self.add_function_labels() + self.change_scalars() + + def init_camera(self): + self.camera.set_distance(1000) + + def add_three_graphs(self): + axes_group = self.get_axes_group() + axes0, axes1, axes2 = axes_group + freqs = self.freqs + scalar_trackers = Group( + ValueTracker(1), + ValueTracker(1), + ) + graphs = VGroup( + self.get_graph(axes0, [freqs[0]], [scalar_trackers[0]]), + self.get_graph(axes1, [freqs[1]], [scalar_trackers[1]]), + self.get_graph(axes2, freqs, scalar_trackers), + ) + + plus = TexMobject("+").scale(2) + equals = TexMobject("=").scale(2) + plus.move_to(midpoint( + axes0.get_right(), + axes1.get_left(), + )) + equals.move_to(midpoint( + axes1.get_right(), + axes2.get_left(), + )) + + self.add(axes_group) + self.add(graphs) + self.add(plus) + self.add(equals) + + self.axes_group = axes_group + self.graphs = graphs + self.scalar_trackers = scalar_trackers + self.plus = plus + self.equals = equals + + def show_words(self): + equation = self.get_d1_equation() + name = TextMobject("Heat equation") + name.next_to(equation, DOWN) + name.set_color_by_gradient(RED, YELLOW) + group = VGroup(equation, name) + group.to_edge(UP) + + shift_val = 0.5 * RIGHT + + arrow = Vector(1.5 * RIGHT) + arrow.move_to(group) + arrow.shift(shift_val) + linear_word = TextMobject("``Linear''") + linear_word.scale(2) + linear_word.next_to(arrow, RIGHT) + + self.add(group) + self.wait() + self.play( + ShowCreation(arrow), + group.next_to, arrow, LEFT + ) + self.play(FadeInFrom(linear_word, LEFT)) + self.wait() + + def add_function_labels(self): + axes_group = self.axes_group + graphs = self.graphs + + solution_labels = VGroup() + for axes in axes_group: + label = TextMobject("Solution", "$\\checkmark$") + label.set_color_by_tex("checkmark", GREEN) + label.next_to(axes, DOWN) + solution_labels.add(label) + + kw = { + "tex_to_color_map": { + "T_1": BLUE, + "T_2": GREEN, + } + } + T1 = TexMobject("a", "T_1", **kw) + T2 = TexMobject("b", "T_2", **kw) + T_sum = TexMobject("T_1", "+", "T_2", **kw) + T_sum_with_scalars = TexMobject( + "a", "T_1", "+", "b", "T_2", **kw + ) + + T1.next_to(graphs[0], UP) + T2.next_to(graphs[1], UP) + T_sum.next_to(graphs[2], UP) + T_sum.shift(SMALL_BUFF * DOWN) + T_sum_with_scalars.move_to(T_sum) + + a_brace = Brace(T1[0], UP, buff=SMALL_BUFF) + b_brace = Brace(T2[0], UP, buff=SMALL_BUFF) + s1_decimal = DecimalNumber() + s1_decimal.match_color(T1[1]) + s1_decimal.next_to(a_brace, UP, SMALL_BUFF) + s1_decimal.add_updater(lambda m: m.set_value( + self.scalar_trackers[0].get_value() + )) + s2_decimal = DecimalNumber() + s2_decimal.match_color(T2[1]) + s2_decimal.next_to(b_brace, UP, SMALL_BUFF) + s2_decimal.add_updater(lambda m: m.set_value( + self.scalar_trackers[1].get_value() + )) + + self.play( + FadeInFrom(T1[1], DOWN), + FadeInFrom(solution_labels[0], UP), + ) + self.play( + FadeInFrom(T2[1], DOWN), + FadeInFrom(solution_labels[1], UP), + ) + self.wait() + self.play( + TransformFromCopy(T1[1], T_sum[0]), + TransformFromCopy(T2[1], T_sum[2]), + TransformFromCopy(self.plus, T_sum[1]), + *[ + Transform( + graph.copy().set_fill(opacity=0), + graphs[2].copy().set_fill(opacity=0), + remover=True + ) + for graph in graphs[:2] + ] + ) + self.wait() + self.play(FadeInFrom(solution_labels[2], UP)) + self.wait() + + # Show constants + self.play( + FadeIn(T1[0]), + FadeIn(T2[0]), + FadeIn(a_brace), + FadeIn(b_brace), + FadeIn(s1_decimal), + FadeIn(s2_decimal), + FadeOut(T_sum), + FadeIn(T_sum_with_scalars), + ) + + def change_scalars(self): + s1, s2 = self.scalar_trackers + + kw = { + "run_time": 2, + } + for graph in self.graphs: + graph.resume_updating() + self.play(s2.set_value, -0.5, **kw) + self.play(s1.set_value, -0.2, **kw) + self.play(s2.set_value, 1.5, **kw) + self.play(s1.set_value, 1.2) + self.play(s2.set_value, 0.3) + self.wait() + + # + def get_axes_group(self): + axes_group = VGroup(*[ + self.get_axes() + for x in range(3) + ]) + axes_group.arrange(RIGHT, buff=2) + axes_group.set_width(FRAME_WIDTH - 1) + axes_group.to_edge(DOWN, buff=1) + return axes_group + + def get_axes(self): + axes = self.get_three_d_axes() + # axes.input_plane.set_fill(opacity=0) + # axes.input_plane.set_stroke(width=0.5) + # axes.add(axes.input_plane) + self.orient_three_d_mobject(axes) + axes.rotate(-5 * DEGREES, UP) + axes.set_width(4) + axes.x_axis.label.next_to( + axes.x_axis.get_end(), DOWN, + buff=2 * SMALL_BUFF + ) + return axes + + def get_graph(self, axes, freqs, scalar_trackers): + L = axes.x_max + a = self.alpha + + def func(x, t): + scalars = [st.get_value() for st in scalar_trackers] + return np.sum([ + s * np.cos(k * x) * np.exp(-a * (k**2) * t) + for freq, s in zip(freqs, scalars) + for k in [freq * PI / L] + ]) + + def get_surface_graph_group(): + return VGroup( + self.get_surface(axes, func), + self.get_time_slice_graph(axes, func, t=0), + ) + + result = always_redraw(get_surface_graph_group) + result.suspend_updating() + return result diff --git a/active_projects/diffyq/solve_pendulum_ode_sample_code.py b/active_projects/diffyq/solve_pendulum_ode_sample_code.py new file mode 100644 index 00000000..e7076c3c --- /dev/null +++ b/active_projects/diffyq/solve_pendulum_ode_sample_code.py @@ -0,0 +1,63 @@ +import numpy as np + +# Physical constants +g = 9.8 +L = 2 +mu = 0.1 + +THETA_0 = np.pi / 3 # 60 degrees +THETA_DOT_0 = 0 # No initial angular velocity + +# Definition of ODE +def get_theta_double_dot(theta, theta_dot): + return -mu * theta_dot - (g / L) * np.sin(theta) + + +# Solution to the differential equation +def theta(t): + # Initialize changing values + theta = THETA_0 + theta_dot = THETA_DOT_0 + delta_t = 0.01 # Some time step + for time in np.arange(0, t, delta_t): + # Take many little time steps of size delta_t + # until the total time is the function's input + theta_double_dot = get_theta_double_dot( + theta, theta_dot + ) + theta += theta_dot * delta_t + theta_dot += theta_double_dot * delta_t + return theta + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +