diff --git a/active_projects/holomorphic.py b/active_projects/conformal.py similarity index 63% rename from active_projects/holomorphic.py rename to active_projects/conformal.py index 5227985d..a769898d 100644 --- a/active_projects/holomorphic.py +++ b/active_projects/conformal.py @@ -1,174 +1,6 @@ from big_ol_pile_of_manim_imports import * -# Helper functions -def get_flow_start_points(x_min=-8, x_max=8, - y_min=-5, y_max=5, - delta_x=0.5, delta_y=0.5, - n_repeats=1, - noise_factor=0.01 - ): - return np.array([ - x * RIGHT + y * UP + noise_factor * np.random.random(3) - for n in xrange(n_repeats) - for x in np.arange(x_min, x_max + delta_x, delta_x) - for y in np.arange(y_min, y_max + delta_y, delta_y) - ]) - - -def joukowsky_map(z): - return z + fdiv(1, z) - - -def inverse_joukowsky_map(w): - u = 1 if w.real <= 0 else -1 - return (w + u * np.sqrt(w**2 - 4)) / 2 - - -def derivative(func, dt=1e-7): - return lambda z: (func(z + dt) - func(z)) / dt - - -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)) - - -# Continual animations - -class VectorFieldFlow(ContinualAnimation): - CONFIG = { - "mode": None, - } - - def __init__(self, mobject, func, **kwargs): - """ - Func should take in a vector in R3, and output a vector in R3 - """ - self.func = func - ContinualAnimation.__init__(self, mobject, **kwargs) - - def update_mobject(self, dt): - self.apply_nudge(dt) - - def apply_nudge(self): - 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) - - -class VectorFieldPointFlow(VectorFieldFlow): - def apply_nudge(self, dt): - self.mobject.apply_function( - lambda p: p + self.func(p) * dt - ) - - -class StreamLines(VGroup): - CONFIG = { - "start_points_generator": get_flow_start_points, - "dt": 0.05, - "virtual_time": 20, - "n_anchors_per_line": 30, - "stroke_width": 1, - "stroke_color": WHITE, - } - - def __init__(self, func, **kwargs): - VGroup.__init__(self, **kwargs) - self.func = func - dt = self.dt - - start_points = self.start_points_generator() - for point in start_points: - points = [point] - for t in np.arange(0, self.virtual_time, dt): - last_point = points[-1] - points.append(last_point + dt * func(last_point)) - line = VMobject() - line.set_points_smoothly( - points[::(len(points) / self.n_anchors_per_line)] - ) - self.add(line) - - self.set_stroke(self.stroke_color, self.stroke_width) - - -class StreamLineAnimation(ContinualAnimation): - CONFIG = { - "lag_range": 4, - "line_anim_class": ShowPassingFlash, - "line_anim_config": { - "run_time": 4, - "rate_func": None, - "time_width": 0.4, - }, - } - - def __init__(self, stream_lines, **kwargs): - digest_config(self, kwargs) - self.stream_lines = stream_lines - for line in stream_lines: - line.anim = self.line_anim_class(line, **self.line_anim_config) - line.time = -self.lag_range * random.random() - ContinualAnimation.__init__(self, stream_lines, **kwargs) - - def update_mobject(self, dt): - stream_lines = self.stream_lines - for line in stream_lines: - line.time += dt - adjusted_time = max(line.time, 0) % line.anim.run_time - line.anim.update(adjusted_time / line.anim.run_time) - -# Scenes - - -class TestVectorField(Scene): - CONFIG = { - "func": cylinder_flow_vector_field, - } - - def construct(self): - plane = ComplexPlane() - self.add(plane) - - circle = Circle(stroke_color=YELLOW) - circle.set_fill(BLACK, 1) - self.add_foreground_mobject(circle) - - lines = StreamLines( - self.func, - start_points_generator=lambda: get_flow_start_points( - x_min=-8, x_max=-7, y_min=-2, y_max=2, - delta_x=0.5, delta_y=0.2, - n_repeats=4, - noise_factor=0.2, - ), - ) - self.add(lines) - return - stream_line_animation = StreamLineAnimation(lines) - self.add(stream_line_animation) - self.wait(8) - self.play(VFadeOut(lines)) - self.remove(stream_line_animation) - self.wait() - - # dots = VGroup(*[ - # Dot().move_to(start_point) - # for start_point in get_flow_start_points() - # ]) - # dots.set_color_by_gradient(YELLOW, RED) - # self.add(dots) - - # self.add(VectorFieldSubmobjectFlow(dots, self.func)) - # self.wait(5) - - class ComplexAnalysisOverlay(Scene): def construct(self): words = TextMobject("Complex analysis") diff --git a/active_projects/div_curl.py b/active_projects/div_curl.py new file mode 100644 index 00000000..67eda49f --- /dev/null +++ b/active_projects/div_curl.py @@ -0,0 +1,353 @@ +from big_ol_pile_of_manim_imports import * + + +DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, WHITE, ORANGE, RED] + + +# Helper functions +def get_flow_start_points(x_min=-8, x_max=8, + y_min=-5, y_max=5, + delta_x=0.5, delta_y=0.5, + n_repeats=1, + noise_factor=0.01 + ): + return np.array([ + x * RIGHT + y * UP + noise_factor * np.random.random(3) + for n in xrange(n_repeats) + for x in np.arange(x_min, x_max + delta_x, delta_x) + for y in np.arange(y_min, y_max + delta_y, delta_y) + ]) + + +def joukowsky_map(z): + return z + fdiv(1, z) + + +def inverse_joukowsky_map(w): + u = 1 if w.real <= 0 else -1 + return (w + u * np.sqrt(w**2 - 4)) / 2 + + +def derivative(func, dt=1e-7): + return lambda z: (func(z + dt) - func(z)) / dt + + +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)) + return complex_to_R3(derivative(joukowsky_map)(z).conjugate()) + + +def cylinder_flow_magnitude_field(point): + return np.linalg.norm(cylinder_flow_vector_field(point)) + + +def get_colored_background_image(scalar_field_func, + number_to_rgb_func, + pixel_height=DEFAULT_PIXEL_HEIGHT, + pixel_width=DEFAULT_PIXEL_WIDTH, + ): + ph = pixel_height + pw = pixel_width + fw = FRAME_WIDTH + fh = FRAME_HEIGHT + points_array = np.zeros((ph, pw, 3)) + x_array = np.linspace(-fw / 2, fw / 2, ph).repeat(pw).reshape((ph, pw)) + y_array = np.linspace(fh / 2, -fh / 2, pw).repeat(ph).reshape((pw, ph)).T + points_array[:, :, 0] = x_array + points_array[:, :, 1] = y_array + scalars = np.apply_along_axis(scalar_field_func, 2, points_array) + rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3)) + return Image.fromarray((rgb_array * 255).astype('uint8')) + + +def get_rgb_gradient_function(min_value=0, max_value=1, colors=[BLUE, RED]): + rgbs = np.array(map(color_to_rgb, colors)) + + def func(values): + alphas = inverse_interpolate(min_value, max_value, values) + alphas = np.clip(alphas, 0, 1) + alphas = 1 - alphas # Why? + scaled_alphas = alphas * (len(rgbs) - 1) + indices = scaled_alphas.astype(int) + next_indices = np.clip(indices + 1, 0, len(rgbs) - 1) + inter_alphas = scaled_alphas % 1 + inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3)) + result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas) + return result + + return func + + +def get_color_field_image_file(scalar_func, + min_value=0, max_value=2, + colors=DEFAULT_SCALAR_FIELD_COLORS + ): + # try_hash + np.random.seed(0) + sample_inputs = 5 * np.random.random(size=(10, 3)) - 10 + sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs) + func_hash = hash( + str(min_value) + str(max_value) + str(colors) + str(sample_outputs) + ) + file_name = "%d.png" % func_hash + full_path = os.path.join(RASTER_IMAGE_DIR, file_name) + if not os.path.exists(full_path): + print "Rendering color field image " + str(func_hash) + rgb_gradient_func = get_rgb_gradient_function( + min_value=min_value, + max_value=max_value, + colors=colors + ) + image = get_colored_background_image(scalar_func, rgb_gradient_func) + image.save(full_path) + return full_path + + +# Continual animations + + +class VectorFieldFlow(ContinualAnimation): + CONFIG = { + "mode": None, + } + + def __init__(self, mobject, func, **kwargs): + """ + Func should take in a vector in R3, and output a vector in R3 + """ + self.func = func + ContinualAnimation.__init__(self, mobject, **kwargs) + + def update_mobject(self, dt): + self.apply_nudge(dt) + + def apply_nudge(self): + 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) + + +class VectorFieldPointFlow(VectorFieldFlow): + def apply_nudge(self, dt): + self.mobject.apply_function( + lambda p: p + self.func(p) * dt + ) + + +class StreamLines(VGroup): + CONFIG = { + "start_points_generator": get_flow_start_points, + "dt": 0.05, + "virtual_time": 15, + "n_anchors_per_line": 40, + "stroke_width": 1, + "stroke_color": WHITE, + "color_lines_by_magnitude": True, + "min_magnitude": 0.5, + "max_magnitude": 1.5, + "colors": DEFAULT_SCALAR_FIELD_COLORS, + } + + def __init__(self, func, **kwargs): + VGroup.__init__(self, **kwargs) + self.func = func + dt = self.dt + + start_points = self.start_points_generator() + for point in start_points: + points = [point] + for t in np.arange(0, self.virtual_time, dt): + last_point = points[-1] + points.append(last_point + dt * func(last_point)) + line = VMobject() + line.set_points_smoothly( + points[::(len(points) / self.n_anchors_per_line)] + ) + self.add(line) + + self.set_stroke(self.stroke_color, self.stroke_width) + + if self.color_lines_by_magnitude: + image_file = get_color_field_image_file( + lambda p: np.linalg.norm(func(p)), + min_value=self.min_magnitude, + max_value=self.max_magnitude, + colors=self.colors, + ) + self.color_using_background_image(image_file) + + +class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup): + CONFIG = { + "n_segments": 10, + "time_width": 0.1, + "remover": True + } + + def __init__(self, vmobject, **kwargs): + digest_config(self, kwargs) + max_stroke_width = vmobject.get_stroke_width() + max_time_width = kwargs.pop("time_width", self.time_width) + AnimationGroup.__init__(self, *[ + ShowPassingFlash( + vmobject.deepcopy().set_stroke(width=stroke_width), + time_width=time_width, + **kwargs + ) + for stroke_width, time_width in zip( + np.linspace(0, max_stroke_width, self.n_segments), + np.linspace(max_time_width, 0, self.n_segments) + ) + ]) + + +class StreamLineAnimation(ContinualAnimation): + CONFIG = { + "lag_range": 4, + "line_anim_class": ShowPassingFlashWithThinningStrokeWidth, + "line_anim_config": { + "run_time": 4, + "rate_func": None, + "time_width": 0.3, + }, + } + + def __init__(self, stream_lines, **kwargs): + digest_config(self, kwargs) + self.stream_lines = stream_lines + group = VGroup() + for line in stream_lines: + line.anim = self.line_anim_class(line, **self.line_anim_config) + line.time = -self.lag_range * random.random() + group.add(line.anim.mobject) + ContinualAnimation.__init__(self, group, **kwargs) + + def update_mobject(self, dt): + stream_lines = self.stream_lines + for line in stream_lines: + line.time += dt + adjusted_time = max(line.time, 0) % line.anim.run_time + line.anim.update(adjusted_time / line.anim.run_time) + +# Scenes + + +class TestVectorField(Scene): + CONFIG = { + "func": cylinder_flow_vector_field, + "flow_time": 15, + } + + def construct(self): + plane = ComplexPlane() + self.add(plane) + + circle = Circle(stroke_color=YELLOW) + circle.set_fill(BLACK, 1) + self.add_foreground_mobject(circle) + + lines = StreamLines( + self.func, + start_points_generator=lambda: get_flow_start_points( + x_min=-8, x_max=-7, y_min=-4, y_max=4, + delta_x=0.5, delta_y=0.1, + n_repeats=1, + noise_factor=0.1, + ), + stroke_width=2, + ) + # self.add(lines) + # self.play(ShowPassingFlashWithThinningStrokeWidth(lines[5], run_time=4)) + # return + stream_line_animation = StreamLineAnimation(lines) + self.add(stream_line_animation) + self.wait(self.flow_time) + # self.play(VFadeOut(lines)) + # self.remove(stream_line_animation) + # self.wait() + + # dots = VGroup(*[ + # Dot().move_to(start_point) + # for start_point in get_flow_start_points() + # ]) + # dots.set_color_by_gradient(YELLOW, RED) + # self.add(dots) + + # self.add(VectorFieldSubmobjectFlow(dots, self.func)) + # self.wait(5) + + +class Introduction(Scene): + CONFIG = { + + } + + def construct(self): + self.add_plane() + self.show_numbers() + self.show_contour_lines() + self.show_flow() + self.apply_joukowsky_map() + + def add_plane(self): + self.plane = ComplexPlane() + self.plane.add_coordinates() + self.plane.coordinate_labels.submobjects.pop(-1) + self.add(self.plane) + + def show_numbers(self): + title = TextMobject("Complex Plane") + title.scale(1.5) + title.to_edge(UP, buff=MED_SMALL_BUFF) + title.add_background_rectangle() + self.add(title) + + unit_circle = Circle(radius=self.plane.unit_size) + unit_circle.set_color(YELLOW) + dot = Dot() + dot_update = UpdateFromFunc( + dot, lambda d: d.move_to(unit_circle.point_from_proportion(1)) + ) + exp_tex = TexMobject("e^{", "0.00", "i}") + zero = exp_tex.get_part_by_tex("0.00") + zero.fade(1) + exp_tex_update = UpdateFromFunc( + exp_tex, lambda et: et.next_to(dot, UR, SMALL_BUFF) + ) + exp_decimal = DecimalNumber( + 0, num_decimal_places=2, + include_background_rectangle=True, + color=YELLOW + ) + exp_decimal.replace(zero) + exp_decimal_update = ChangeDecimalToValue( + exp_decimal, TAU, + position_update_func=lambda mob: mob.move_to(zero), + run_time=4, + ) + + self.play( + ShowCreation(unit_circle, run_time=4), + VFadeIn(exp_tex), + UpdateFromAlphaFunc( + exp_decimal, + lambda ed, a: ed.set_fill(opacity=a) + ), + dot_update, + exp_tex_update, + exp_decimal_update, + ) + + def show_contour_lines(self): + pass + + def show_flow(self): + pass + + def apply_joukowsky_map(self): + pass