diff --git a/active_projects/fourier.py b/active_projects/fourier.py index 386f9bf0..1c7fd54c 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -45,7 +45,7 @@ def get_fourier_graph( # T = time_range/n_samples time_range = float(t_max - t_min) time_step_size = time_range/n_samples - time_samples = time_func(np.linspace(t_min, t_max, n_samples)) + time_samples = np.vectorize(time_func)(np.linspace(t_min, t_max, n_samples)) fft_output = np.fft.fft(time_samples) frequencies = np.linspace(0.0, n_samples/(2.0*time_range), n_samples//2) # #Cycles per second of fouier_samples[1] diff --git a/active_projects/uncertainty.py b/active_projects/uncertainty.py index bb3be45b..2c385c8c 100644 --- a/active_projects/uncertainty.py +++ b/active_projects/uncertainty.py @@ -1686,7 +1686,8 @@ class IntroduceDopplerRadar(Scene): def construct(self): self.setup_axes() self.measure_distance_with_time() - self.measure_velocity_with_frequency() + self.show_frequency_shift() + self.show_frequency_shift_in_fourier() def setup_axes(self): self.dish = RadarDish() @@ -1694,8 +1695,8 @@ class IntroduceDopplerRadar(Scene): axes = Axes( x_min = 0, x_max = 10, - y_min = -2, - y_max = 2 + y_min = -1.5, + y_max = 1.5 ) axes.move_to(DOWN) time_label = TextMobject("Time") @@ -1718,20 +1719,15 @@ class IntroduceDopplerRadar(Scene): randy.move_to(dish.get_right(), LEFT) randy.shift(distance*RIGHT) - pulse_graph = self.get_single_pulse_graph(1, color = BLUE) - echo_graph = self.get_single_pulse_graph(1+time_diff, color = YELLOW) - sum_graph = axes.get_graph( - lambda x : sum([ - pulse_graph.underlying_function(x), - echo_graph.underlying_function(x), - ]), - color = WHITE - ) - sum_graph.background_image_file = "blue_yellow_gradient" + pulse_graph, echo_graph, sum_graph = \ + self.get_pulse_and_echo_graphs( + self.get_single_pulse_graph, + (1,), (1+time_diff,) + ) words = ["Original signal", "Echo"] for graph, word in zip([pulse_graph, echo_graph], words): - arrow = Vector(DOWN+LEFT) - arrow.next_to(graph.peak_point, UP+RIGHT, SMALL_BUFF) + arrow = Vector(DOWN) + arrow.next_to(graph.peak_point, UP, SMALL_BUFF) arrow.match_color(graph) graph.arrow = arrow label = TextMobject(word) @@ -1739,22 +1735,35 @@ class IntroduceDopplerRadar(Scene): label.match_color(graph) graph.label = label + double_arrow = DoubleArrow( + pulse_graph.peak_point, + echo_graph.peak_point, + color = WHITE + ) + distance_text = TextMobject("$2 \\times$ distance/(signal speed)") + distance_text.scale_to_fit_width(0.9*double_arrow.get_width()) + distance_text.next_to(double_arrow, UP, SMALL_BUFF) + #v_line anim? - pulse = RadarPulseSingleton(dish, randy, speed = speed) + pulse = RadarPulseSingleton( + dish, randy, + speed = 0.97*speed, #Just needs slightly better alignment + ) graph_draw = NormalAnimationAsContinualAnimation( ShowCreation( sum_graph, rate_func = None, - run_time = axes.x_max + run_time = 0.97*axes.x_max ) ) randy_look_at = ContinualUpdateFromFunc( randy, lambda pi : pi.look_at(pulse.mobject) ) + axes_anim = ContinualAnimation(axes) - self.add(randy_look_at, ContinualAnimation(axes), graph_draw) - self.wait() + self.add(randy_look_at, axes_anim, graph_draw) + self.wait(0.5) self.add(pulse) self.play( Write(pulse_graph.label), @@ -1768,35 +1777,263 @@ class IntroduceDopplerRadar(Scene): GrowArrow(echo_graph.arrow), run_time = 1 ) - self.wait(3) - graph_draw.update(10) - self.add(sum_graph) + self.wait() + self.play( + GrowFromCenter(double_arrow), + FadeIn(distance_text) + ) self.wait() + self.remove(graph_draw, pulse, randy_look_at, axes_anim) + self.add(axes) + self.play(LaggedStart(FadeOut, VGroup( + sum_graph, randy, + pulse_graph.arrow, pulse_graph.label, + echo_graph.arrow, echo_graph.label, + double_arrow, distance_text + ))) + + def show_frequency_shift(self): + axes = self.axes + dish = self.dish + plane = Plane() + plane.flip() + plane.move_to(dish) + plane.to_edge(RIGHT) + + time_diff = 6 + + pulse_graph, echo_graph, sum_graph = graphs = \ + self.get_pulse_and_echo_graphs( + self.get_frequency_pulse_graph, + (1,25), (1+time_diff,50) + ) + for graph in graphs: + graph.set_stroke(width = 3) + signal_graph = self.get_frequency_pulse_graph(1) + + pulse_brace = Brace(Line(ORIGIN, RIGHT), UP) + pulse_brace.move_to(axes.coords_to_point(1, 1.2)) + echo_brace = pulse_brace.copy() + echo_brace.stretch(0.6, 0) + echo_brace.move_to(axes.coords_to_point(7, 1.2)) + pulse_text = pulse_brace.get_text("Original signal") + pulse_text.add_background_rectangle() + echo_text = echo_brace.get_text("Echo") + echo_subtext = TextMobject("(Higher frequency)") + echo_subtext.next_to(echo_text, RIGHT) + echo_subtext.match_color(echo_graph) + + graph_draw = NormalAnimationAsContinualAnimation( + ShowCreation(sum_graph, run_time = 8, rate_func = None) + ) + pulse = RadarPulse(dish, plane, n_pulse_singletons = 12) + plane_flight = AmbientMovement( + plane, direction = LEFT, rate = 1.5 + ) + + self.add(graph_draw, pulse, plane_flight) + self.play(UpdateFromAlphaFunc( + plane, lambda m, a : m.set_fill(opacity = a) + )) + self.play( + GrowFromCenter(pulse_brace), + FadeIn(pulse_text), + ) + self.wait(3) + self.play( + GrowFromCenter(echo_brace), + GrowFromCenter(echo_text), + ) + self.play(UpdateFromAlphaFunc( + plane, lambda m, a : m.set_fill(opacity = 1-a) + )) + #Only for when -s is run + graph_draw.update(10) + self.wait(0.1) + self.play(Write(echo_subtext, run_time = 1)) + self.wait() + self.remove(graph_draw, pulse, plane_flight) + + pulse_graph.set_stroke(width = 0) + echo_graph.set_stroke(width = 0) + self.time_graph_group = VGroup( + axes, pulse_brace, pulse_text, + echo_brace, echo_text, echo_subtext, + pulse_graph, echo_graph, sum_graph, + ) + self.set_variables_as_attrs(*self.time_graph_group) + + def show_frequency_shift_in_fourier(self): + sum_graph = self.sum_graph + pulse_graph = self.pulse_graph + pulse_label = VGroup(self.pulse_brace, self.pulse_text) + echo_graph = self.echo_graph + echo_label = VGroup( + self.echo_brace, self.echo_text, self.echo_subtext + ) + + #Setup all fourier graph stuff + f_max = 0.02 + frequency_axes = Axes( + x_min = 0, x_max = 20, + x_axis_config = {"unit_size" : 0.5}, + y_min = -f_max, y_max = f_max, + y_axis_config = { + "unit_size" : 50, + "tick_frequency" : 0.01, + }, + ) + frequency_axes.move_to(self.axes, LEFT) + frequency_axes.to_edge(DOWN) + frequency_label = TextMobject("Frequency") + frequency_label.next_to( + frequency_axes.x_axis.get_right(), UP, + ) + frequency_label.to_edge(RIGHT) + frequency_axes.add(frequency_label) + + for graph in pulse_graph, echo_graph, sum_graph: + graph.fourier_transform = get_fourier_graph( + frequency_axes, graph.underlying_function, + frequency_axes.x_min, 25, + complex_to_real_func = abs, + ) + + #Braces labeling F.T. + original_fourier_brace = Brace( + Line( + frequency_axes.coords_to_point(7, 0.9*f_max), + frequency_axes.coords_to_point(9, 0.9*f_max), + ), + UP, + ).highlight(BLUE) + echo_fourier_brace = Brace( + Line( + frequency_axes.coords_to_point(14, 0.4*f_max), + frequency_axes.coords_to_point(18, 0.4*f_max), + ), + UP, + ).highlight(YELLOW) + # braces = [original_fourier_brace, echo_fourier_brace] + # words = ["original signal", "echo"] + # for brace, word in zip(braces, words): + # brace.add(brace.get_text("F.T. of \\\\ %s"%word)) + fourier_label = TexMobject("||\\text{Fourier transform}||") + # fourier_label.next_to(sum_graph.fourier_transform, UP, MED_LARGE_BUFF) + fourier_label.next_to(frequency_axes.y_axis, UP, buff = SMALL_BUFF) + fourier_label.shift_onto_screen() + fourier_label.highlight(RED) - def measure_velocity_with_frequency(self): - pass + #v_lines + v_line = DashedLine( + frequency_axes.coords_to_point(8, 0), + frequency_axes.coords_to_point(8, 1.2*f_max), + color = YELLOW, + dashed_segment_length = 0.025, + ) + v_line_pair = VGroup(*[ + v_line.copy().shift(u*0.6*RIGHT) + for u in -1, 1 + ]) + v_line = VGroup(v_line) + + double_arrow = DoubleArrow( + frequency_axes.coords_to_point(8, 0.007), + frequency_axes.coords_to_point(16, 0.007), + buff = 0, + color = WHITE + ) + + self.play( + self.time_graph_group.to_edge, UP, + ApplyMethod( + self.dish.shift, 2*UP, + remover = True + ), + FadeIn(frequency_axes) + ) + self.wait() + self.play( + FadeOut(sum_graph), + FadeOut(echo_label), + pulse_graph.set_stroke, {"width" : 3}, + ) + self.play( + ReplacementTransform( + pulse_label[0].copy(), + original_fourier_brace + ), + ShowCreation(pulse_graph.fourier_transform) + ) + self.play(Write(fourier_label)) + self.wait() + self.play(ShowCreation(v_line)) + self.wait() + self.play(ReplacementTransform(v_line, v_line_pair)) + self.wait() + self.play(FadeOut(v_line_pair)) + self.wait() + + self.play( + FadeOut(pulse_graph), + FadeIn(sum_graph), + ReplacementTransform( + pulse_graph.fourier_transform, + sum_graph.fourier_transform + ) + ) + self.play(FadeIn(echo_label)) + self.play(ReplacementTransform( + echo_label[0].copy(), + echo_fourier_brace, + )) + self.wait(2) + self.play(GrowFromCenter(double_arrow)) + self.wait() ### - def get_single_pulse_graph(self, x, **kwargs): - graph = self.axes.get_graph( - self.get_single_pulse_function(x), - **kwargs - ) + def get_graph(self, func, **kwargs): + graph = self.axes.get_graph(func, **kwargs) graph.peak_point = self.get_peak_point(graph) return graph + def get_single_pulse_graph(self, x, **kwargs): + return self.get_graph(self.get_single_pulse_function(x), **kwargs) + def get_single_pulse_function(self, x): return lambda t : -2*np.sin(10*(t-x))*np.exp(-100*(t-x)**2) + def get_frequency_pulse_graph(self, x, freq = 50, **kwargs): + return self.get_graph( + self.get_frequency_pulse_function(x, freq), + num_graph_points = 700, + **kwargs + ) + + def get_frequency_pulse_function(self, x, freq): + return lambda t : 2*np.cos(2*freq*(t-x))*min(np.exp(-(freq**2/100)*(t-x)**2), 0.5) + def get_peak_point(self, graph): anchors = graph.get_anchors() return anchors[np.argmax([p[1] for p in anchors])] - + def get_pulse_and_echo_graphs(self, func, args1, args2): + pulse_graph = func(*args1, color = BLUE) + echo_graph = func(*args2, color = YELLOW) + sum_graph = self.axes.get_graph( + lambda x : sum([ + pulse_graph.underlying_function(x), + echo_graph.underlying_function(x), + ]), + num_graph_points = echo_graph.get_num_anchor_points(), + color = WHITE + ) + sum_graph.background_image_file = "blue_yellow_gradient" + return pulse_graph, echo_graph, sum_graph diff --git a/animation/animation.py b/animation/animation.py index 45156f59..1e4b5594 100644 --- a/animation/animation.py +++ b/animation/animation.py @@ -129,19 +129,6 @@ class Animation(object): return self -def sync_animation_run_times_and_rate_funcs(*animations, **kwargs): - for animation in animations: - animation.update_config(**kwargs) - max_run_time = max([a.run_time for a in animations]) - for animation in animations: - if animation.run_time != max_run_time: - new_rate_func = squish_rate_func( - animation.get_rate_func(), - 0, float(animation.run_time)/max_run_time - ) - animation.set_rate_func(new_rate_func) - animation.set_run_time(max_run_time) - diff --git a/animation/simple_animations.py b/animation/simple_animations.py index dac11771..801181b9 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -7,7 +7,6 @@ from mobject import Mobject, Group from mobject.vectorized_mobject import VMobject from mobject.tex_mobject import TextMobject from animation import Animation -from animation import sync_animation_run_times_and_rate_funcs from transform import Transform class Rotating(Animation): @@ -492,15 +491,17 @@ class AnimationGroup(Animation): self.empty = True self.run_time = 0 else: - # Should really make copies of animations, instead of messing with originals... - sync_animation_run_times_and_rate_funcs(*sub_anims, **kwargs) + for anim in sub_anims: + # If AnimationGroup is called with any configuration, + # it is propagated to the sub_animations + anim.update_config(**kwargs) self.run_time = max([a.run_time for a in sub_anims]) everything = Mobject(*[a.mobject for a in sub_anims]) Animation.__init__(self, everything, **kwargs) - def update_mobject(self, alpha): + def update(self, alpha): for anim in self.sub_anims: - anim.update(alpha) + anim.update(alpha * self.run_time / anim.run_time) def clean_up(self, *args, **kwargs): for anim in self.sub_anims: diff --git a/scene/scene.py b/scene/scene.py index 769ce11a..3ee98dcd 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -17,7 +17,6 @@ from camera import Camera from tk_scene import TkSceneRoot from mobject import Mobject, VMobject from animation import Animation -from animation.animation import sync_animation_run_times_and_rate_funcs from animation.transform import MoveToTarget from animation.continual_animation import ContinualAnimation from container import * @@ -65,7 +64,10 @@ class Scene(Container): self.setup() if self.write_to_movie: self.open_movie_pipe() - self.construct(*self.construct_args) + try: + self.construct(*self.construct_args) + except EndSceneEarlyException: + pass if self.write_to_movie: self.close_movie_pipe() print("Played a total of %d animations"%self.num_plays) @@ -138,7 +140,10 @@ class Scene(Container): mobjects = None, background = None, include_submobjects = True, + dont_update_when_skipping = True, **kwargs): + if self.skip_animations and dont_update_when_skipping: + return if mobjects is None: mobjects = list_update( self.mobjects, @@ -331,12 +336,16 @@ class Scene(Container): return moving_mobjects def get_time_progression(self, run_time): - times = np.arange(0, run_time, self.frame_duration) + if self.skip_animations: + times = [run_time] + else: + step = self.frame_duration + times = np.arange(0, run_time + step, step) time_progression = ProgressDisplay(times) return time_progression def get_animation_time_progression(self, animations): - run_time = animations[0].run_time + run_time = np.max([animation.run_time for animation in animations]) time_progression = self.get_time_progression(run_time) time_progression.set_description("".join([ "Animation %d: "%self.num_plays, @@ -406,24 +415,29 @@ class Scene(Container): compile_method(state) return animations + def handle_animation_skipping(self): + if self.start_at_animation_number: + if self.num_plays == self.start_at_animation_number: + self.skip_animations = self.original_skipping_status + if self.end_at_animation_number: + if self.num_plays >= self.end_at_animation_number: + self.skip_animations = True + raise EndSceneEarlyException() + def play(self, *args, **kwargs): if len(args) == 0: warnings.warn("Called Scene.play with no animations") return - if self.start_at_animation_number: - if self.num_plays == self.start_at_animation_number: - self.skip_animations = False - if self.end_at_animation_number: - if self.num_plays >= self.end_at_animation_number: - self.skip_animations = True - return self #Don't even both with the rest... - if self.skip_animations: - kwargs["run_time"] = 0 - + self.handle_animation_skipping() animations = self.compile_play_args_to_animation_list(*args) - - sync_animation_run_times_and_rate_funcs(*animations, **kwargs) + for animation in animations: + # This is where kwargs to play like run_time and rate_func + # get applied to all animations + animation.update_config(**kwargs) moving_mobjects = self.get_moving_mobjects(*animations) + + # Paint all non-moving objects onto the screen, so they don't + # have to be rendered every frame self.update_frame(excluded_mobjects = moving_mobjects) static_image = self.get_frame() for t in self.get_animation_time_progression(animations): @@ -435,7 +449,12 @@ class Scene(Container): self.add(*moving_mobjects) self.mobjects_from_last_animation = moving_mobjects self.clean_up_animations(*animations) - self.continual_update(0) + if self.skip_animations: + # Todo, not great that this uses a variable from + # a previous loop... + self.continual_update(t) + else: + self.continual_update(0) self.num_plays += 1 return self @@ -451,17 +470,17 @@ class Scene(Container): return [] def wait(self, duration = DEFAULT_WAIT_TIME): - if self.skip_animations: - return self - if self.should_continually_update(): for t in self.get_time_progression(duration): self.continual_update() self.update_frame() self.add_frames(self.get_frame()) - else: + elif not self.skip_animations: self.update_frame() self.add_frames(*[self.get_frame()]*int(duration / self.frame_duration)) + else: + #If self.skip_animations is set, do nothing + pass return self @@ -500,7 +519,7 @@ class Scene(Container): #Display methods def show_frame(self): - self.update_frame() + self.update_frame(dont_update_when_skipping = False) self.get_image().show() def preview(self): @@ -520,7 +539,7 @@ class Scene(Container): if not os.path.exists(directory_path): os.makedirs(directory_path) if not dont_update: - self.update_frame() + self.update_frame(dont_update_when_skipping = False) image = self.get_image() image = image.convert(mode) image.save(path) @@ -573,3 +592,17 @@ class Scene(Container): shutil.move(*self.args_to_rename_file) else: os.rename(*self.args_to_rename_file) + +class EndSceneEarlyException(Exception): + pass + + + + + + + + + + +