From 2d34b2e28be0cf16a39d66b6d0e5919506ab11cd Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 18 Jan 2018 11:51:09 -0800 Subject: [PATCH 1/5] Incremental progress on WindingNumber --- active_projects/WindingNumber.py | 64 +++++++------------------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 98abe10e..95c90d1d 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -514,7 +514,6 @@ class FuncRotater(Animation): angle_revs * 2 * np.pi, ) self.mobject.set_color(color_func(angle_revs)) - # Will want to have arrow colors change to match direction as well class TestRotater(Scene): def construct(self): @@ -618,7 +617,7 @@ class RectangleData(): def complex_to_pair(c): return (c.real, c.imag) -class iterative_2d_test(Scene): +class Iterative2dTest(Scene): CONFIG = { "func" : lambda (x, y) : complex_to_pair(complex(x, y)**2 - complex(1, 2)**2), "initial_lower_x" : -5.1, @@ -657,50 +656,28 @@ class iterative_2d_test(Scene): rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), stroke_width = 5, - color = "#FF0000") + color = RED) self.play( ShowCreation(line), - #ChangingDecimal(num_display, rebased_winder) + ChangingDecimal(num_display, rebased_winder) ) - line.set_color("#00FF00") + line.set_color(GREEN) # Temporary hack to see (some) redraws; TODO: figure out a better approach return rebased_winder(1) for i in range(self.num_iterations): (explore_rect, alt_rect) = rect.splits_on_dim(dim_to_split) - top_wind = draw_line_return_wind( - explore_rect.get_top_left(), - explore_rect.get_top_right(), - 0 - ) + wind_so_far = 0 + sides = [ + explore_rect.get_top(), + explore_rect.get_right(), + explore_rect.get_bottom(), + explore_rect.get_left() + ] + for (start, end) in sides: + wind_so_far = draw_line_return_wind(start, end, wind_so_far) - print(len(self.mobjects)) - - right_wind = draw_line_return_wind( - explore_rect.get_top_right(), - explore_rect.get_bottom_right(), - top_wind - ) - - print(len(self.mobjects)) - - bottom_wind = draw_line_return_wind( - explore_rect.get_bottom_right(), - explore_rect.get_bottom_left(), - right_wind - ) - - print(len(self.mobjects)) - - left_wind = draw_line_return_wind( - explore_rect.get_bottom_left(), - explore_rect.get_top_left(), - bottom_wind - ) - - print(len(self.mobjects)) - - total_wind = round(left_wind) + total_wind = round(wind_so_far) if total_wind == 0: rect = alt_rect @@ -711,16 +688,3 @@ class iterative_2d_test(Scene): self.wait() - -class EquationSolver2d(ZoomedScene): - #TODO - CONFIG = { - "func" : lambda p : p, - "target_input" : (0, 0), - "target_output" : (0, 0), - "initial_top_left_point" : (0, 0), - "initial_guess_dimensions" : (0, 0), - "num_iterations" : 10, - "iteration_at_which_to_start_zoom" : None - } - From d74127bb7fb221f08788586514d2f7b71c118820 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 13:02:41 -0800 Subject: [PATCH 2/5] Bug fixes to implementation of Succession animations --- animation/simple_animations.py | 36 +++++++++++++++++----------------- helpers.py | 6 ++++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index bac8ffcc..68e4176e 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -329,7 +329,7 @@ class Succession(Animation): """ Each arg will either be an animation, or an animation class followed by its arguments (and potentially a dict for - configuraiton). + configuration). For example, Succession( @@ -387,27 +387,27 @@ class Succession(Animation): #might very well mess with it. self.original_run_time = run_time + # critical_alphas[i] is the start alpha of self.animations[i] + # critical_alphas[i + 1] is the end alpha of self.animations[i] + critical_times = np.concatenate(([0], np.cumsum(self.run_times))) + self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times) + mobject = Group(*[anim.mobject for anim in self.animations]) Animation.__init__(self, mobject, run_time = run_time, **kwargs) def update_mobject(self, alpha): - if alpha >= 1.0: - self.animations[-1].update(1) - return - run_times = self.run_times - index = 0 - time = alpha*self.original_run_time - while sum(run_times[:index+1]) < time: - index += 1 - if index > self.last_index: - self.animations[self.last_index].update(1) - self.animations[self.last_index].clean_up() - self.last_index = index - curr_anim = self.animations[index] - sub_alpha = np.clip( - (time - sum(run_times[:index]))/run_times[index], 0, 1 - ) - curr_anim.update(sub_alpha) + for i in range(len(self.animations)): + sub_alpha = anti_interpolate( + self.critical_alphas[i], + self.critical_alphas[i + 1], + alpha + ) + sub_alpha = clamp(0, 1, sub_alpha) # Could possibly adopt a non-clamping convention here + self.animations[i].update(sub_alpha) + + def clean_up(self, *args, **kwargs): + for anim in self.animations: + anim.clean_up(*args, **kwargs) class AnimationGroup(Animation): CONFIG = { diff --git a/helpers.py b/helpers.py index 87a04a46..57db6db0 100644 --- a/helpers.py +++ b/helpers.py @@ -304,6 +304,12 @@ def digest_locals(obj, keys = None): def interpolate(start, end, alpha): return (1-alpha)*start + alpha*end +def mid(start, end): + return (start + end)/2.0 + +def anti_interpolate(start, end, value): + return np.true_divide(value - start, end - start) + def clamp(lower, upper, val): if val < lower: return lower From 7bcde6713d48f4045c159125dfa1bef700f0c872 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 13:03:12 -0800 Subject: [PATCH 3/5] Incremental progress on WindingNumber --- active_projects/WindingNumber.py | 154 +++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 50 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 95c90d1d..80e5cd7d 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -542,6 +542,12 @@ class OdometerScene(Scene): rate_func = None) def point_to_rev((x, y)): + # Warning: np.arctan2 would happily discontinuously returns the value 0 for (0, 0), due to + # design choices in the underlying atan2 library call, but for our purposes, this is + # illegitimate, and all winding number calculations must be set up to avoid this + if (x, y) == (0, 0): + print "Error! Angle of (0, 0) computed!" + return None return np.true_divide(np.arctan2(y, x), 2 * np.pi) # Returns the value with the same fractional component as x, closest to m @@ -577,16 +583,16 @@ class RectangleData(): self.rect = (x_interval, y_interval) def get_top_left(self): - return np.array((self.rect[0][0], self.rect[1][0])) + return np.array((self.rect[0][0], self.rect[1][1])) def get_top_right(self): - return np.array((self.rect[0][1], self.rect[1][0])) - - def get_bottom_right(self): return np.array((self.rect[0][1], self.rect[1][1])) + def get_bottom_right(self): + return np.array((self.rect[0][1], self.rect[1][0])) + def get_bottom_left(self): - return np.array((self.rect[0][0], self.rect[1][1])) + return np.array((self.rect[0][0], self.rect[1][0])) def get_top(self): return (self.get_top_left(), self.get_top_right()) @@ -610,22 +616,47 @@ class RectangleData(): elif dim == 1: return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)] else: - print "Error!" + print "RectangleData.splits_on_dim passed illegitimate dimension!" return tuple(return_data) + def split_line_on_dim(self, dim): + x_interval = self.rect[0] + y_interval = self.rect[1] + + if dim == 0: + sides = (self.get_top(), self.get_bottom()) + elif dim == 1: + sides = (self.get_left(), self.get_right()) + + return tuple([mid(x, y) for (x, y) in sides]) + def complex_to_pair(c): return (c.real, c.imag) -class Iterative2dTest(Scene): +def plane_poly_with_roots(*points): + def f((x, y)): + return complex_to_pair(np.prod([complex(x, y) - complex(a,b) for (a,b) in points])) + return f + +def plane_func_from_complex_func(f): + return lambda (x, y) : complex_to_pair(f(complex(x,y))) + +empty_animation = Animation(Mobject()) +def EmptyAnimation(): + return empty_animation + +class EquationSolver2d(Scene): CONFIG = { - "func" : lambda (x, y) : complex_to_pair(complex(x, y)**2 - complex(1, 2)**2), + "func" : plane_poly_with_roots((1, 2), (-1, 3)), "initial_lower_x" : -5.1, "initial_upper_x" : 5.1, "initial_lower_y" : -3.1, "initial_upper_y" : 3.1, - "num_iterations" : 20, - "num_checkpoints" : 10 + "num_iterations" : 10, + "num_checkpoints" : 10, + # TODO: Consider adding a "find_all_roots" flag, which could be turned off + # to only explore one of the two candidate subrectangles when both are viable } def construct(self): @@ -633,8 +664,63 @@ class Iterative2dTest(Scene): num_plane.fade() self.add(num_plane) - num_display = DecimalNumber(0, color = ORANGE) - num_display.move_to(UP + RIGHT) + rev_func = lambda p : point_to_rev(self.func(p)) + + def Animate2dSolver(cur_depth, rect, dim_to_split): + if cur_depth >= self.num_iterations: + return EmptyAnimation() + + def draw_line_return_wind(start, end, start_wind): + alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints) + a0 = alpha_winder(0) + rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind + line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), + stroke_width = 5, + color = RED) + thin_line = line.copy() + thin_line.set_stroke(width = 1) + anim = Succession(ShowCreation, line)#, Transform, line, thin_line) + return (anim, rebased_winder(1)) + + wind_so_far = 0 + anim = EmptyAnimation() + sides = [ + rect.get_top(), + rect.get_right(), + rect.get_bottom(), + rect.get_left() + ] + for (start, end) in sides: + (next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far) + anim = Succession(anim, next_anim) + + total_wind = round(wind_so_far) + + if total_wind == 0: + coords = [ + rect.get_top_left(), + rect.get_top_right(), + rect.get_bottom_right(), + rect.get_bottom_left() + ] + points = [num_plane.coords_to_point(x, y) for (x, y) in coords] + fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = RED) + return Succession(anim, FadeIn(fill_rect)) + else: + (sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split) + sub_rects = [sub_rect1, sub_rect2] + sub_anims = [ + Animate2dSolver( + cur_depth = cur_depth + 1, + rect = sub_rect, + dim_to_split = 1 - dim_to_split + ) + for sub_rect in sub_rects + ] + mid_line_coords = rect.split_line_on_dim(dim_to_split) + mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords] + mid_line = DashedLine(*mid_line_points) + return Succession(anim, ShowCreation(mid_line), AnimationGroup(*sub_anims)) lower_x = self.initial_lower_x upper_x = self.initial_upper_x @@ -646,45 +732,13 @@ class Iterative2dTest(Scene): rect = RectangleData(x_interval, y_interval) - rev_func = lambda p : point_to_rev(self.func(p)) + anim = Animate2dSolver( + cur_depth = 0, + rect = rect, + dim_to_split = 0, + ) - dim_to_split = 0 # 0 for x, 1 for y - - def draw_line_return_wind(start, end, start_wind): - alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints) - a0 = alpha_winder(0) - rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind - line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), - stroke_width = 5, - color = RED) - self.play( - ShowCreation(line), - ChangingDecimal(num_display, rebased_winder) - ) - line.set_color(GREEN) # Temporary hack to see (some) redraws; TODO: figure out a better approach - return rebased_winder(1) - - for i in range(self.num_iterations): - (explore_rect, alt_rect) = rect.splits_on_dim(dim_to_split) - - wind_so_far = 0 - sides = [ - explore_rect.get_top(), - explore_rect.get_right(), - explore_rect.get_bottom(), - explore_rect.get_left() - ] - for (start, end) in sides: - wind_so_far = draw_line_return_wind(start, end, wind_so_far) - - total_wind = round(wind_so_far) - - if total_wind == 0: - rect = alt_rect - else: - rect = explore_rect - - dim_to_split = 1 - dim_to_split + self.play(anim) self.wait() From 123bae00a67575b01dafbc1ce6cf00c9d3e1cf04 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 16:52:31 -0800 Subject: [PATCH 4/5] Further fixes to Succession animations --- animation/simple_animations.py | 38 +++++++++++++++------------------- helpers.py | 2 +- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 68e4176e..c7bd8f89 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -395,13 +395,22 @@ class Succession(Animation): mobject = Group(*[anim.mobject for anim in self.animations]) Animation.__init__(self, mobject, run_time = run_time, **kwargs) + def rewind_to_start(self): + for anim in reversed(self.animations): + anim.update(0) + def update_mobject(self, alpha): + self.rewind_to_start() + for i in range(len(self.animations)): - sub_alpha = anti_interpolate( + sub_alpha = inverse_interpolate( self.critical_alphas[i], self.critical_alphas[i + 1], alpha ) + if sub_alpha < 0: + return + sub_alpha = clamp(0, 1, sub_alpha) # Could possibly adopt a non-clamping convention here self.animations[i].update(sub_alpha) @@ -424,23 +433,10 @@ class AnimationGroup(Animation): for anim in self.sub_anims: anim.update(alpha) - - - - - - - - - - - - - - - - - - - - +# Parallel animations where shorter animations are not stretched out to match the longest +class UnsyncedParallel(AnimationGroup): + def __init__(self, *sub_anims, **kwargs): + digest_config(self, kwargs, locals()) + 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) \ No newline at end of file diff --git a/helpers.py b/helpers.py index 57db6db0..737eb2b7 100644 --- a/helpers.py +++ b/helpers.py @@ -307,7 +307,7 @@ def interpolate(start, end, alpha): def mid(start, end): return (start + end)/2.0 -def anti_interpolate(start, end, value): +def inverse_interpolate(start, end, value): return np.true_divide(value - start, end - start) def clamp(lower, upper, val): From 6e293782ffbb79696facfdc40515fd21d961884c Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 16:53:05 -0800 Subject: [PATCH 5/5] Incremental progress on WindingNumber (equation solver 2d) --- active_projects/WindingNumber.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 80e5cd7d..33478038 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -646,6 +646,9 @@ empty_animation = Animation(Mobject()) def EmptyAnimation(): return empty_animation +# TODO: Perhaps restructure this to avoid using AnimationGroup/UnsyncedParallels, and instead +# use lists of animations or lists or other such data, to be merged and processed into parallel +# animations later class EquationSolver2d(Scene): CONFIG = { "func" : plane_poly_with_roots((1, 2), (-1, 3)), @@ -653,7 +656,7 @@ class EquationSolver2d(Scene): "initial_upper_x" : 5.1, "initial_lower_y" : -3.1, "initial_upper_y" : 3.1, - "num_iterations" : 10, + "num_iterations" : 5, "num_checkpoints" : 10, # TODO: Consider adding a "find_all_roots" flag, which could be turned off # to only explore one of the two candidate subrectangles when both are viable @@ -679,7 +682,10 @@ class EquationSolver2d(Scene): color = RED) thin_line = line.copy() thin_line.set_stroke(width = 1) - anim = Succession(ShowCreation, line)#, Transform, line, thin_line) + anim = Succession( + ShowCreation, line, + Transform, line, thin_line + ) return (anim, rebased_winder(1)) wind_so_far = 0 @@ -720,7 +726,11 @@ class EquationSolver2d(Scene): mid_line_coords = rect.split_line_on_dim(dim_to_split) mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords] mid_line = DashedLine(*mid_line_points) - return Succession(anim, ShowCreation(mid_line), AnimationGroup(*sub_anims)) + return Succession(anim, + ShowCreation(mid_line), + FadeOut(mid_line), + UnsyncedParallel(*sub_anims) + ) lower_x = self.initial_lower_x upper_x = self.initial_upper_x