diff --git a/active_projects/div_curl.py b/active_projects/div_curl.py index e4173084..3f4b5b87 100644 --- a/active_projects/div_curl.py +++ b/active_projects/div_curl.py @@ -60,7 +60,7 @@ def negative_gradient(potential_func, dt=1e-7): return result -def divergence(vector_func, dt=1e-1): +def divergence(vector_func, dt=1e-7): def result(point): value = vector_func(point) return sum([ @@ -70,6 +70,16 @@ def divergence(vector_func, dt=1e-1): return result +def two_d_curl(vector_func, dt=1e-7): + def result(point): + value = vector_func(point) + return op.add( + (vector_func(point + dt * RIGHT) - value)[1] / dt, + -(vector_func(point + dt * UP) - value)[0] / dt, + ) + return result + + def cylinder_flow_vector_field(point, R=1, U=1): z = R3_to_complex(point) # return complex_to_R3(1.0 / derivative(joukowsky_map)(z)) @@ -186,7 +196,7 @@ def get_force_field_func(*point_strength_pairs, **kwargs): return func -def get_chraged_particle(color, sign, radius=0.1): +def get_charged_particles(color, sign, radius=0.1): result = Circle( stroke_color=WHITE, stroke_width=0.5, @@ -203,13 +213,17 @@ def get_chraged_particle(color, sign, radius=0.1): def get_proton(radius=0.1): - return get_chraged_particle(RED, "+", radius) + return get_charged_particles(RED, "+", radius) def get_electron(radius=0.05): - return get_chraged_particle(BLUE, "-", radius) + return get_charged_particles(BLUE, "-", radius) +def preditor_prey_vector_field(point): + x, y = point[:2] + return -(y - 30) * RIGHT + (x - 30) * UP + # Mobjects @@ -276,6 +290,7 @@ class VectorField(VGroup): "length_func": lambda norm: 0.5 * sigmoid(norm), "stroke_color": BLACK, "stroke_width": 0.5, + "fill_opacity": 1.0, } def __init__(self, func, **kwargs): @@ -301,9 +316,10 @@ class VectorField(VGroup): output *= self.length_func(norm) / norm vect = Vector(output) vect.shift(point) - vect.set_fill(rgb_to_color( + fill_color = rgb_to_color( self.rgb_gradient_function(np.array([norm]))[0] - )) + ) + vect.set_fill(fill_color, self.fill_opacity) vect.set_stroke( self.stroke_color, self.stroke_width @@ -329,14 +345,16 @@ class VectorFieldFlow(ContinualAnimation): def update_mobject(self, dt): self.apply_nudge(dt) - def apply_nudge(self): + def apply_nudge(self, dt): self.mobject.shift(self.func(self.mobject.get_center()) * dt) class VectorFieldSubmobjectFlow(VectorFieldFlow): def apply_nudge(self, dt): for submob in self.mobject: - submob.shift(self.func(submob.get_center()) * dt) + x, y = submob.get_center()[:2] + if abs(x) < FRAME_WIDTH and abs(y) < FRAME_HEIGHT: + submob.shift(self.func(submob.get_center()) * dt) class VectorFieldPointFlow(VectorFieldFlow): @@ -1819,6 +1837,7 @@ class IntroduceCurl(IntroduceVectorField): def show_vector_field(self): vector_field = self.vector_field = VectorField( four_swirls_function, + **self.vector_field_config ) vector_field.submobjects.sort( lambda v1, v2: cmp(v1.get_length(), v2.get_length()) @@ -1871,6 +1890,7 @@ class IntroduceCurl(IntroduceVectorField): ) self.wait(2) for group in counterclockwise_arrows, clockwise_arrows: + self.play(FocusOn(group[0])) self.play( UpdateFromAlphaFunc( group, @@ -1901,3 +1921,736 @@ class IntroduceCurl(IntroduceVectorField): result.flip() result.scale_to_fit_width(width) return result + + +class ShearCurl(IntroduceCurl): + def construct(self): + self.show_vector_field() + self.begin_flow() + self.wait(2) + self.comment_on_relevant_region() + + def show_vector_field(self): + vector_field = self.vector_field = VectorField( + self.func, **self.vector_field_config + ) + vector_field.submobjects.sort( + lambda a1, a2: cmp(a1.get_length(), a2.get_length()) + ) + self.play(LaggedStart(GrowArrow, vector_field)) + + def comment_on_relevant_region(self): + circle = Circle(color=WHITE, radius=0.75) + circle.next_to(ORIGIN, UP, LARGE_BUFF) + self.play(ShowCreation(circle)) + + slow_words, fast_words = words = [ + TextMobject("Slow flow below"), + TextMobject("Fast flow above") + ] + for word, vect in zip(words, [DOWN, UP]): + word.add_background_rectangle(buff=SMALL_BUFF) + word.next_to(circle, vect) + self.add_foreground_mobjects(word) + self.play(Write(word)) + self.wait() + + twig = Rectangle( + height=0.8 * 2 * circle.radius, + width=SMALL_BUFF, + stroke_width=0, + fill_color=GREY_BROWN, + fill_opacity=1, + ) + twig.add(Dot(twig.get_center())) + twig.move_to(circle) + twig_rotation = ContinualRotation( + twig, rate=-90 * DEGREES, + start_up_time=8, + ) + + self.play(FadeInAndShiftFromDirection(twig, UP)) + self.add(twig_rotation) + self.wait(16) + + # Helpers + def func(self, point): + return 0.5 * point[1] * RIGHT + + +class FromKAWrapper(TeacherStudentsScene): + def construct(self): + screen = self.screen + self.play( + self.teacher.change, "raise_right_hand", + self.get_student_changes( + "pondering", "confused", "hooray", + ) + ) + self.look_at(screen) + self.wait(2) + self.change_student_modes("erm", "happy", "confused") + self.wait(3) + self.teacher_says( + "Our focus is \\\\ the 2d version", + bubble_kwargs={"width": 4, "height": 3}, + added_anims=[self.get_student_changes( + "happy", "hooray", "happy" + )] + ) + self.wait() + + +class ShowCurlAtVariousPoints(IntroduceCurl): + CONFIG = { + "func": four_swirls_function, + "sample_points": [ + 4 * RIGHT, + 2 * UP, + 4 * LEFT, + 2 * DOWN, + ORIGIN, + 3 * RIGHT + 2 * UP, + 3 * LEFT + 2 * UP, + ], + "vector_field_config": { + "fill_opacity": 0.75 + }, + "stream_line_config": { + "virtual_time": 5, + "start_points_generator_config": { + "delta_x": 0.25, + "delta_y": 0.25, + } + } + } + + def construct(self): + self.add_plane() + self.show_vector_field() + self.begin_flow() + self.show_curl_at_points() + + def add_plane(self): + plane = NumberPlane() + plane.add_coordinates() + self.add(plane) + self.plane = plane + + def show_curl_at_points(self): + dot = Dot() + circle = Circle(radius=0.25, color=WHITE) + circle.move_to(dot) + circle_update = ContinualUpdateFromFunc( + circle, + lambda m: m.move_to(dot) + ) + + curl_tex = TexMobject( + "\\text{curl} \\, \\textbf{F}(x, y) = " + ) + curl_tex.add_background_rectangle(buff=0.025) + curl_tex_update = ContinualUpdateFromFunc( + curl_tex, + lambda m: m.next_to(circle, UP, SMALL_BUFF) + ) + + curl_func = two_d_curl(self.func) + curl_value = DecimalNumber( + 0, include_sign=True, + include_background_rectangle=True, + ) + curl_value_update = ContinualChangingDecimal( + curl_value, + lambda a: curl_func(dot.get_center()), + position_update_func=lambda m: m.next_to( + curl_tex, RIGHT, buff=0 + ), + include_background_rectangle=True, + include_sign=True, + ) + + points = self.sample_points + self.add(dot, circle_update) + self.play( + dot.move_to, points[0], + VFadeIn(dot), + VFadeIn(circle), + ) + curl_tex_update.update(0) + curl_value_update.update(0) + self.play(Write(curl_tex), FadeIn(curl_value)) + self.add(curl_tex_update, curl_value_update) + self.wait() + for point in points[1:]: + self.play(dot.move_to, point, run_time=3) + self.wait(2) + self.wait(2) + + +class IllustrationUseVennDiagram(Scene): + def construct(self): + title = Title("Divergence \\& Curl") + title.to_edge(UP, buff=MED_SMALL_BUFF) + + useful_for = TextMobject("Useful for") + useful_for.next_to(title, DOWN) + useful_for.set_color(BLUE) + + fluid_flow = TextMobject("Fluid \\\\ flow") + fluid_flow.next_to(ORIGIN, UL) + ff_circle = Circle(color=YELLOW) + ff_circle.surround(fluid_flow, stretch=True) + fluid_flow.match_color(ff_circle) + + big_circle = Circle( + fill_color=BLUE, + fill_opacity=0.2, + stroke_color=BLUE, + ) + big_circle.stretch_to_fit_width(9) + big_circle.stretch_to_fit_height(6) + big_circle.next_to(useful_for, DOWN, SMALL_BUFF) + + illustrated_by = TextMobject("Illustrated by") + illustrated_by.next_to( + big_circle.point_from_proportion(3. / 8), UL + ) + illustrated_by.match_color(ff_circle) + illustrated_by_arrow = Arrow( + illustrated_by.get_bottom(), + ff_circle.get_left(), + path_arc=90 * DEGREES, + use_rectangular_stem=False, + color=YELLOW, + ) + illustrated_by_arrow.pointwise_become_partial( + illustrated_by_arrow, 0, 0.95 + ) + + examples = VGroup( + TextMobject("Electricity"), + TextMobject("Magnetism"), + TextMobject("Phase flow"), + TextMobject("Stokes' theorem"), + ) + points = [ + 2 * RIGHT + 0.5 * UP, + 2 * RIGHT + 0.5 * DOWN, + 2 * DOWN, + 2 * LEFT + DOWN, + ] + for example, point in zip(examples, points): + example.move_to(point) + + self.play(Write(title), run_time=1) + self.play( + Write(illustrated_by), + ShowCreation(illustrated_by_arrow), + run_time=1, + ) + self.play( + ShowCreation(ff_circle), + FadeIn(fluid_flow), + ) + self.wait() + self.play( + Write(useful_for), + DrawBorderThenFill(big_circle), + Animation(fluid_flow), + Animation(ff_circle), + ) + self.play(LaggedStart( + FadeIn, examples, + run_time=3, + )) + self.wait() + + +class MaxwellsEquations(Scene): + CONFIG = { + "faded_opacity": 0.3, + } + + def construct(self): + self.add_equations() + self.circle_gauss_law() + self.circle_magnetic_divergence() + self.circle_curl_equations() + + def add_equations(self): + title = Title("Maxwell's equations") + title.to_edge(UP, buff=MED_SMALL_BUFF) + + tex_to_color_map = { + "\\textbf{E}": BLUE, + "\\textbf{B}": YELLOW, + "\\rho": WHITE, + } + + equations = self.equations = VGroup(*[ + TexMobject( + tex, tex_to_color_map=tex_to_color_map + ) + for tex in [ + """ + \\text{div} \\, \\textbf{E} = + {\\rho \\over \\varepsilon_0} + """, + """\\text{div} \\, \\textbf{B} = 0""", + """ + \\text{curl} \\, \\textbf{E} = + -{\\partial \\textbf{B} \\over \\partial t} + """, + """ + \\text{curl} \\, \\textbf{B} = + \\mu_0 \\left( + \\textbf{J} + \\varepsilon_0 + {\\partial \\textbf{E} \\over \\partial t} + \\right) + """, + ] + ]) + equations.arrange_submobjects( + DOWN, aligned_edge=LEFT, + buff=MED_LARGE_BUFF + ) + + field_definitions = VGroup(*[ + TexMobject(text, tex_to_color_map=tex_to_color_map) + for text in [ + "\\text{Electric fild: } \\textbf{E}", + "\\text{Magnetic fild: } \\textbf{B}", + ] + ]) + field_definitions.arrange_submobjects( + RIGHT, buff=MED_LARGE_BUFF + ) + field_definitions.next_to(title, DOWN, MED_LARGE_BUFF) + equations.next_to(field_definitions, DOWN, MED_LARGE_BUFF) + field_definitions.shift(MED_SMALL_BUFF * UP) + + self.add(title) + self.add(field_definitions) + self.play(LaggedStart( + FadeIn, equations, + run_time=3, + lag_range=0.4 + )) + self.wait() + + def circle_gauss_law(self): + equation = self.equations[0] + rect = SurroundingRectangle(equation) + rect.set_color(RED) + rho = equation.get_part_by_tex("\\rho") + sub_rect = SurroundingRectangle(rho) + sub_rect.match_color(rect) + rho_label = TextMobject("Charge density") + rho_label.next_to(sub_rect, RIGHT) + rho_label.match_color(sub_rect) + gauss_law = TextMobject("Gauss's law") + gauss_law.next_to(rect, RIGHT) + + self.play( + ShowCreation(rect), + Write(gauss_law, run_time=1), + self.equations[1:].set_fill, {"opacity": self.faded_opacity} + ) + self.wait(2) + self.play( + ReplacementTransform(rect, sub_rect), + FadeOut(gauss_law), + FadeIn(rho_label), + rho.match_color, sub_rect, + ) + self.wait() + self.play( + self.equations.to_edge, LEFT, + MaintainPositionRelativeTo(rho_label, equation), + MaintainPositionRelativeTo(sub_rect, equation), + VFadeOut(rho_label), + VFadeOut(sub_rect), + ) + self.wait() + + def circle_magnetic_divergence(self): + equations = self.equations + rect = SurroundingRectangle(equations[1]) + + self.play( + equations[0].set_fill, {"opacity": self.faded_opacity}, + equations[1].set_fill, {"opacity": 1.0}, + ) + self.play(ShowCreation(rect)) + self.wait(3) + self.play(FadeOut(rect)) + + def circle_curl_equations(self): + equations = self.equations + rect = SurroundingRectangle(equations[2:]) + randy = Randolph(height=2) + randy.flip() + randy.next_to(rect, RIGHT, aligned_edge=DOWN) + randy.look_at(rect) + + self.play( + equations[1].set_fill, {"opacity": self.faded_opacity}, + equations[2:].set_fill, {"opacity": 1.0}, + ) + self.play(ShowCreation(rect)) + self.play( + randy.change, "confused", + VFadeIn(randy), + ) + self.play(Blink(randy)) + self.play(randy.look_at, 2 * RIGHT) + self.wait(3) + self.play( + FadeOut(rect), + randy.change, "pondering", + randy.look_at, rect, + ) + self.wait() + self.play(Blink(randy)) + self.wait() + + +class IllustrateGaussLaw(DefineDivergence, MovingCameraScene): + CONFIG = { + "flow_time": 10, + "stream_line_config": { + "start_points_generator_config": { + "delta_x": 1.0 / 16, + "delta_y": 1.0 / 16, + "x_min": -2, + "x_max": 2, + "y_min": -1.5, + "y_max": 1.5, + }, + "color_lines_by_magnitude": True, + "colors": [BLUE_E, BLUE_D, BLUE_C], + "stroke_width": 3, + }, + "stream_line_animation_config": { + "line_anim_class": ShowPassingFlashWithThinningStrokeWidth, + "line_anim_config": { + "n_segments": 5, + } + }, + "final_frame_width": 4, + } + + def construct(self): + particles = self.get_particles() + vector_field = self.get_vector_field() + + self.add_foreground_mobjects(vector_field) + self.add_foreground_mobjects(particles) + self.zoom_in() + self.show_flow() + + def get_particles(self): + particles = VGroup( + get_proton(radius=0.1), + get_electron(radius=0.1), + ) + particles.arrange_submobjects(RIGHT, buff=2.25) + particles.shift(0.25 * UP) + for particle, sign in zip(particles, [+1, -1]): + particle.charge = sign + + self.particles = particles + return particles + + def zoom_in(self): + self.play( + self.camera_frame.scale_to_fit_width, self.final_frame_width, + run_time=2 + ) + + +class IllustrateGaussMagnetic(IllustrateGaussLaw): + CONFIG = { + "final_frame_width": 7, + "stream_line_config": { + "start_points_generator_config": { + "delta_x": 1.0 / 16, + "delta_y": 1.0 / 16, + "x_min": -3.5, + "x_max": 3.5, + "y_min": -2, + "y_max": 2, + }, + "color_lines_by_magnitude": True, + "colors": [BLUE_E, BLUE_D, BLUE_C], + "stroke_width": 3, + }, + "flow_time": 10, + } + + def construct(self): + self.add_wires() + self.show_vector_field() + self.zoom_in() + self.show_flow() + + def add_wires(self): + top, bottom = [ + Circle( + radius=0.275, + stroke_color=WHITE, + fill_color=BLACK, + fill_opacity=1 + ) + for x in range(2) + ] + top.add(TexMobject("\\times").scale(0.5)) + bottom.add(Dot().scale(0.5)) + top.move_to(1 * UP) + bottom.move_to(1 * DOWN) + + self.add_foreground_mobjects(top, bottom) + + def show_vector_field(self): + vector_field = self.vector_field = VectorField( + self.func, **self.vector_field_config + ) + vector_field.submobjects.sort( + lambda a1, a2: -cmp(a1.get_length(), a2.get_length()) + ) + self.play(LaggedStart(GrowArrow, vector_field)) + self.add_foreground_mobjects( + vector_field, *self.foreground_mobjects + ) + + def func(self, point): + x, y = point[:2] + top_part = np.array([(y - 0.25), -x, 0]) + bottom_part = np.array([-(y + 0.25), x, 0]) + norm = np.linalg.norm + return 3 * op.add( + top_part / (norm(top_part)**2 + 1), + bottom_part / (norm(bottom_part)**2 + 1), + ) + + +class IllustrateEMCurlEquations(ExternallyAnimatedScene): + pass + + +class RelevantInNonSpatialCircumstances(TeacherStudentsScene): + def construct(self): + self.teacher_says( + """ + $\\textbf{div}$ and $\\textbf{curl}$ are \\\\ + even useful in some \\\\ + non-spatial problems + """, + target_mode="hooray" + ) + self.change_student_modes( + "sassy", "confused", "hesitant" + ) + self.wait(3) + + +class ShowTwoPopulations(Scene): + CONFIG = { + "total_num_animals": 50, + "start_num_foxes": 20, + "start_num_rabbits": 30, + "animal_height": 0.5, + "final_wait_time": 30, + } + + def construct(self): + self.introduce_animals() + self.evolve_system() + + def introduce_animals(self): + foxes = self.foxes = VGroup(*[ + self.get_fox() + for n in range(self.total_num_animals) + ]) + rabbits = self.rabbits = VGroup(*[ + self.get_rabbit() + for n in range(self.total_num_animals) + ]) + foxes[self.start_num_foxes:].set_fill(opacity=0) + rabbits[self.start_num_rabbits:].set_fill(opacity=0) + + fox, rabbit = examples = VGroup(foxes[0], rabbits[0]) + for mob in examples: + mob.save_state() + mob.scale_to_fit_height(3) + examples.arrange_submobjects(RIGHT, buff=2) + + preditor, prey = words = VGroup( + TextMobject("Preditor"), + TextMobject("Prey") + ) + for mob, word in zip(examples, words): + word.scale(1.5) + word.next_to(mob, UP) + self.play( + FadeInFromDown(mob), + Write(word, run_time=1), + ) + self.play( + LaggedStart( + ApplyMethod, examples, + lambda m: (m.restore,) + ), + LaggedStart(FadeOut, words), + *[ + LaggedStart( + FadeIn, + group[1:], + run_time=4, + lag_ratio=0.1, + ) + for group in [ + foxes[:self.start_num_foxes], + rabbits[:self.start_num_rabbits], + ] + ] + ) + + def evolve_system(self): + foxes = self.foxes + rabbits = self.rabbits + phase_point = VectorizedPoint( + self.start_num_foxes * RIGHT + + self.start_num_rabbits * UP + ) + self.add(VectorFieldFlow( + phase_point, + preditor_prey_vector_field, + )) + + def get_num_foxes(): + return phase_point.get_center()[0] + + def get_num_rabbits(): + return phase_point.get_center()[1] + + def get_updater(pop_size_getter): + def update(animals): + target_num = pop_size_getter() + for n, animal in enumerate(animals): + animal.set_fill( + opacity=np.clip(target_num - n, 0, 1) + ) + target_int = int(np.ceil(target_num)) + tail = animals.submobjects[target_int:] + random.shuffle(tail) + animals.submobjects[target_int:] = tail + + return update + + self.add(ContinualUpdateFromFunc( + foxes, get_updater(get_num_foxes) + )) + self.add(ContinualUpdateFromFunc( + rabbits, get_updater(get_num_rabbits) + )) + + # Add counts for foxes and rabbits + labels = self.get_pop_labels() + num_foxes = Integer(10) + num_foxes.next_to(labels[0], RIGHT) + num_rabbits = Integer(10) + num_rabbits.next_to(labels[1], RIGHT) + + self.add(ContinualChangingDecimal( + num_foxes, lambda a: get_num_foxes() + )) + self.add(ContinualChangingDecimal( + num_rabbits, lambda a: get_num_rabbits() + )) + + for count in num_foxes, num_rabbits: + self.add(ContinualUpdateFromFunc( + count, self.update_count_color, + )) + + self.play( + FadeIn(labels), + *[ + UpdateFromAlphaFunc(count, lambda m, a: m.set_fill(opacity=a)) + for count in num_foxes, num_rabbits + ] + ) + + self.wait(self.final_wait_time) + + # Helpers + + def get_animal(self, name, color): + result = SVGMobject( + file_name=name, + height=self.animal_height, + fill_color=color, + # stroke_color=BLACK, + # stroke_width=0.5 + ) + x_shift, y_shift = [ + (2 * random.random() - 1) * max_val + for max_val in [ + FRAME_WIDTH / 2 - 2, + FRAME_HEIGHT / 2 - 2 + ] + ] + result.shift(x_shift * RIGHT + y_shift * UP) + return result + + def get_fox(self): + return self.get_animal("fox", "#DF7F20") + + def get_rabbit(self): + return self.get_animal("rabbit", WHITE) + + def get_pop_labels(self): + labels = VGroup( + TextMobject("\\# Foxes: "), + TextMobject("\\# Rabbits: "), + ) + labels.arrange_submobjects(RIGHT, buff=2) + labels.to_edge(UP) + return labels + + def update_count_color(self, count): + count.set_fill(interpolate_color( + BLUE, RED, (count.number - 20) / 30.0 + )) + return count + + +class PhaseSpaceOfPopulationModel(ShowTwoPopulations): + def construct(self): + self.add_population_size_labels() + self.add_axes() + self.add_example_point() + self.add_vectors() + self.show_evolution_of_one_point() + self.show_phase_flow() + + def add_population_size_labels(self): + pass + + def add_axes(self): + pass + + def add_example_point(self): + pass + + def add_vectors(self): + pass + + def show_evolution_of_one_point(self): + pass + + def show_phase_flow(self): + pass +