From 71400a39413cda1690a44a8fea9ce0d72387f443 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 18 Jun 2018 13:04:58 -0700 Subject: [PATCH] Tiny nudges for of div and curl --- active_projects/div_curl.py | 577 ++++++++++++++++++++++++++++++++++-- 1 file changed, 545 insertions(+), 32 deletions(-) diff --git a/active_projects/div_curl.py b/active_projects/div_curl.py index 40f0edac..dde5b79f 100644 --- a/active_projects/div_curl.py +++ b/active_projects/div_curl.py @@ -4,7 +4,7 @@ DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED] # Quick note to anyone coming to this file with the # intent of recreating animations from the video. Some -# of these, espeically those involving StreamLineAnimation, +# of these, especially those involving StreamLineAnimation, # can take an extremely long time to run, but much of the # computational cost is just for giving subtle little effects # which don't matter too much. Switching the line_anim_class @@ -14,6 +14,8 @@ DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED] # run at production quality. FOX_COLOR = "#DF7F20" +RABBIT_COLOR = "#C6D6EF" + # Helper functions def get_flow_start_points(x_min=-8, x_max=8, @@ -301,6 +303,7 @@ class VectorField(VGroup): "stroke_color": BLACK, "stroke_width": 0.5, "fill_opacity": 1.0, + "vector_config": {}, } def __init__(self, func, **kwargs): @@ -317,19 +320,22 @@ class VectorField(VGroup): point = x * RIGHT + y * UP self.add(self.get_vector(point)) - def get_vector(self, point): + def get_vector(self, point, **kwargs): output = np.array(self.func(point)) norm = np.linalg.norm(output) if norm == 0: output *= 0 else: output *= self.length_func(norm) / norm - vect = Vector(output) + vector_config = dict(self.vector_config) + vector_config.update(kwargs) + vect = Vector(output, **vector_config) vect.shift(point) fill_color = rgb_to_color( self.rgb_gradient_function(np.array([norm]))[0] ) - vect.set_fill(fill_color, self.fill_opacity) + vect.set_color(fill_color) + vect.set_fill(opacity=self.fill_opacity) vect.set_stroke( self.stroke_color, self.stroke_width @@ -456,6 +462,22 @@ class JigglingSubmobjects(ContinualAnimation): # Scenes +class Introduction(MovingCameraScene): + def construct(self): + self.show_divergence() + self.show_curl() + self.show_context() + + def show_divergence(self): + pass + + def show_curl(self): + pass + + def show_context(self): + pass + + class TestVectorField(Scene): CONFIG = { "func": cylinder_flow_vector_field, @@ -476,7 +498,7 @@ class TestVectorField(Scene): self.wait(10) -class Introduction(Scene): +class CylinderModel(Scene): CONFIG = { "production_quality_flow": True, "vector_field_func": cylinder_flow_vector_field, @@ -735,7 +757,7 @@ class Introduction(Scene): ) -class ElectricField(Introduction, MovingCameraScene): +class ElectricField(CylinderModel, MovingCameraScene): def construct(self): self.add_plane() self.add_title() @@ -1232,7 +1254,7 @@ class TotallyToScale(Scene): # TODO: Revisit this -class FluidFlowAsHillGradient(Introduction, ThreeDScene): +class FluidFlowAsHillGradient(CylinderModel, ThreeDScene): CONFIG = { "production_quality_flow": False, } @@ -1756,13 +1778,6 @@ class DivergenceAsNewFunction(Scene): class DivergenceZeroCondition(Scene): def construct(self): - self.add_vector_field() - self.add_title() - self.begin_flow() - self.add_circle() - self.wait(5) - - def add_title(self): title = TextMobject( "For actual (incompressible) fluid flow:" ) @@ -1775,6 +1790,15 @@ class DivergenceZeroCondition(Scene): for mob in title, equation: mob.add_background_rectangle(buff=MED_SMALL_BUFF / 2) self.add_foreground_mobjects(mob) + self.wait(1) + + +class PureCylinderFlow(Scene): + def construct(self): + self.add_vector_field() + self.begin_flow() + self.add_circle() + self.wait(5) def add_vector_field(self): vector_field = VectorField( @@ -1800,18 +1824,35 @@ class DivergenceZeroCondition(Scene): if np.linalg.norm(stream_line.points[0]) < 1: stream_lines.remove(stream_line) + self.modify_flow(stream_lines) + stream_line_animation = StreamLineAnimation(stream_lines) stream_line_animation.update(3) self.add(stream_line_animation) def add_circle(self): - self.add_foreground_mobjects(Circle( + circle = Circle( radius=1, stroke_color=YELLOW, fill_color=BLACK, fill_opacity=1, - )) + num_anchors=24, + ) + self.modify_flow(circle) + self.add_foreground_mobjects() + + def modify_flow(self, mobject): + pass + + +class PureAirfoilFlow(PureCylinderFlow): + def modify_flow(self, mobject): + vect = 0.1 * UP + 0.2 * LEFT + mobject.scale(np.linalg.norm(vect - RIGHT)) + mobject.shift(vect) + mobject.apply_complex_function(joukowsky_map) + return mobject class IntroduceCurl(IntroduceVectorField): @@ -2502,7 +2543,7 @@ class ShowTwoPopulations(Scene): examples.arrange_submobjects(LEFT, buff=2) preditor, prey = words = VGroup( - TextMobject("Preditor"), + TextMobject("Predator"), TextMobject("Prey") ) for mob, word in zip(examples, words): @@ -2524,11 +2565,9 @@ class ShowTwoPopulations(Scene): group[1:], run_time=4, lag_ratio=0.1, + rate_func=lambda t: np.clip(smooth(2 * t), 0, 1) ) - for group in [ - foxes[:self.start_num_foxes], - rabbits[:self.start_num_rabbits], - ] + for group in [foxes, rabbits] ] ) @@ -2536,8 +2575,8 @@ class ShowTwoPopulations(Scene): foxes = self.foxes rabbits = self.rabbits phase_point = VectorizedPoint( - self.start_num_foxes * RIGHT + - self.start_num_rabbits * UP + self.start_num_rabbits * RIGHT + + self.start_num_foxes * UP ) self.add(VectorFieldFlow( phase_point, @@ -2629,7 +2668,8 @@ class ShowTwoPopulations(Scene): return self.get_animal("fox", FOX_COLOR) def get_rabbit(self): - return self.get_animal("rabbit", WHITE) + # return self.get_animal("rabbit", WHITE) + return self.get_animal("bunny", RABBIT_COLOR) def get_pop_labels(self): labels = VGroup( @@ -2773,18 +2813,18 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam self.pop_sizes_updates = pop_sizes_updates def write_differential_equations(self): - variables = ["XX", "YY"] + variables = ["X", "YY"] equations = TexMobject( """ - {dXX \\over dt} = - 30 \\cdot XX - XX \\cdot YY \\\\ + {dX \\over dt} = + X \\cdot (\\alpha - \\beta YY \\,) \\\\ \\quad \\\\ {dYY \\over dt} = - XX \\cdot YY - 30 \\cdot YY + YY \\cdot (\\delta X - \\gamma) """, substrings_to_isolate=variables ) - animals = [self.get_fox().flip(), self.get_rabbit()] + animals = [self.get_rabbit(), self.get_fox().flip()] for char, animal in zip(variables, animals): for part in equations.get_parts_by_tex(char): animal_copy = animal.copy() @@ -3191,7 +3231,7 @@ class ShowDotProduct(MovingCameraScene): def get_product(self, v1, v2): return np.dot(v1.get_vector(), v2.get_vector()) - def add_additional_continual_animations(self): + def add_additional_continual_animations(self, v1, v2): pass @@ -3224,6 +3264,479 @@ class ShowCrossProduct(ShowDotProduct): )) -class NewSceneName(Scene): +class DivergenceTinyNudgesView(MovingCameraScene): + CONFIG = { + "scale_factor": 0.25 + } + def construct(self): - pass + self.add_vector_field() + self.zoom_in() + self.take_tiny_step() + self.show_dot_product() + self.show_circle_of_values() + self.switch_to_curl_words() + self.rotate_difference_vectors() + + def add_vector_field(self): + plane = self.plane = NumberPlane() + + vector_field = self.vector_field = VectorField( + lambda p: 0.4 * np.array([ + np.cos(3 * p[0] * p[1] / 4), + np.sin(TAU * p[1] / 4), + 0 + ]), + length_func=lambda n: max(n, 0.5), + max_magnitude=1.0, + ) + self.add(plane) + self.add(vector_field) + + def zoom_in(self): + point = self.point = 2 * LEFT + UP + vector_field = self.vector_field + sf = self.scale_factor + + self.play( + self.camera_frame.scale, sf, + self.camera_frame.move_to, point, + FadeOut(vector_field), + run_time=2 + ) + vector_field.vector_config.update({ + "rectangular_stem_width": 0.02, + "tip_length": 0.1 + }) + vector = vector_field.get_vector(point) + + input_dot = Dot(point).scale(sf) + input_words = TextMobject("$(x_0, y_0)$").scale(sf) + input_words.next_to(input_dot, DL, SMALL_BUFF * sf) + output_words = TextMobject("Output").scale(sf) + output_words.add_background_rectangle() + output_words.next_to(vector.get_top(), UP, sf * SMALL_BUFF) + output_words.match_color(vector) + + self.add_foreground_mobjects(input_dot) + self.play( + FadeInAndShiftFromDirection(input_dot, SMALL_BUFF * DL), + Write(input_words), + ) + self.play( + GrowArrow(vector), + Write(output_words), + ) + self.wait() + + self.set_variables_as_attrs( + point, vector, input_dot, + input_words, output_words, + ) + + def take_tiny_step(self): + sf = self.scale_factor + vector_field = self.vector_field + point = self.point + vector = self.vector + output_words = self.output_words + input_dot = self.input_dot + + nudge = 0.5 * RIGHT + nudged_point = point + nudge + + new_vector = vector_field.get_vector(nudged_point) + new_vector.set_color(YELLOW) + new_dot = Dot(nudged_point).scale(sf) + step_vector = Arrow( + point, nudged_point, + buff=0, + color=TEAL, + **vector_field.vector_config + ) + step_vector.set_stroke(BLACK, 0.5) + + new_output_words = TextMobject("New output").scale(sf) + new_output_words.add_background_rectangle() + new_output_words.next_to(new_vector.get_end(), UP, sf * SMALL_BUFF) + new_output_words.match_color(new_vector) + step_words = TextMobject("Step").scale(sf) + step_words.next_to(step_vector, UP, buff=0) + step_words.set_color(step_vector.get_fill_color()) + step_words.add_background_rectangle() + small_step_words = TextMobject("(think tiny step)").scale(sf) + small_step_words.next_to( + step_words, RIGHT, + buff=sf * MED_SMALL_BUFF, + ) + small_step_words.add_background_rectangle() + small_step_words.match_style(step_words) + + shifted_vector = vector.copy().shift(nudge) + diff_vector = Arrow( + shifted_vector.get_end(), + new_vector.get_end(), + buff=0, + color=RED, + **vector_field.vector_config + ) + diff_words = TextMobject("Difference").scale(sf) + diff_words.add_background_rectangle() + diff_words.next_to(diff_vector.get_start(), UR, buff=2 * sf * SMALL_BUFF) + diff_words.match_color(diff_vector) + diff_words.rotate( + diff_vector.get_angle(), + about_point=diff_vector.get_start() + ) + + self.play( + GrowArrow(step_vector), + Write(step_words), + ReplacementTransform(input_dot.copy(), new_dot) + ) + self.add_foreground_mobjects(new_dot) + self.play(FadeIn(small_step_words)) + self.play(FadeOut(small_step_words)) + self.play( + ReplacementTransform(vector.copy(), new_vector), + ReplacementTransform(output_words.copy(), new_output_words), + ) + self.wait() + self.play(ReplacementTransform( + vector.copy(), shifted_vector, + path_arc=-TAU / 4 + )) + self.wait() + self.play( + FadeOut(output_words), + FadeOut(new_output_words), + GrowArrow(diff_vector), + Write(diff_words) + ) + self.wait() + self.play( + vector.scale, 0, {"about_point": vector.get_start()}, + shifted_vector.scale, 0, {"about_point": shifted_vector.get_start()}, + ReplacementTransform( + new_vector, + diff_vector.copy().shift(-vector.get_vector()), + remover=True + ), + diff_vector.shift, -vector.get_vector(), + MaintainPositionRelativeTo(diff_words, diff_vector), + run_time=2 + ) + self.wait() + + self.set_variables_as_attrs( + step_vector, step_words, + diff_vector, diff_words, + ) + + def show_dot_product(self): + sf = self.scale_factor + point = self.point + step_vector = self.step_vector + step_words = self.step_words + diff_vector = self.diff_vector + diff_words = self.diff_words + vects = VGroup(step_vector, diff_vector) + + moving_step_vector = step_vector.copy() + moving_diff_vector = diff_vector.copy() + + def update_moving_diff_vector(dv): + step = moving_step_vector.get_vector() + o1 = self.vector_field.get_vector(point).get_vector() + o2 = self.vector_field.get_vector(point + step).get_vector() + diff = o2 - o1 + dv.put_start_and_end_on( + moving_step_vector.get_end(), + moving_step_vector.get_end() + diff, + ) + self.moving_diff_vector_update = ContinualUpdateFromFunc( + moving_diff_vector, + update_moving_diff_vector + ) + self.add(self.moving_diff_vector_update) + + div_text = self.get_operator_text("div") + + step_words_copy = step_words.copy() + diff_words_copy = diff_words.copy() + copies = VGroup(step_words_copy, diff_words_copy) + + substrings = ["Step", "Difference"] + dot_product = TextMobject( + "(Step) $\\cdot$ (Difference)", + substrings_to_isolate=substrings, + arg_separator="", + ).scale(sf) + group = VGroup(div_text, dot_product) + group.arrange_submobjects(RIGHT, buff=sf * MED_SMALL_BUFF) + group.next_to( + self.camera_frame.get_top(), DOWN, + buff=sf * MED_SMALL_BUFF + ) + + for substring, mob, vect in zip(substrings, copies, vects): + part = dot_product.get_part_by_tex(substring) + mob.generate_target() + mob.target.rotate(-vect.get_angle()) + mob.target.replace(part) + # part.set_fill(opacity=0) + part.match_color(mob) + dot_product.add_background_rectangle() + + brace = Brace( + dot_product.copy().scale(1 / sf, about_point=ORIGIN), DOWN, + buff=SMALL_BUFF + ).scale(sf, about_point=ORIGIN) + dp_kwargs = { + "include_sign": True, + } + dot_product_value = DecimalNumber(1.0, **dp_kwargs) + dot_product_value.scale(sf) + dot_product_value.next_to(brace, DOWN, sf * SMALL_BUFF) + dot_product_value_update = ContinualChangingDecimal( + dot_product_value, + lambda a: np.dot( + moving_step_vector.get_vector(), + moving_diff_vector.get_vector(), + ), + **dp_kwargs + ) + + self.play( + Write(dot_product), + LaggedStart(MoveToTarget, copies) + ) + self.remove(copies) + self.play(FadeIn(div_text)) + self.play(ShowPassingFlashAround( + div_text[1:3], + surrounding_rectangle_config={"buff": sf * SMALL_BUFF} + )) + self.add(BackgroundRectangle(dot_product_value)) + self.play( + GrowFromCenter(brace), + Write(dot_product_value), + ) + self.add(dot_product_value_update) + self.wait() + + self.set_variables_as_attrs( + div_text, dot_product, + moving_step_vector, + moving_diff_vector, + dot_product_value, + dot_product_value_update, + brace, + ) + + def show_circle_of_values(self): + point = self.point + moving_step_vector = self.moving_step_vector + moving_diff_vector = self.moving_diff_vector + + all_diff_vectors = VGroup() + all_step_vectors = VGroup() + # Loop around + n_samples = 12 + angle = TAU / n_samples + self.add_foreground_mobjects(self.step_words) + for n in range(n_samples): + self.play( + Rotating( + moving_step_vector, + radians=angle, + about_point=point, + run_time=15.0 / n_samples, + rate_func=None, + ) + ) + step_vector_copy = moving_step_vector.copy() + diff_vector_copy = moving_diff_vector.copy() + diff_vector_copy.set_stroke(BLACK, 0.5) + self.add(step_vector_copy, diff_vector_copy) + all_step_vectors.add(step_vector_copy) + all_diff_vectors.add(diff_vector_copy) + self.remove( + self.step_vector, self.diff_vector, + self.moving_step_vector, self.moving_diff_vector, + self.moving_diff_vector_update, + self.dot_product_value_update + ) + self.remove_foreground_mobjects(self.step_words) + self.play( + FadeOut(self.brace), + FadeOut(self.dot_product_value), + FadeOut(self.step_words), + FadeOut(self.diff_words), + ) + self.wait() + + for s in 0.6, -0.6: + for step, diff in zip(all_step_vectors, all_diff_vectors): + diff.generate_target() + diff.target.put_start_and_end_on( + step.get_end(), + step.get_end() + s * step.get_vector() + ) + self.play( + all_step_vectors.set_fill, {"opacity": 0.5}, + LaggedStart( + MoveToTarget, all_diff_vectors, + run_time=3 + ), + ) + self.wait() + self.show_stream_lines(lambda p: s * (p - point)) + self.wait() + + self.set_variables_as_attrs( + all_step_vectors, all_diff_vectors, + ) + + def switch_to_curl_words(self): + sf = self.scale_factor + div_text = self.div_text + dot_product = self.dot_product + + curl_text = self.get_operator_text("curl") + cross_product = TextMobject( + "(Step) $\\times$ (Difference)", + tex_to_color_map={ + "Step": TEAL, + "Difference": RED + }, + arg_separator="", + ).scale(sf) + cross_product.add_background_rectangle(opacity=1) + + group = VGroup(curl_text, cross_product) + group.arrange_submobjects(RIGHT, buff=sf * MED_SMALL_BUFF) + group.next_to(self.camera_frame.get_top(), sf * DOWN) + + self.play( + dot_product.shift, sf * DOWN, + dot_product.fade, 1, + remover=True + ) + self.play(FadeInAndShiftFromDirection(cross_product, sf * DOWN)) + self.play( + div_text.shift, sf * DOWN, + div_text.fade, 1, + remover=True + ) + self.play(FadeInAndShiftFromDirection(curl_text, sf * DOWN)) + self.wait() + + def rotate_difference_vectors(self): + point = self.point + all_step_vectors = self.all_step_vectors + all_diff_vectors = self.all_diff_vectors + + for s in 0.6, -0.6: + for step, diff in zip(all_step_vectors, all_diff_vectors): + diff.generate_target() + diff.target.put_start_and_end_on( + step.get_end(), + step.get_end() + s * rotate_vector( + step.get_vector(), + 90 * DEGREES + ) + ) + self.play( + LaggedStart( + MoveToTarget, all_diff_vectors, + run_time=2 + ), + ) + self.wait() + self.show_stream_lines( + lambda p: s * rotate_vector((p - point), 90 * DEGREES) + ) + self.wait() + + self.set_variables_as_attrs( + all_step_vectors, all_diff_vectors, + ) + + # Helpers + + def get_operator_text(self, operator): + text = TextMobject( + operator + "\\,", + "$\\textbf{F}(x_0, y_0)\\,$", + "corresponds to average of", + arg_separator="" + ).scale(self.scale_factor) + text.set_color_by_tex(operator, YELLOW) + text.add_background_rectangle() + return text + + def show_stream_lines(self, func): + point = self.point + stream_lines = StreamLines( + func, + start_points_generator_config={ + "x_min": point[0] - 2, + "x_max": point[0] + 2, + "y_min": point[1] - 1, + "y_max": point[1] + 1, + "delta_x": 0.025, + "delta_y": 0.025, + }, + virtual_time=1, + ) + random.shuffle(stream_lines.submobjects) + self.play(LaggedStart( + ShowPassingFlash, + stream_lines, + run_time=4, + )) + + +class IncmpressibleAndIrrotational(Scene): + def construct(self): + div_0 = TextMobject("div$\\textbf{F} = 0$") + curl_0 = TextMobject("curl$\\textbf{F}$ = 0") + incompressible = TextMobject("Incompressible") + irrotational = TextMobject("Irrotational") + + for text in [div_0, curl_0, incompressible, irrotational]: + text.add_background_rectangle(buff=SMALL_BUFF) + + div_0.to_edge(UP) + curl_0.next_to(div_0, DOWN, MED_LARGE_BUFF) + + for op, word in (div_0, incompressible), (curl_0, irrotational): + op.generate_target() + group = VGroup(op.target, word) + group.arrange_submobjects(RIGHT, buff=LARGE_BUFF) + group.move_to(op) + + self.play(FadeInFromDown(div_0)) + self.play(FadeInFromDown(curl_0)) + self.wait() + self.play( + MoveToTarget(div_0), + FadeInFromDown(incompressible), + ) + self.wait() + self.play( + MoveToTarget(curl_0), + FadeInFromDown(irrotational), + ) + self.wait() + + rect = SurroundingRectangle(VGroup(curl_0, irrotational)) + question = TextMobject("Does this actually happen?") + question.next_to(rect, DOWN) + question.match_color(rect) + question.add_background_rectangle() + + self.play(ShowCreation(rect)) + self.play(Write(question)) + self.wait()