Merge pull request #76 from 3b1b/WindingNumber

Winding number
This commit is contained in:
Grant Sanderson 2018-01-19 17:34:39 -08:00 committed by GitHub
commit d8b49f471a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 124 deletions

View file

@ -514,7 +514,6 @@ class FuncRotater(Animation):
angle_revs * 2 * np.pi, angle_revs * 2 * np.pi,
) )
self.mobject.set_color(color_func(angle_revs)) self.mobject.set_color(color_func(angle_revs))
# Will want to have arrow colors change to match direction as well
class TestRotater(Scene): class TestRotater(Scene):
def construct(self): def construct(self):
@ -543,6 +542,12 @@ class OdometerScene(Scene):
rate_func = None) rate_func = None)
def point_to_rev((x, y)): 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) return np.true_divide(np.arctan2(y, x), 2 * np.pi)
# Returns the value with the same fractional component as x, closest to m # Returns the value with the same fractional component as x, closest to m
@ -578,16 +583,16 @@ class RectangleData():
self.rect = (x_interval, y_interval) self.rect = (x_interval, y_interval)
def get_top_left(self): 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): 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])) 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): 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): def get_top(self):
return (self.get_top_left(), self.get_top_right()) return (self.get_top_left(), self.get_top_right())
@ -611,22 +616,50 @@ class RectangleData():
elif dim == 1: elif dim == 1:
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)] return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)]
else: else:
print "Error!" print "RectangleData.splits_on_dim passed illegitimate dimension!"
return tuple(return_data) 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): def complex_to_pair(c):
return (c.real, c.imag) return (c.real, c.imag)
class iterative_2d_test(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
# 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 = { 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_lower_x" : -5.1,
"initial_upper_x" : 5.1, "initial_upper_x" : 5.1,
"initial_lower_y" : -3.1, "initial_lower_y" : -3.1,
"initial_upper_y" : 3.1, "initial_upper_y" : 3.1,
"num_iterations" : 20, "num_iterations" : 5,
"num_checkpoints" : 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): def construct(self):
@ -634,8 +667,70 @@ class iterative_2d_test(Scene):
num_plane.fade() num_plane.fade()
self.add(num_plane) self.add(num_plane)
num_display = DecimalNumber(0, color = ORANGE) rev_func = lambda p : point_to_rev(self.func(p))
num_display.move_to(UP + RIGHT)
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),
FadeOut(mid_line),
UnsyncedParallel(*sub_anims)
)
lower_x = self.initial_lower_x lower_x = self.initial_lower_x
upper_x = self.initial_upper_x upper_x = self.initial_upper_x
@ -647,80 +742,13 @@ class iterative_2d_test(Scene):
rect = RectangleData(x_interval, y_interval) 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 self.play(anim)
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 = "#FF0000")
self.play(
ShowCreation(line),
#ChangingDecimal(num_display, rebased_winder)
)
line.set_color("#00FF00")
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
)
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)
if total_wind == 0:
rect = alt_rect
else:
rect = explore_rect
dim_to_split = 1 - dim_to_split
self.wait() 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
}

View file

@ -357,7 +357,7 @@ class Succession(Animation):
""" """
Each arg will either be an animation, or an animation class Each arg will either be an animation, or an animation class
followed by its arguments (and potentially a dict for followed by its arguments (and potentially a dict for
configuraiton). configuration).
For example, For example,
Succession( Succession(
@ -415,27 +415,36 @@ class Succession(Animation):
#might very well mess with it. #might very well mess with it.
self.original_run_time = run_time 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]) mobject = Group(*[anim.mobject for anim in self.animations])
Animation.__init__(self, mobject, run_time = run_time, **kwargs) 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): def update_mobject(self, alpha):
if alpha >= 1.0: self.rewind_to_start()
self.animations[-1].update(1)
return for i in range(len(self.animations)):
run_times = self.run_times sub_alpha = inverse_interpolate(
index = 0 self.critical_alphas[i],
time = alpha*self.original_run_time self.critical_alphas[i + 1],
while sum(run_times[:index+1]) < time: alpha
index += 1 )
if index > self.last_index: if sub_alpha < 0:
self.animations[self.last_index].update(1) return
self.animations[self.last_index].clean_up()
self.last_index = index sub_alpha = clamp(0, 1, sub_alpha) # Could possibly adopt a non-clamping convention here
curr_anim = self.animations[index] self.animations[i].update(sub_alpha)
sub_alpha = np.clip(
(time - sum(run_times[:index]))/run_times[index], 0, 1 def clean_up(self, *args, **kwargs):
) for anim in self.animations:
curr_anim.update(sub_alpha) anim.clean_up(*args, **kwargs)
class AnimationGroup(Animation): class AnimationGroup(Animation):
CONFIG = { CONFIG = {
@ -452,23 +461,10 @@ class AnimationGroup(Animation):
for anim in self.sub_anims: for anim in self.sub_anims:
anim.update(alpha) 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)

View file

@ -304,6 +304,12 @@ def digest_locals(obj, keys = None):
def interpolate(start, end, alpha): def interpolate(start, end, alpha):
return (1-alpha)*start + alpha*end return (1-alpha)*start + alpha*end
def mid(start, end):
return (start + end)/2.0
def inverse_interpolate(start, end, value):
return np.true_divide(value - start, end - start)
def clamp(lower, upper, val): def clamp(lower, upper, val):
if val < lower: if val < lower:
return lower return lower