From ebbc2d5ef35988cbe5a89b92f0e174892f8faba2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 4 Nov 2018 16:18:41 -0800 Subject: [PATCH] Several more turbulence animations --- active_projects/turbulence.py | 868 ++++++++++++++++++++++++++++++++++ 1 file changed, 868 insertions(+) create mode 100644 active_projects/turbulence.py diff --git a/active_projects/turbulence.py b/active_projects/turbulence.py new file mode 100644 index 00000000..f848b87a --- /dev/null +++ b/active_projects/turbulence.py @@ -0,0 +1,868 @@ +from big_ol_pile_of_manim_imports import * +from old_projects.div_curl import PureAirfoilFlow +from old_projects.div_curl import VectorFieldSubmobjectFlow +from old_projects.div_curl import VectorFieldPointFlow +from old_projects.div_curl import four_swirls_function + + +class CreationDestructionMobject(VMobject): + CONFIG = { + "start_time": 0, + "frequency": 0.25, + "max_ratio_shown": 0.3, + "use_copy": True, + } + + def __init__(self, template, **kwargs): + VMobject.__init__(self, **kwargs) + if self.use_copy: + self.ghost_mob = template.copy().fade(1) + self.add(self.ghost_mob) + else: + self.ghost_mob = template + # Don't add + self.shown_mob = template.deepcopy() + self.shown_mob.clear_updaters() + self.add(self.shown_mob) + self.total_time = self.start_time + + def update(mob, dt): + mob.total_time += dt + period = 1.0 / mob.frequency + unsmooth_alpha = (mob.total_time % period) / period + alpha = bezier([0, 0, 1, 1])(unsmooth_alpha) + mrs = mob.max_ratio_shown + mob.shown_mob.pointwise_become_partial( + mob.ghost_mob, + max(interpolate(-mrs, 1, alpha), 0), + min(interpolate(0, 1 + mrs, alpha), 1), + ) + + self.add_updater(update) + + +class Eddy(VMobject): + CONFIG = { + "cd_mob_config": { + "frequency": 0.2, + "max_ratio_shown": 0.3 + }, + "n_spirils": 5, + "n_layers": 20, + "radius": 1, + "colors": [BLUE_A, BLUE_E], + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + lines = self.get_lines() + # self.add(lines) + self.add(*[ + CreationDestructionMobject(line, **self.cd_mob_config) + for line in lines + ]) + self.randomize_times() + + def randomize_times(self): + for submob in self.submobjects: + if hasattr(submob, "total_time"): + T = 1.0 / submob.frequency + submob.total_time = T * random.random() + + def get_lines(self): + a = 0.2 + return VGroup(*[ + self.get_line(r=self.radius * (1 - a + 2 * a * random.random())) + for x in range(self.n_layers) + ]) + + def get_line(self, r): + return ParametricFunction( + lambda t: r * (t + 1)**(-1) * np.array([ + np.cos(TAU * t), + np.sin(TAU * t), + 0, + ]), + t_min=0.1 * random.random(), + t_max=self.n_spirils, + stroke_width=1, + color=interpolate_color(*self.colors, random.random()) + ) + + +class Chaos(Eddy): + CONFIG = { + "n_lines": 12, + "height": 1, + "width": 2, + "n_midpoints": 4, + "cd_mob_config": { + "use_copy": False, + "frequency": 1, + "max_ratio_shown": 0.8 + } + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + rect = Rectangle(height=self.height, width=self.width) + rect.move_to(ORIGIN, DL) + rect.fade(1) + self.rect = rect + self.add(rect) + + lines = self.get_lines() + self.add(*[ + CreationDestructionMobject(line, **self.cd_mob_config) + for line in lines + ]) + self.randomize_times() + lines.fade(1) + self.add(lines) + + def get_lines(self): + return VGroup(*[ + self.get_line(y) + for y in np.linspace(0, self.height, self.n_lines) + ]) + + def get_line(self, y): + frequencies = [0] + list(2 + 2 * np.random.random(self.n_midpoints)) + [0] + rect = self.rect + line = Line( + y * UP, y * UP + self.width * RIGHT, + stroke_width=1 + ) + line.insert_n_anchor_points(self.n_midpoints) + line.total_time = random.random() + delta_h = self.height / (self.n_lines - 1) + + def update(line, dt): + x0, y0 = rect.get_corner(DL)[:2] + x1, y1 = rect.get_corner(UR)[:2] + line.total_time += dt + xs = np.linspace(x0, x1, self.n_midpoints + 2) + new_anchors = [ + np.array([ + x + 1.0 * delta_h * np.cos(f * line.total_time), + y0 + y + 1.0 * delta_h * np.cos(f * line.total_time), + 0 + ]) + for (x, f) in zip(xs, frequencies) + ] + line.set_points_smoothly(new_anchors) + + line.add_updater(update) + return line + + +class DoublePendulum(VMobject): + CONFIG = { + "start_angles": [3 * PI / 7, 3 * PI / 4], + "color1": BLUE, + "color2": RED, + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + line1 = Line(ORIGIN, UP) + dot1 = Dot(color=self.color1) + dot1.add_updater(lambda d: d.move_to(line1.get_end())) + line2 = Line(UP, 2 * UP) + dot2 = Dot(color=self.color2) + dot2.add_updater(lambda d: d.move_to(line2.get_end())) + self.add(line1, line2, dot1, dot2) + + # Largely copied from https://scipython.com/blog/the-double-pendulum/ + # Pendulum rod lengths (m), bob masses (kg). + L1, L2 = 1, 1 + m1, m2 = 1, 1 + # The gravitational acceleration (m.s-2). + g = 9.81 + + self.state_vect = np.array([ + self.start_angles[0], 0, + self.start_angles[1], 0, + ]) + self.state_vect += np.random.random(4) * 1e-7 + + def update(group, dt): + for x in range(2): + line1, line2 = group.submobjects[:2] + theta1, z1, theta2, z2 = group.state_vect + + c, s = np.cos(theta1 - theta2), np.sin(theta1 - theta2) + + theta1dot = z1 + z1dot = (m2 * g * np.sin(theta2) * c - m2 * s * (L1 * (z1**2) * c + L2 * z2**2) - + (m1 + m2) * g * np.sin(theta1)) / L1 / (m1 + m2 * s**2) + theta2dot = z2 + z2dot = ((m1 + m2) * (L1 * (z1**2) * s - g * np.sin(theta2) + g * np.sin(theta1) * c) + + m2 * L2 * (z2**2) * s * c) / L2 / (m1 + m2 * s**2) + + group.state_vect += 0.5 * dt * np.array([ + theta1dot, z1dot, theta2dot, z2dot, + ]) + group.state_vect[1::2] *= 0.9999 + + p1 = L1 * np.sin(theta1) * RIGHT - L1 * np.cos(theta1) * UP + p2 = p1 + L2 * np.sin(theta2) * RIGHT - L2 * np.cos(theta2) * UP + + line1.put_start_and_end_on(ORIGIN, p1) + line2.put_start_and_end_on(p1, p2) + + self.add_updater(update) + + +class DoublePendulums(VGroup): + def __init__(self, **kwargs): + colors = [BLUE, RED, YELLOW, PINK, MAROON_B, PURPLE, GREEN] + VGroup.__init__( + self, + *[ + DoublePendulum( + color1=random.choice(colors), + color2=random.choice(colors), + ) + for x in range(5) + ], + **kwargs, + ) + + +class Diffusion(VMobject): + CONFIG = { + "height": 1.5, + "n_dots": 1000, + "colors": [RED, BLUE] + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + self.add_dots() + self.add_invisible_circles() + + def add_dots(self): + dots = VGroup(*[Dot() for x in range(self.n_dots)]) + dots.arrange_submobjects_in_grid(buff=SMALL_BUFF) + dots.center() + dots.set_height(self.height) + dots.sort_submobjects(lambda p: p[0]) + dots[:len(dots) // 2].set_color(self.colors[0]) + dots[len(dots) // 2:].set_color(self.colors[1]) + dots.set_fill(opacity=0.8) + self.dots = dots + self.add(dots) + + def add_invisible_circles(self): + circles = VGroup() + for dot in self.dots: + point = dot.get_center() + radius = get_norm(point) + circle = Circle(radius=radius) + circle.rotate(angle_of_vector(point)) + circle.fade(1) + circles.add(circle) + self.add_updater_to_dot(dot, circle) + self.add(circles) + + def add_updater_to_dot(self, dot, circle): + dot.total_time = 0 + radius = get_norm(dot.get_center()) + freq = 0.1 + 0.05 * random.random() + 0.05 / radius + + def update(dot, dt): + dot.total_time += dt + prop = (freq * dot.total_time) % 1 + dot.move_to(circle.point_from_proportion(prop)) + + dot.add_updater(update) + + +class NavierStokesEquations(TexMobject): + CONFIG = { + "tex_to_color_map": { + "\\rho": YELLOW, + "\\mu": RED, + "\\textbf{u}": BLUE, + }, + "width": 10, + } + + def __init__(self, **kwargs): + u_tex = "\\textbf{u}" + TexMobject.__init__( + self, + "\\rho", + "\\left(" + "{\\partial", u_tex, "\\over", + "\\partial", "t}", + "+", + u_tex, "\\cdot", "\\nabla", u_tex, + "\\right)", + "=", + "-", "\\nabla", "p", "+", + "\\mu", "\\nabla^2", u_tex, "+", + "\\frac{1}{3}", "\\mu", "\\nabla", + "(", "\\nabla", "\\cdot", u_tex, ")", "+", + "\\textbf{F}", + **kwargs + ) + self.set_width(self.width) + + +class Test(Scene): + def construct(self): + self.add(DoublePendulums()) + self.wait(30) + +# Scenes + + +class EddyReference(Scene): + CONFIG = { + "radius": 0.5, + "label": "Eddy", + "label": "", + } + + def construct(self): + eddy = Eddy() + new_eddy = eddy.get_lines() + label = TextMobject(self.label) + label.next_to(new_eddy, UP) + + self.play( + LaggedStart(ShowCreationThenDestruction, new_eddy), + FadeIn( + label, + rate_func=there_and_back_with_pause, + ), + run_time=3 + ) + + +class EddyReferenceWithLabel(EddyReference): + CONFIG = { + "label": "Eddy" + } + + +class LargeEddyReference(EddyReference): + CONFIG = { + "radius": 1.5, + "label": "Large eddy" + } + + +class SmallEddyReference(EddyReference): + CONFIG = { + "radius": 0.25, + "label": "Small eddy" + } + + +class SomeTurbulenceEquations(PiCreatureScene): + def construct(self): + randy, morty = self.pi_creatures + navier_stokes = NavierStokesEquations() + line = Line(randy.get_right(), morty.get_left()) + navier_stokes.replace(line, dim_to_match=0) + navier_stokes.scale(1.2) + + distribution = TexMobject( + "E(k)=\\alpha \\epsilon^{2/3}_d k^{-5/3}", + tex_to_color_map={ + "k": GREEN, + "-5/3": YELLOW, + "\\epsilon": BLUE, + "_d": BLUE, + } + ) + distribution.next_to(morty, UL) + brace = Brace(distribution, DOWN, buff=SMALL_BUFF) + brace_words = brace.get_text("Explained soon...") + brace_group = VGroup(brace, brace_words) + + self.play( + Write(navier_stokes), + randy.change, "confused", navier_stokes, + morty.change, "confused", navier_stokes, + ) + self.wait(3) + self.play( + morty.change, "raise_right_hand", distribution, + randy.look_at, distribution, + FadeInFromDown(distribution), + navier_stokes.fade, 0.5, + ) + self.play(GrowFromCenter(brace_group)) + self.play(randy.change, "pondering", distribution) + self.wait(3) + dist_group = VGroup(distribution, brace_group) + self.play( + LaggedStart(FadeOut, VGroup(randy, morty, navier_stokes)), + dist_group.scale, 1.5, + dist_group.center, + dist_group.to_edge, UP, + ) + self.wait() + + def create_pi_creatures(self): + randy, morty = Randolph(), Mortimer() + randy.to_corner(DL) + morty.to_corner(DR) + return (randy, morty) + + +class JokeRingEquation(Scene): + def construct(self): + items = VGroup( + TextMobject("Container with a lip"), + TextMobject("Fill with smoke (or fog)"), + TextMobject("Hold awkwardly"), + ) + line = Line(LEFT, RIGHT).set_width(items.get_width() + 1) + items.add(line) + items.add(TextMobject("Vortex ring")) + items.arrange_submobjects(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + line.shift(LEFT) + plus = TexMobject("+") + plus.next_to(line.get_left(), UR, SMALL_BUFF) + line.add(plus) + items.to_edge(RIGHT) + + point = 3.8 * LEFT + 0.2 * UP + arrow1 = Arrow( + items[0].get_left(), point + 0.8 * UP + 0.3 * RIGHT, + use_rectangular_stem=False, + path_arc=90 * DEGREES, + ) + arrow1.pointwise_become_partial(arrow1, 0, 0.99) + + arrow2 = Arrow( + items[1].get_left(), point, + ) + arrows = VGroup(arrow1, arrow2) + + for i in 0, 1: + self.play( + FadeInFromDown(items[i]), + ShowCreation(arrows[i]) + ) + self.wait() + self.play(LaggedStart(FadeIn, items[2:])) + self.wait() + self.play(FadeOut(arrows)) + self.wait() + + +class VideoOnPhysicsGirlWrapper(Scene): + def construct(self): + rect = ScreenRectangle(height=6) + title = TextMobject("Video on Physics Girl") + title.scale(1.5) + title.to_edge(UP) + rect.next_to(title, DOWN) + + self.add(title) + self.play(ShowCreation(rect)) + self.wait() + + +class LightBouncingOffFogParticle(Scene): + def construct(self): + words = TextMobject( + "Light bouncing\\\\", + "off fog particles" + ) + arrow = Vector(UP + 0.5 * RIGHT) + arrow.next_to(words, UP) + arrow.set_color(WHITE) + + self.add(words) + self.play(GrowArrow(arrow)) + self.wait() + + +class NightHawkInLightWrapper(Scene): + def construct(self): + title = TextMobject("NightHawkInLight") + title.scale(1.5) + title.to_edge(UP) + rect = ScreenRectangle(height=6) + rect.next_to(title, DOWN) + self.add(title) + self.play(ShowCreation(rect)) + self.wait() + + +class CarefulWithLasers(TeacherStudentsScene): + def construct(self): + morty = self.teacher + randy = self.students[1] + randy2 = self.students[2] + # randy.change('hooray') + laser = VGroup( + Rectangle( + height=0.1, + width=0.3, + fill_color=LIGHT_GREY, + fill_opacity=1, + stroke_color=DARK_GREY, + stroke_width=1, + ), + Line(ORIGIN, 10 * RIGHT, color=GREEN_SCREEN) + ) + laser.arrange_submobjects(RIGHT, buff=0) + laser.rotate(45 * DEGREES) + laser.shift(randy.get_corner(UR) - laser[0].get_center() + 0.1 * DR) + + laser.time = 0 + + def update_laser(laser, dt): + laser.time += dt + laser.rotate( + 0.5 * dt * np.sin(laser.time), + about_point=laser[0].get_center() + ) + laser.add_updater(update_laser) + + self.play(LaggedStart(FadeInFromDown, self.pi_creatures, run_time=1)) + self.add(self.pi_creatures, laser) + for pi in self.pi_creatures: + pi.add_updater(lambda p: p.look_at(laser[1])) + self.play( + ShowCreation(laser), + self.get_student_changes( + "surprised", "hooray", "horrified", + look_at_arg=laser + ) + ) + self.teacher_says( + "Careful with \\\\ the laser!", + target_mode="angry" + ) + self.wait(2.2) + morty.save_state() + randy2.save_state() + self.play( + morty.blink, randy2.blink, + run_time=0.3 + ) + self.wait(2) + self.play( + morty.restore, randy2.restore, + run_time=0.3 + ) + self.wait(2) + + +class SetAsideTurbulence(PiCreatureScene): + def construct(self): + self.pi_creature_says( + "Forget vortex rings", + target_mode="speaking" + ) + self.wait() + self.pi_creature_says( + "look at that\\\\ turbulence!", + target_mode="surprised" + ) + self.wait() + + def create_pi_creature(self): + morty = Mortimer() + morty.to_corner(DR) + return morty + + +class WavingRodLabel(Scene): + def construct(self): + words = TextMobject( + "(Waving a glass rod \\\\ through the air)" + ) + self.play(Write(words)) + self.wait() + + +class LongEddy(Scene): + def construct(self): + self.add(Eddy()) + self.wait(30) + + +class LongDoublePendulum(Scene): + def construct(self): + self.add(DoublePendulums()) + self.wait(30) + + +class LongDiffusion(Scene): + def construct(self): + self.add(Diffusion()) + self.wait(30) + + +class AskAboutTurbulence(TeacherStudentsScene): + def construct(self): + self.pi_creatures_ask() + self.divide_by_qualitative_quantitative() + self.three_qualitative_descriptors() + self.rigorous_definition() + + def pi_creatures_ask(self): + morty = self.teacher + randy = self.students[1] + morty.change("surprised") + + words = TextMobject("Wait,", "what", "exactly \\\\", "is turbulence?") + question = TextMobject("What", "is turbulence?") + question.to_edge(UP, buff=MED_SMALL_BUFF) + h_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH - 1) + h_line.next_to(question, DOWN, buff=MED_LARGE_BUFF) + + self.student_says( + words, + target_mode='raise_left_hand', + added_anims=[morty.change, 'pondering'] + ) + self.change_student_modes( + "erm", "raise_left_hand", "confused", + ) + self.wait(3) + self.play( + morty.change, "raise_right_hand", + FadeOut(randy.bubble), + ReplacementTransform(VGroup(words[1], words[3]), question), + FadeOut(VGroup(words[0], words[2])), + self.get_student_changes( + *3 * ["pondering"], + look_at_arg=question + ) + ) + self.play( + ShowCreation(h_line), + LaggedStart( + FadeOutAndShiftDown, self.pi_creatures, + run_time=1, + lag_ratio=0.8 + ) + ) + self.wait() + + self.question = question + self.h_line = h_line + + def divide_by_qualitative_quantitative(self): + v_line = Line( + self.h_line.get_center(), + FRAME_HEIGHT * DOWN / 2, + ) + words = VGroup( + TextMobject("Features", color=YELLOW), + TextMobject("Rigorous definition", color=BLUE), + ) + words.next_to(self.h_line, DOWN) + words[0].shift(FRAME_WIDTH * LEFT / 4) + words[1].shift(FRAME_WIDTH * RIGHT / 4) + self.play( + ShowCreation(v_line), + LaggedStart(FadeInFromDown, words) + ) + self.wait() + + def three_qualitative_descriptors(self): + words = VGroup( + TextMobject("- Eddies"), + TextMobject("- Chaos"), + TextMobject("- Diffusion"), + ) + words.arrange_submobjects( + DOWN, buff=1.25, + aligned_edge=LEFT + ) + words.to_edge(LEFT) + words.shift(MED_LARGE_BUFF * DOWN) + + # objects = VGroup( + # Eddy(), + # DoublePendulum(), + # Diffusion(), + # ) + + # for word, obj in zip(words, objects): + for word in words: + # obj.next_to(word, RIGHT) + self.play( + FadeInFromDown(word), + # VFadeIn(obj) + ) + self.wait(3) + + def rigorous_definition(self): + randy = Randolph() + randy.move_to(FRAME_WIDTH * RIGHT / 4) + + self.play(FadeIn(randy)) + self.play(randy.change, "shruggie") + for x in range(2): + self.play(Blink(randy)) + self.wait() + + +class BumpyPlaneRide(Scene): + def construct(self): + plane = SVGMobject(file_name="plane2") + self.add(plane) + + total_time = 0 + while total_time < 10: + point = 2 * np.append(np.random.random(2), 2) + DL + point *= 0.2 + time = 0.2 * random.random() + total_time += time + arc = PI * random.random() - PI / 2 + self.play( + plane.move_to, point, + run_time=time, + path_arc=arc + ) + + +class PureAirfoilFlowCopy(PureAirfoilFlow): + def modify_vector_field(self, vector_field): + PureAirfoilFlow.modify_vector_field(self, vector_field) + vector_field.set_fill(opacity=0.1) + vector_field.set_stroke(opacity=0.1) + + +class LaminarFlowLabel(Scene): + def construct(self): + words = TextMobject("Laminar flow") + words.scale(1.5) + words.to_edge(UP) + subwords = TextMobject( + "`Lamina', in Latin, means \\\\" + "``a thin sheet of material''", + tex_to_color_map={"Lamina": YELLOW}, + arg_separator="", + ) + subwords.next_to(words, DOWN, MED_LARGE_BUFF) + VGroup(words, subwords).set_background_stroke(width=4) + self.play(Write(words)) + self.wait() + self.play(FadeInFromDown(subwords)) + self.wait() + + +class HighCurlFieldBreakingLayers(Scene): + CONFIG = { + "flow_anim": VectorFieldSubmobjectFlow, + } + + def construct(self): + lines = VGroup(*[ + self.get_line() + for x in range(20) + ]) + lines.arrange_submobjects(DOWN, buff=MED_SMALL_BUFF) + lines[0::2].set_color(BLUE) + lines[1::2].set_color(RED) + all_dots = VGroup(*it.chain(*lines)) + + def func(p): + vect = four_swirls_function(p) + norm = get_norm(vect) + if norm > 2: + vect *= 4.0 / get_norm(vect)**2 + return vect + + self.add(lines) + self.add(self.flow_anim(all_dots, func)) + self.wait(16) + + def get_line(self): + line = VGroup(*[Dot() for x in range(100)]) + line.set_height(0.1) + line.arrange_submobjects(RIGHT, buff=0) + line.set_width(10) + return line + + +class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers): + CONFIG = { + "flow_anim": VectorFieldPointFlow + } + + def get_line(self): + line = Line(LEFT, RIGHT) + line.insert_n_anchor_points(500) + line.set_width(5) + return line + + +class VorticitySynonyms(Scene): + def construct(self): + words = VGroup( + TextMobject("High", "vorticity"), + TexMobject( + "\\text{a.k.a} \\,", + "|\\nabla \\times \\vec{\\textbf{v}}| > 0" + ), + TextMobject("a.k.a", "high", "swirly-swirly", "factor"), + ) + words[0].set_color_by_tex("vorticity", BLUE) + words[1].set_color_by_tex("nabla", BLUE) + words[2].set_color_by_tex("swirly", BLUE) + words.arrange_submobjects( + DOWN, + aligned_edge=LEFT, + buff=MED_LARGE_BUFF + ) + + for word in words: + word.add_background_rectangle() + self.play(FadeInFromDown(word)) + self.wait() + + +class VorticityDoesNotImplyTurbulence(TeacherStudentsScene): + def construct(self): + t_to_v = TextMobject( + "Turbulence", "$\\Rightarrow$", "Vorticity", + ) + v_to_t = TextMobject( + "Vorticity", "$\\Rightarrow$", "Turbulence", + ) + for words in t_to_v, v_to_t: + words.move_to(self.hold_up_spot, DR) + words.set_color_by_tex_to_color_map({ + "Vorticity": BLUE, + "Turbulence": GREEN, + }) + v_to_t.submobjects.reverse() + cross = Cross(v_to_t[1]) + + morty = self.teacher + self.play( + morty.change, "raise_right_hand", + FadeInFromDown(t_to_v) + ) + self.wait() + self.play(t_to_v.shift, 2 * UP,) + self.play( + TransformFromCopy(t_to_v, v_to_t, path_arc=PI / 2), + self.get_student_changes( + "erm", "confused", "sassy", + run_time=1 + ), + ShowCreation(cross, run_time=2), + ) + self.add(cross) + self.wait(4) + + +class ShowNavierStokesEquations(Scene): + def construct(self): + pass