diff --git a/active_projects/ode/all_part2_scenes.py b/active_projects/ode/all_part2_scenes.py index 9db9861d..70441f4e 100644 --- a/active_projects/ode/all_part2_scenes.py +++ b/active_projects/ode/all_part2_scenes.py @@ -1,6 +1,7 @@ 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 * OUTPUT_DIRECTORY = "ode/part2" ALL_SCENE_CLASSES = [ @@ -28,4 +29,8 @@ ALL_SCENE_CLASSES = [ TwoDBodyWithManyTemperaturesGraph, TwoDBodyWithManyTemperaturesContour, BringTwoRodsTogether, + ShowEvolvingTempGraphWithArrows, + WriteHeatEquation, + ReactionsToInitialHeatEquation, + TalkThrough1DHeatGraph, ] diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py index c3e0a3be..86677e70 100644 --- a/active_projects/ode/part2/fourier_series.py +++ b/active_projects/ode/part2/fourier_series.py @@ -387,7 +387,9 @@ class FourierNailAndGear(FourierOfTrebleClef): CONFIG = { "height": 6, "n_circles": 200, - "run_time": 10, + "run_time": 100, + "slow_factor": 0.01, + "parametric_function_step_size": 0.0001, "arrow_config": { "tip_length": 0.1, "stroke_width": 2, diff --git a/active_projects/ode/part2/heat_equation.py b/active_projects/ode/part2/heat_equation.py index c1c07768..f56acbf8 100644 --- a/active_projects/ode/part2/heat_equation.py +++ b/active_projects/ode/part2/heat_equation.py @@ -153,7 +153,7 @@ class TwoDBodyWithManyTemperaturesContour(ExternallyAnimatedScene): class BringTwoRodsTogether(Scene): CONFIG = { - "step_size": 0.1, + "step_size": 0.05, "axes_config": { "x_min": -1, "x_max": 11, @@ -162,10 +162,12 @@ class BringTwoRodsTogether(Scene): "y_axis_config": { "unit_size": 0.06, "tick_frequency": 10, - # "numbers_with_elongated_ticks": range(20, 100, 20) }, }, - "wait_time": 5, + "graph_x_min": 0, + "graph_x_max": 10, + "wait_time": 30, + "alpha": 1.0, } def construct(self): @@ -173,9 +175,6 @@ class BringTwoRodsTogether(Scene): self.setup_graph() self.setup_clock() - self.add(self.axes) - self.add(self.graph) - self.show_rods() self.show_equilibration() @@ -194,9 +193,9 @@ class BringTwoRodsTogether(Scene): def setup_graph(self): graph = self.axes.get_graph( - lambda x: 90 if x <= 5 else 10, - x_min=0, - x_max=10, + self.initial_function, + x_min=self.graph_x_min, + x_max=self.graph_x_max, step_size=self.step_size, discontinuities=[5], ) @@ -229,34 +228,115 @@ class BringTwoRodsTogether(Scene): self.clock = clock def show_rods(self): - pass + 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): - self.add(self.time_group) - self.add(self.clock) + 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 + ], + ) - self.graph.add_updater(self.update_graph) + 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 update_graph(self, graph, dt, alpha=1.0, n_mini_steps=100): + def initial_function(self, x): + if x <= 5: + return 90 + else: + return 10 + + def update_graph(self, graph, dt, alpha=None, n_mini_steps=100): + 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): - change = np.zeros(points.shape) + y_change = np.zeros(points.shape[0]) dx = points[1][0] - points[0][0] for i in range(len(points)): p = points[i] @@ -270,12 +350,188 @@ class BringTwoRodsTogether(Scene): second_deriv = 0.5 * d2y / dx second_deriv = 0 - change[i][1] = alpha * second_deriv * dt / n_mini_steps + y_change[i] = alpha * second_deriv * dt / n_mini_steps - change[0][1] = change[1][1] - change[-1][1] = change[-2][1] - # change[0][1] = 0 - # change[-1][1] = 0 - points += change + # 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) + ] + d2y = ry - 2 * y + ly + return d2y / (dx**2) + + def get_rod(self, x_min, x_max, n_pieces=20): + 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_color(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, + ) + y = axes.y_axis.p2n( + graph.point_from_proportion(alpha) + ) + return temperature_to_color( + (y - 45) / 45 + ) + + +class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether): + CONFIG = { + "alpha": 0.1, + "n_arrows": 20, + "wait_time": 30, + } + + def construct(self): + self.add_axes() + self.add_graph() + self.add_clock() + self.add_rod() + self.add_arrows() + 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) + + def add_rod(self): + rod = self.rod = self.get_rod(0, 10) + self.add(rod) + + def add_arrows(self): + graph = self.graph + x_min = self.graph_x_min + x_max = self.graph_x_max + + xs = np.linspace(x_min, x_max, self.n_arrows + 2)[1:-1] + arrows = VGroup(*[Vector(DOWN) for x in xs]) + + def update_arrows(arrows): + for x, arrow in zip(xs, arrows): + d2y_dx2 = self.get_second_derivative(x) + mag = np.sign(d2y_dx2) * np.sqrt(abs(d2y_dx2)) + mag = np.clip(mag, -2, 2) + 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 let_play(self): + graph = self.graph + rod = self.rod + clock = self.clock + time_label = self.time_label + + graph.add_updater(self.update_graph) + time_label.add_updater( + lambda d, dt: d.increment_value(dt) + ) + rod.add_updater(self.color_rod_by_graph) + + # return + self.play( + ClockPassesTime( + clock, + run_time=self.wait_time, + hours_passed=self.wait_time, + ), + ) + + # + def initial_function(self, x): + new_x = TAU * x / 10 + return 50 + 20 * np.sum([ + np.sin(new_x), + np.sin(2 * new_x), + 0.5 * np.sin(3 * new_x), + 0.3 * np.sin(4 * new_x), + 0.3 * np.sin(5 * new_x), + 0.2 * np.sin(7 * new_x), + 0.1 * np.sin(21 * new_x), + 0.05 * np.sin(41 * new_x), + ]) + + +class TalkThrough1DHeatGraph(ShowEvolvingTempGraphWithArrows): + def construct(self): + self.add_axes() + self.add_graph() + self.add_rod() + + # + def initial_function(self, x): + new_x = TAU * x / 10 + return 50 + 20 * np.sum([ + np.sin(new_x), + np.sin(2 * new_x), + 0.5 * np.sin(3 * new_x), + 0.3 * np.sin(4 * new_x), + 0.3 * np.sin(5 * new_x), + 0.2 * np.sin(7 * new_x), + ]) diff --git a/active_projects/ode/part2/pi_scenes.py b/active_projects/ode/part2/pi_scenes.py new file mode 100644 index 00000000..ba1d98c1 --- /dev/null +++ b/active_projects/ode/part2/pi_scenes.py @@ -0,0 +1,23 @@ +from big_ol_pile_of_manim_imports import * + + +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) + self.wait(3) diff --git a/active_projects/ode/part2/staging.py b/active_projects/ode/part2/staging.py index 52675785..8bfe90ca 100644 --- a/active_projects/ode/part2/staging.py +++ b/active_projects/ode/part2/staging.py @@ -86,3 +86,70 @@ class PartTwoOfTour(TourOfDifferentialEquations): class CompareODEToPDE(Scene): def construct(self): pass + + +class WriteHeatEquation(Scene): + def construct(self): + d1_words = TextMobject("Heat equation\\\\", "(1 dimension)") + d3_words = TextMobject("Heat equation\\\\", "(3 dimensions)") + + kwargs = { + "tex_to_color_map": { + "{T}": YELLOW, + "{t}": WHITE, + "{x}": GREEN, + "{y}": RED, + "{z}": BLUE, + } + } + d1_equation = TexMobject( + "{\\partial {T} \\over \\partial {t}}({x}, {t})=" + "{\\partial^2 {T} \\over \\partial {x}^2} ({x}, {t})", + **kwargs + ) + d3_equation = 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}", + **kwargs + ) + + d1_group = VGroup(d1_words, d1_equation) + d3_group = VGroup(d3_words, d3_equation) + groups = VGroup(d1_group, d3_group) + for group in groups: + group.arrange(DOWN, buff=MED_LARGE_BUFF) + groups.arrange(RIGHT, buff=2) + groups.to_edge(UP) + + d3_rhs = d3_equation[6:] + 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}", **kwargs) + 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()