diff --git a/active_projects/diffyq/all_part1_scenes.py b/active_projects/diffyq/all_part1_scenes.py index 85f395b9..b82e379a 100644 --- a/active_projects/diffyq/all_part1_scenes.py +++ b/active_projects/diffyq/all_part1_scenes.py @@ -4,7 +4,7 @@ from active_projects.diffyq.part1.pi_scenes import * from active_projects.diffyq.part1.phase_space import * from active_projects.diffyq.part1.wordy_scenes import * -OUTPUT_DIRECTORY = "ode/part1" +OUTPUT_DIRECTORY = "diffyq/part1" SCENES_IN_ORDER = [ WhenChangeIsEasier, VectorFieldTest, diff --git a/active_projects/diffyq/all_part2_scenes.py b/active_projects/diffyq/all_part2_scenes.py index 9b676ccd..47830a1a 100644 --- a/active_projects/diffyq/all_part2_scenes.py +++ b/active_projects/diffyq/all_part2_scenes.py @@ -4,7 +4,7 @@ from active_projects.diffyq.part2.heat_equation import * from active_projects.diffyq.part2.pi_scenes import * from active_projects.diffyq.part2.wordy_scenes import * -OUTPUT_DIRECTORY = "ode/part2" +OUTPUT_DIRECTORY = "diffyq/part2" SCENES_IN_ORDER = [ PartTwoOfTour, HeatEquationIntroTitle, diff --git a/active_projects/diffyq/all_part3_scenes.py b/active_projects/diffyq/all_part3_scenes.py index 126d56ea..e86aa927 100644 --- a/active_projects/diffyq/all_part3_scenes.py +++ b/active_projects/diffyq/all_part3_scenes.py @@ -5,7 +5,7 @@ from active_projects.diffyq.part3.wordy_scenes import * from active_projects.diffyq.part3.discrete_case import * -OUTPUT_DIRECTORY = "ode/part3" +OUTPUT_DIRECTORY = "diffyq/part3" SCENES_IN_ORDER = [ LastChapterWrapper, ThreeConstraints, diff --git a/active_projects/diffyq/all_part4_scenes.py b/active_projects/diffyq/all_part4_scenes.py index 3a3479b3..63ef67de 100644 --- a/active_projects/diffyq/all_part4_scenes.py +++ b/active_projects/diffyq/all_part4_scenes.py @@ -1,15 +1,24 @@ from active_projects.diffyq.part4.staging import * from active_projects.diffyq.part4.fourier_series_scenes import * from active_projects.diffyq.part4.pi_creature_scenes import * +from active_projects.diffyq.part4.three_d_graphs import * +from active_projects.diffyq.part4.temperature_scenes import * -OUTPUT_DIRECTORY = "ode/part4" +OUTPUT_DIRECTORY = "diffyq/part4" SCENES_IN_ORDER = [ ComplexFourierSeriesExample, ComplexFourierSeriesExampleEnd, FourierSeriesExampleWithRectForZoom, ZoomedInFourierSeriesExample, + ZoomedInFourierSeriesExample10xMore, RelationToOtherVideos, WhyWouldYouCare, + ShowLinearity, + CombineSeveralSolutions, + FourierGainsImmortality, + CycleThroughManyLinearCombinations, + StepFunctionExample, + WhichWavesAreAvailable, # Oldies # FourierSeriesIllustraiton, diff --git a/active_projects/diffyq/part2/fourier_series.py b/active_projects/diffyq/part2/fourier_series.py index 6a85ebfc..fc4e475e 100644 --- a/active_projects/diffyq/part2/fourier_series.py +++ b/active_projects/diffyq/part2/fourier_series.py @@ -155,6 +155,9 @@ class FourierCirclesScene(Scene): return path # TODO, this should be a general animated mobect + def get_drawn_path_alpha(self): + return self.get_vector_time() + def get_drawn_path(self, vectors, stroke_width=2, **kwargs): path = self.get_vector_sum_path(vectors, **kwargs) broken_path = CurvesAsSubmobjects(path) @@ -162,7 +165,7 @@ class FourierCirclesScene(Scene): def update_path(path, dt): # alpha = path.curr_time * self.get_slow_factor() - alpha = self.get_vector_time() + alpha = self.get_drawn_path_alpha() n_curves = len(path) for a, sp in zip(np.linspace(0, 1, n_curves), path): b = alpha - a diff --git a/active_projects/diffyq/part2/heat_equation.py b/active_projects/diffyq/part2/heat_equation.py index e48c3d1f..4442db45 100644 --- a/active_projects/diffyq/part2/heat_equation.py +++ b/active_projects/diffyq/part2/heat_equation.py @@ -166,6 +166,9 @@ class BringTwoRodsTogether(Scene): }, "graph_x_min": 0, "graph_x_max": 10, + "midpoint": 5, + "max_temp": 90, + "min_temp": 10, "wait_time": 30, "default_n_rod_pieces": 20, "alpha": 1.0, @@ -200,7 +203,7 @@ class BringTwoRodsTogether(Scene): x_min=self.graph_x_min, x_max=self.graph_x_max, step_size=self.step_size, - discontinuities=[5], + discontinuities=[self.midpoint], ) graph.color_using_background_image("VerticalTempGradient") @@ -315,20 +318,27 @@ class BringTwoRodsTogether(Scene): rods.add_updater(self.update_rods) self.play( - ClockPassesTime( - self.clock, - run_time=self.wait_time, - hours_passed=self.wait_time, - ), + self.get_clock_anim(self.wait_time), FadeOut(labels) ) # + def get_clock_anim(self, time, **kwargs): + config = { + "run_time": time, + "hours_passed": time, + } + config.update(kwargs) + return ClockPassesTime(self.clock, **kwargs) + def initial_function(self, x): - if x <= 5: - return 90 + epsilon = 1e-10 + if x < self.midpoint - epsilon: + return self.max_temp + elif x > self.midpoint + epsilon: + return self.min_temp else: - return 10 + return (self.min_temp + self.max_temp) / 2 def update_graph(self, graph, dt, alpha=None, n_mini_steps=500): if alpha is None: @@ -432,7 +442,10 @@ class BringTwoRodsTogether(Scene): ) def y_to_color(self, y): - return temperature_to_color((y - 45) / 45) + y_max = self.max_temp + y_min = self.min_temp + alpha = inverse_interpolate(y_min, y_max, y) + return temperature_to_color(interpolate(-0.8, 0.8, alpha)) def rod_point_to_color(self, point): return self.y_to_color( diff --git a/active_projects/diffyq/part3/staging.py b/active_projects/diffyq/part3/staging.py index 3df414cc..356a20c5 100644 --- a/active_projects/diffyq/part3/staging.py +++ b/active_projects/diffyq/part3/staging.py @@ -248,9 +248,27 @@ class FourierSeriesIllustraiton(Scene): } } - def construct(self): - n_range = self.n_range + def get_sine_graphs(self, axes): + sine_graphs = VGroup(*[ + axes.get_graph(self.generate_nth_func(n)) + for n in self.n_range + ]) + sine_graphs.set_stroke(width=3) + sine_graphs.set_color_by_gradient( + BLUE, GREEN, RED, YELLOW, PINK, + BLUE, GREEN, RED, YELLOW, PINK, + ) + return sine_graphs + def get_partial_sums(self, axes, sine_graphs): + partial_sums = VGroup(*[ + axes.get_graph(self.generate_kth_partial_sum_func(k + 1)) + for k in range(len(self.n_range)) + ]) + partial_sums.match_style(sine_graphs) + return partial_sums + + def construct(self): axes1 = Axes(**self.axes_config) axes1.x_axis.add_numbers( 0.5, 1, @@ -266,21 +284,8 @@ class FourierSeriesIllustraiton(Scene): 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) + sine_graphs = self.get_sine_graphs(axes1) + partial_sums = self.get_partial_sums(axes1, sine_graphs) sum_tex = self.get_sum_tex() sum_tex.next_to(axes1, DOWN, LARGE_BUFF) diff --git a/active_projects/diffyq/part3/temperature_graphs.py b/active_projects/diffyq/part3/temperature_graphs.py index 23345478..18b5ad5a 100644 --- a/active_projects/diffyq/part3/temperature_graphs.py +++ b/active_projects/diffyq/part3/temperature_graphs.py @@ -137,19 +137,19 @@ class TemperatureGraphScene(SpecialThreeDScene): 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, + "u_min": axes.y_min, + "u_max": axes.y_max, + "v_min": axes.x_min, + "v_max": axes.x_max, "resolution": ( - (axes.x_max - axes.x_min) // axes.x_axis.tick_frequency, (axes.y_max - axes.y_min) // axes.y_axis.tick_frequency, + (axes.x_max - axes.x_min) // axes.x_axis.tick_frequency, ), } config.update(self.default_surface_config) config.update(kwargs) return ParametricSurface( - lambda x, t: axes.c2p( + lambda t, x: axes.c2p( x, t, func(x, t) ), **config @@ -181,13 +181,12 @@ class TemperatureGraphScene(SpecialThreeDScene): fill_color=WHITE, fill_opacity=0.2 ) - plane.add_updater(lambda m: m.move_to( + plane.add_updater(lambda m: m.shift( axes.c2p( axes.x_min, t_tracker.get_value(), axes.z_min, - ), - IN + LEFT, + ) - plane.points[0] )) plane.t_tracker = t_tracker return plane diff --git a/active_projects/diffyq/part4/fourier_series_scenes.py b/active_projects/diffyq/part4/fourier_series_scenes.py index d2ea6fee..bbdd3948 100644 --- a/active_projects/diffyq/part4/fourier_series_scenes.py +++ b/active_projects/diffyq/part4/fourier_series_scenes.py @@ -1,6 +1,6 @@ from manimlib.imports import * -from active_projects.ode.part2.fourier_series import FourierOfTrebleClef +from active_projects.diffyq.part2.fourier_series import FourierOfTrebleClef class ComplexFourierSeriesExample(FourierOfTrebleClef): @@ -200,9 +200,9 @@ class FourierSeriesExampleWithRectForZoom(ComplexFourierSeriesExample): CONFIG = { "n_vectors": 100, "slow_factor": 0.01, - "rect_scale_factor": 0.15, - "parametric_function_step_size": 0.0001, + "rect_scale_factor": 0.1, "start_drawn": True, + "drawing_height": 7, } def construct(self): @@ -211,17 +211,20 @@ class FourierSeriesExampleWithRectForZoom(ComplexFourierSeriesExample): 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.get_rect_center() )) self.add(rect) self.run_one_cycle() + def get_rect_center(self): + return center_of_mass([ + v.get_end() + for v in self.vectors + ]) + def get_rect(self): return ScreenRectangle( - color=WHITE, + color=BLUE, stroke_width=2, ) @@ -231,7 +234,8 @@ class ZoomedInFourierSeriesExample(FourierSeriesExampleWithRectForZoom, MovingCa "vector_config": { "max_tip_length_to_length_ratio": 0.15, "tip_length": 0.05, - } + }, + "parametric_function_step_size": 0.001, } def setup(self): @@ -240,3 +244,66 @@ class ZoomedInFourierSeriesExample(FourierSeriesExampleWithRectForZoom, MovingCa def get_rect(self): return self.camera_frame + + def add_vectors_circles_path(self): + super().add_vectors_circles_path() + for v in self.vectors: + if v.get_stroke_width() < 1: + v.set_stroke(width=1) + + def get_path_end(self, vectors, stroke_width=2, **kwargs): + full_path = self.get_vector_sum_path(vectors, **kwargs) + path = VMobject() + path.set_stroke(YELLOW, stroke_width) + + def update_path(p): + alpha = self.get_vector_time() % 1 + p.pointwise_become_partial( + full_path, 0, np.clip(alpha, 0, 1), + ) + p.points[-1] = vectors[-1].get_end() + + path.add_updater(update_path) + return path + + def get_drawn_path_alpha(self): + return super().get_drawn_path_alpha() - 0.002 + + def get_drawn_path(self, vectors, stroke_width=2, **kwargs): + odp = super().get_drawn_path(vectors, stroke_width, **kwargs) + return VGroup( + odp, + self.get_path_end(vectors, stroke_width, **kwargs), + ) + + +class ZoomedInFourierSeriesExample10xMore(ZoomedInFourierSeriesExample): + CONFIG = { + "vector_config": { + "max_tip_length_to_length_ratio": 0.15 * 0.4, + "tip_length": 0.05 * 0.2, + "max_stroke_width_to_length_ratio": 80, + "stroke_width": 3, + }, + "max_circle_stroke_width": 0.5, + "rect_scale_factor": 0.01, + # "parametric_function_step_size": 0.01, + } + + def get_rect_center(self): + return self.vectors[-1].get_end() + + # def get_drawn_path(self, vectors, stroke_width=2, **kwargs): + # return self.get_path_end(vectors, stroke_width, **kwargs) + + +class FourierSeriesExampleWithRectForZoomTrebleClef(FourierSeriesExampleWithRectForZoom): + CONFIG = { + "file_name": "TrebleClef", + } + + +class ZoomedInFourierSeriesExampleTrebleClef(ZoomedInFourierSeriesExample): + CONFIG = { + "file_name": "TrebleClef", + } diff --git a/active_projects/diffyq/part4/staging.py b/active_projects/diffyq/part4/staging.py index 04cd3af9..fb7eba82 100644 --- a/active_projects/diffyq/part4/staging.py +++ b/active_projects/diffyq/part4/staging.py @@ -1,6 +1,4 @@ from manimlib.imports import * -from active_projects.diffyq.part3.temperature_graphs import TemperatureGraphScene -from active_projects.diffyq.part2.wordy_scenes import WriteHeatEquationTemplate class RelationToOtherVideos(Scene): @@ -104,242 +102,82 @@ class RelationToOtherVideos(Scene): return thumbnails -class ShowLinearity(WriteHeatEquationTemplate, TemperatureGraphScene): +class FourierGainsImmortality(Scene): 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], + "mathematicians": [ + "Pythagoras", + "Euclid", + "Archimedes", + "Fermat", + "Newton", + "Leibniz", + "Johann_Bernoulli2", + "Euler", + "Joseph Fourier", + "Gauss", + "Riemann", + "Cantor", + "Noether", + "Ramanujan", + "Godel", + "Turing", + ] } - 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() + fourier = ImageMobject("Joseph Fourier") + fourier.set_height(5) + fourier.to_edge(LEFT) + name = TextMobject("Joseph Fourier") + name.next_to(fourier, DOWN) - def init_camera(self): - self.camera.set_distance(1000) + immortals = self.get_immortals() + immortals.remove(immortals.fourier) + immortals.to_edge(RIGHT) - 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(), + self.add(fourier, name) + self.play(LaggedStartMap( + FadeIn, immortals, + lag_ratio=0.1, + run_time=2, )) - 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), + TransformFromCopy(fourier, immortals.fourier) ) 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) + def get_immortals(self): + images = Group(*[ + ImageMobject(name) + for name in self.mathematicians ]) - axes_group.arrange(RIGHT, buff=2) - axes_group.set_width(FRAME_WIDTH - 1) - axes_group.to_edge(DOWN, buff=1) - return axes_group + for image in images: + image.set_height(1) + images.arrange_in_grid(n_rows=4) - 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 + last_row = images[-4:] + low_center = last_row.get_center() + last_row.arrange(RIGHT, buff=0.4, center=False) + last_row.move_to(low_center) - def get_graph(self, axes, freqs, scalar_trackers): - L = axes.x_max - a = self.alpha + frame = SurroundingRectangle(images) + frame.set_color(WHITE) + title = TextMobject("Immortals of Math") + title.match_width(frame) + title.next_to(frame, UP) - 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 = Group(title, frame, *images) + result.set_height(FRAME_HEIGHT - 1) + result.to_edge(RIGHT) + for image, name in zip(images, self.mathematicians): + setattr( + result, + name.split(" ")[-1].lower(), + image, ) - - result = always_redraw(get_surface_graph_group) - result.suspend_updating() return result + + +class WhichWavesAreAvailable(Scene): + def construct(self): + pass diff --git a/active_projects/diffyq/part4/temperature_scenes.py b/active_projects/diffyq/part4/temperature_scenes.py new file mode 100644 index 00000000..684207dc --- /dev/null +++ b/active_projects/diffyq/part4/temperature_scenes.py @@ -0,0 +1,241 @@ +from manimlib.imports import * +from active_projects.diffyq.part2.heat_equation import BringTwoRodsTogether +from active_projects.diffyq.part3.staging import FourierSeriesIllustraiton + + +class StepFunctionExample(BringTwoRodsTogether, FourierSeriesIllustraiton): + CONFIG = { + "axes_config": { + "y_min": -1.5, + "y_max": 1.5, + "y_axis_config": { + "unit_size": 2.5, + "tick_frequency": 0.5, + }, + "x_min": 0, + "x_max": 1, + "x_axis_config": { + "unit_size": 8, + "tick_frequency": 0.1, + "include_tip": False, + }, + }, + "graph_x_min": 0, + "graph_x_max": 1, + "midpoint": 0.5, + "min_temp": -1, + "max_temp": 1, + "alpha": 0.25, + "step_size": 0.01, + "n_range": range(1, 41, 2), + } + + def construct(self): + self.setup_axes() + self.setup_graph() + self.setup_clock() + + self.bring_rods_together() + self.let_evolve_for_a_bit() + self.add_labels() + self.compare_to_sine_wave() + self.sum_of_sine_waves() + + def bring_rods_together(self): + rods = VGroup( + self.get_rod(0, 0.5), + self.get_rod(0.5, 1), + ) + rods.add_updater(self.update_rods) + + arrows = VGroup( + Vector(RIGHT).next_to(rods[0], UP), + Vector(LEFT).next_to(rods[1], UP), + ) + + words = VGroup( + TextMobject("Hot").next_to(rods[0], DOWN), + TextMobject("Cold").next_to(rods[1], DOWN), + ) + + for pair in rods, words: + pair.save_state() + pair.space_out_submobjects(1.2) + + black_rects = VGroup(*[ + Square( + side_length=1, + fill_color=BLACK, + fill_opacity=1, + stroke_width=0, + ).move_to(self.axes.c2p(0, u)) + for u in [1, -1] + ]) + black_rects[0].add_updater( + lambda m: m.align_to(rods[0].get_right(), LEFT) + ) + black_rects[1].add_updater( + lambda m: m.align_to(rods[1].get_left(), RIGHT) + ) + + self.add( + self.axes, + self.graph, + self.clock, + ) + self.add(rods, words) + self.add(black_rects) + + kw = { + "run_time": 2, + "rate_func": rush_into, + } + self.play( + Restore(rods, **kw), + Restore(words, **kw), + *map(ShowCreation, arrows) + ) + self.remove(black_rects) + + self.to_fade = VGroup(words, arrows) + self.rods = rods + + def let_evolve_for_a_bit(self): + rods = self.rods + # axes = self.axes + time_label = self.time_label + graph = self.graph + graph.save_state() + + graph.add_updater(self.update_graph) + time_label.next_to(self.clock, DOWN) + time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + rods.add_updater(self.update_rods) + + self.add(time_label) + self.play( + FadeOut(self.to_fade), + self.get_clock_anim(1) + ) + self.play(self.get_clock_anim(3)) + + time_label.clear_updaters() + graph.clear_updaters() + self.play( + self.get_clock_anim( + -4, + run_time=1, + rate_func=smooth, + ), + graph.restore, + time_label.set_value, 0, + ) + self.wait() + + def add_labels(self): + axes = self.axes + y_axis = axes.y_axis + x_axis = axes.x_axis + y_numbers = y_axis.get_number_mobjects( + *np.arange(-1, 1.5, 0.5), + number_config={ + "unit": "^\\circ", + "num_decimal_places": 1, + } + ) + x_numbers = x_axis.get_number_mobjects( + *np.arange(0.25, 1.25, 0.25), + number_config={ + "num_decimal_places": 2, + }, + ) + + self.play(FadeIn(y_numbers)) + self.play(ShowCreationThenFadeAround(y_numbers[-1])) + self.play(ShowCreationThenFadeAround(y_numbers[0])) + self.wait() + self.play( + LaggedStartMap( + FadeInFrom, x_numbers, + lambda m: (m, UP) + ), + self.rods.set_opacity, 0.8, + ) + self.wait() + + def compare_to_sine_wave(self): + phi_tracker = ValueTracker(0) + get_phi = phi_tracker.get_value + k_tracker = ValueTracker(TAU) + get_k = k_tracker.get_value + A_tracker = ValueTracker(1) + get_A = A_tracker.get_value + + sine_wave = always_redraw(lambda: self.axes.get_graph( + lambda x: get_A() * np.sin( + get_k() * x - get_phi() + ), + x_min=self.graph_x_min, + x_max=self.graph_x_max, + ).color_using_background_image("VerticalTempGradient")) + + self.play(ShowCreation(sine_wave, run_time=3)) + self.wait() + self.play(A_tracker.set_value, 1.25) + self.play(A_tracker.set_value, 0.75) + self.play( + phi_tracker.set_value, -PI / 2, + k_tracker.set_value, 3 * TAU, + ) + self.wait() + self.play( + k_tracker.set_value, PI, + A_tracker.set_value, 1, + run_time=3 + ) + self.wait() + + self.sine_wave = sine_wave + + def sum_of_sine_waves(self): + curr_sine_wave = self.sine_wave + axes = self.axes + + sine_graphs = self.get_sine_graphs(axes) + partial_sums = self.get_partial_sums(axes, sine_graphs) + + self.play( + FadeOut(curr_sine_wave), + FadeIn(partial_sums[0]) + ) + # Copy-pasting from superclass...in theory, + # this should be better abstracted, but eh. + curr_partial_sum = partial_sums[0] + pairs = list(zip(sine_graphs, partial_sums))[1:] + for sine_graph, partial_sum in pairs: + anims1 = [ + ShowCreation(sine_graph) + ] + partial_sum.set_stroke(BLACK, 4, background=True) + anims2 = [ + curr_partial_sum.set_stroke, + {"width": 1, "opacity": 0.25}, + curr_partial_sum.set_stroke, + {"width": 0, "background": True}, + ReplacementTransform( + sine_graph, partial_sum, + remover=True + ), + ] + self.play(*anims1) + self.play(*anims2) + curr_partial_sum = partial_sum + + # + def setup_axes(self): + super().setup_axes() + self.axes.shift( + self.axes.c2p(0, 0)[1] * DOWN + ) diff --git a/active_projects/diffyq/part4/three_d_graphs.py b/active_projects/diffyq/part4/three_d_graphs.py new file mode 100644 index 00000000..420b5f74 --- /dev/null +++ b/active_projects/diffyq/part4/three_d_graphs.py @@ -0,0 +1,474 @@ +from manimlib.imports import * +from active_projects.diffyq.part3.temperature_graphs import TemperatureGraphScene +from active_projects.diffyq.part2.wordy_scenes import WriteHeatEquationTemplate + + +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, **kw) + self.play(s2.set_value, 0.3, **kw) + 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, **kwargs): + axes = self.get_three_d_axes(**kwargs) + # 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.func = func + result.suspend_updating() + return result + + +class CombineSeveralSolutions(ShowLinearity): + CONFIG = { + "default_surface_config": { + "resolution": (16, 16), + # "resolution": (4, 4), + }, + "n_top_graphs": 5, + "axes_config": { + "y_max": 15, + }, + "target_scalars": [ + 0.81, -0.53, 0.41, 0.62, -0.95 + ], + "final_run_time": 14, + } + + def construct(self): + self.init_camera() + self.add_all_axes() + self.setup_all_graphs() + self.show_infinite_family() + self.show_sum() + self.show_time_passing() + + def add_all_axes(self): + top_axes_group = VGroup(*[ + self.get_axes( + z_min=-1.25, + z_max=1.25, + z_axis_config={ + "unit_size": 2, + "tick_frequency": 0.5, + }, + ) + for x in range(self.n_top_graphs) + ]) + top_axes_group.arrange(RIGHT, buff=2) + top_axes_group.set_width(FRAME_WIDTH - 1.5) + top_axes_group.to_corner(UL) + dots = TexMobject("\\dots") + dots.next_to(top_axes_group, RIGHT) + + low_axes = self.get_axes() + low_axes.center() + low_axes.scale(1.2) + low_axes.to_edge(DOWN, buff=SMALL_BUFF) + + self.add(top_axes_group) + self.add(dots) + self.add(low_axes) + + self.top_axes_group = top_axes_group + self.low_axes = low_axes + + def setup_all_graphs(self): + scalar_trackers = Group(*[ + ValueTracker(1) + for x in range(self.n_top_graphs) + ]) + freqs = np.arange(self.n_top_graphs) + freqs += 1 + self.top_graphs = VGroup(*[ + self.get_graph(axes, [n], [st]) + for axes, n, st in zip( + self.top_axes_group, + freqs, + scalar_trackers, + ) + ]) + self.low_graph = self.get_graph( + self.low_axes, freqs, scalar_trackers + ) + + self.scalar_trackers = scalar_trackers + + def show_infinite_family(self): + top_axes_group = self.top_axes_group + top_graphs = self.top_graphs + scalar_trackers = self.scalar_trackers + + decimals = self.get_decimals( + top_axes_group, scalar_trackers + ) + + self.play(LaggedStart(*[ + AnimationGroup( + Write(graph[0]), + FadeIn(graph[1]), + ) + for graph in top_graphs + ])) + self.wait() + self.play(FadeIn(decimals)) + for graph in top_graphs: + graph.resume_updating() + + self.play(LaggedStart(*[ + ApplyMethod(st.set_value, value) + for st, value in zip( + scalar_trackers, + self.target_scalars, + ) + ]), run_time=3) + self.wait() + + def show_sum(self): + top_graphs = self.top_graphs + low_graph = self.low_graph + low_graph.resume_updating() + low_graph.update() + + self.play( + LaggedStart(*[ + Transform( + top_graph.copy().set_fill(opacity=0), + low_graph.copy().set_fill(opacity=0), + remover=True, + ) + for top_graph in top_graphs + ]), + FadeIn( + low_graph, + rate_func=squish_rate_func(smooth, 0.7, 1) + ), + run_time=3, + ) + self.wait() + + def show_time_passing(self): + all_graphs = [*self.top_graphs, self.low_graph] + all_axes = [*self.top_axes_group, self.low_axes] + + time_tracker = ValueTracker(0) + get_t = time_tracker.get_value + + anims = [ + ApplyMethod( + time_tracker.set_value, 1, + run_time=1, + rate_func=linear + ) + ] + + for axes, graph_group in zip(all_axes, all_graphs): + graph_group.clear_updaters() + surface, gslice = graph_group + plane = self.get_const_time_plane(axes) + plane.t_tracker.add_updater( + lambda m: m.set_value(get_t()) + ) + gslice.axes = axes + gslice.func = graph_group.func + gslice.add_updater(lambda m: m.become( + self.get_time_slice_graph( + m.axes, m.func, t=get_t() + ) + )) + self.add(gslice) + self.add(plane.t_tracker) + anims.append(FadeIn(plane)) + + self.play(*anims) + run_time = self.final_run_time + self.play( + time_tracker.increment_value, run_time, + run_time=run_time, + rate_func=linear, + ) + + # + def get_decimals(self, axes_group, scalar_trackers): + result = VGroup() + for axes, st in zip(axes_group, scalar_trackers): + decimal = DecimalNumber() + decimal.move_to(axes.get_bottom(), UP) + decimal.shift(SMALL_BUFF * RIGHT) + decimal.set_color(YELLOW) + decimal.scalar_tracker = st + times = TexMobject("\\times") + times.next_to(decimal, LEFT, SMALL_BUFF) + decimal.add_updater(lambda d: d.set_value( + d.scalar_tracker.get_value() + )) + group = VGroup(times, decimal) + group.scale(0.7) + result.add(group) + return result + + +class CycleThroughManyLinearCombinations(CombineSeveralSolutions): + CONFIG = { + "default_surface_config": { + "resolution": (16, 16), + # "resolution": (4, 4), + }, + "n_cycles": 10, + } + + def construct(self): + self.init_camera() + self.add_all_axes() + self.setup_all_graphs() + # + self.cycle_through_superpositions() + + def cycle_through_superpositions(self): + top_graphs = self.top_graphs + low_graph = self.low_graph + scalar_trackers = self.scalar_trackers + self.add(self.get_decimals( + self.top_axes_group, scalar_trackers + )) + + for graph in [low_graph, *top_graphs]: + graph.resume_updating() + self.add(graph) + + nst = len(scalar_trackers) + for x in range(self.n_cycles): + self.play(LaggedStart(*[ + ApplyMethod(st.set_value, value) + for st, value in zip( + scalar_trackers, + 3 * np.random.random(nst) - 1.5 + ) + ]), run_time=3) + self.wait()