Merge branch 'master' into eop

This commit is contained in:
Ben Hambrecht
2018-05-10 00:00:40 +02:00
7 changed files with 462 additions and 80 deletions

View File

@ -3,16 +3,78 @@ from big_ol_pile_of_manim_imports import *
class NumberlineTransformationScene(Scene):
CONFIG = {
"input_line_center": 2 * UP,
"output_line_center": 2 * DOWN,
"number_line_config": {
"include_numbers": True,
"x_min": -3.5,
"x_max": 3.5,
"unit_size": 2,
},
# These would override number_line_config
"input_line_config": {
"color": BLUE,
},
"output_line_config": {},
"num_inserted_number_line_anchors": 20,
}
def setup(self):
pass
self.setup_number_lines()
self.setup_titles()
def setup_number_lines(self):
number_lines = self.number_lines = VGroup()
added_configs = (self.input_line_config, self.output_line_config)
centers = (self.input_line_center, self.output_line_center)
for added_config, center in zip(added_configs, centers):
full_config = dict(self.number_line_config)
full_config.update(added_config)
number_line = NumberLine(**full_config)
number_line.main_line.insert_n_anchor_points(
self.num_inserted_number_line_anchors
)
number_line.move_to(center)
number_lines.add(number_line)
self.input_line, self.output_line = number_lines
self.add(number_lines)
def setup_titles(self):
input_title, output_title = self.titles = VGroup(*[
TextMobject()
for word in "Inputs", "Outputs"
])
for title, line in zip(self.titles, self.number_lines):
title.next_to(line, UP)
title.shift_onto_screen()
self.add(self.titles)
def get_line_mapping_animation(self, func, run_time=3, path_arc=30 * DEGREES):
input_line, output_line = self.number_lines
input_line_copy = input_line.deepcopy()
input_line_copy.remove(input_line_copy.numbers)
input_line_copy.set_stroke(width=2)
def point_func(point):
input_number = input_line.point_to_number(point)
output_number = func(input_number)
return output_line.number_to_point(output_number)
return ApplyPointwiseFunction(
point_func, input_line_copy,
run_time=run_time,
path_arc=path_arc,
)
class ExampleNumberlineTransformationScene(NumberlineTransformationScene):
def construct(self):
pass
func = lambda x: x**2
anim = self.get_line_mapping_animation(func)
self.play(anim)
self.wait()
# Scenes
@ -47,18 +109,14 @@ class WriteOpeningWords(Scene):
class StartingCalc101(PiCreatureScene):
CONFIG = {
# "default_pi_creature_kwargs": {
# "color": BLUE,
# "flip_at_start": False,
# },
}
def construct(self):
randy = self.pi_creature
deriv_string = "\\frac{df}{dx}(x) = \\lim(\\delta x \\to \\infty)" + \
"{f(x + \\delta x) - f(x) \\over \\delta x}"
equations = VGroup(
TexMobject(*break_up_string_by_terms(deriv_string, "\\delta x"))
deriv_equation = TexMobject(
"\\frac{df}{dx}(x) = \\lim_{\\Delta x \\to \\infty}" +
"{f(x + \\Delta x) - f(x) \\over \\Delta x}",
tex_to_color_map={"\\Delta x": BLUE}
)
title = TextMobject("Calculus 101")
title.to_edge(UP)
@ -71,3 +129,273 @@ class StartingCalc101(PiCreatureScene):
self.wait()
class StandardDerivativeVisual(GraphScene):
CONFIG = {
"y_max": 8,
"y_axis_height": 5,
}
def construct(self):
self.add_title()
self.show_function_graph()
self.show_slope_of_graph()
self.encourage_not_to_think_of_slope_as_definition()
self.show_sensitivity()
def add_title(self):
title = self.title = TextMobject("Standard derivative visual")
title.to_edge(UP)
h_line = Line(LEFT, RIGHT)
h_line.scale_to_fit_width(FRAME_WIDTH - 2 * LARGE_BUFF)
h_line.next_to(title, DOWN)
self.add(title, h_line)
def show_function_graph(self):
self.setup_axes()
def func(x):
x -= 5
return 0.1 * (x + 3) * (x - 3) * x + 3
graph = self.get_graph(func)
graph_label = self.get_graph_label(graph, x_val=9.5)
input_tracker = ValueTracker(4)
def get_x_value():
return input_tracker.get_value()
def get_y_value():
return graph.underlying_function(get_x_value())
def get_x_point():
return self.coords_to_point(get_x_value(), 0)
def get_y_point():
return self.coords_to_point(0, get_y_value())
def get_graph_point():
return self.coords_to_point(get_x_value(), get_y_value())
def get_v_line():
return DashedLine(get_x_point(), get_graph_point(), stroke_width=2)
def get_h_line():
return DashedLine(get_graph_point(), get_y_point(), stroke_width=2)
input_triangle = RegularPolygon(n=3, start_angle=TAU / 4)
output_triangle = RegularPolygon(n=3, start_angle=0)
for triangle in input_triangle, output_triangle:
triangle.set_fill(WHITE, 1)
triangle.set_stroke(width=0)
triangle.scale(0.1)
input_triangle_update = ContinualUpdateFromFunc(
input_triangle, lambda m: m.move_to(get_x_point(), UP)
)
output_triangle_update = ContinualUpdateFromFunc(
output_triangle, lambda m: m.move_to(get_y_point(), RIGHT)
)
x_label = TexMobject("x")
x_label_update = ContinualUpdateFromFunc(
x_label, lambda m: m.next_to(input_triangle, DOWN, SMALL_BUFF)
)
output_label = TexMobject("f(x)")
output_label_update = ContinualUpdateFromFunc(
output_label, lambda m: m.next_to(
output_triangle, LEFT, SMALL_BUFF)
)
v_line = get_v_line()
v_line_update = ContinualUpdateFromFunc(
v_line, lambda vl: Transform(vl, get_v_line()).update(1)
)
h_line = get_h_line()
h_line_update = ContinualUpdateFromFunc(
h_line, lambda hl: Transform(hl, get_h_line()).update(1)
)
graph_dot = Dot(color=YELLOW)
graph_dot_update = ContinualUpdateFromFunc(
graph_dot, lambda m: m.move_to(get_graph_point())
)
self.play(
ShowCreation(graph),
Write(graph_label),
)
self.play(
DrawBorderThenFill(input_triangle, run_time=1),
Write(x_label),
ShowCreation(v_line),
GrowFromCenter(graph_dot),
)
self.add_foreground_mobject(graph_dot)
self.play(
ShowCreation(h_line),
Write(output_label),
DrawBorderThenFill(output_triangle, run_time=1)
)
self.add(
input_triangle_update,
x_label_update,
graph_dot_update,
v_line_update,
h_line_update,
output_triangle_update,
output_label_update,
)
self.play(
input_tracker.set_value, 8,
run_time=6,
rate_func=there_and_back
)
self.input_tracker = input_tracker
self.graph = graph
def show_slope_of_graph(self):
input_tracker = self.input_tracker
deriv_input_tracker = ValueTracker(input_tracker.get_value())
# Slope line
def get_slope_line():
return self.get_secant_slope_group(
x=deriv_input_tracker.get_value(),
graph=self.graph,
dx=0.01,
secant_line_length=4
).secant_line
slope_line = get_slope_line()
slope_line_update = ContinualUpdateFromFunc(
slope_line, lambda sg: Transform(sg, get_slope_line()).update(1)
)
def position_deriv_label(deriv_label):
deriv_label.next_to(slope_line, UP)
return deriv_label
deriv_label = TexMobject(
"\\frac{df}{dx}(x) =", "\\text{Slope}", "="
)
deriv_label.get_part_by_tex("Slope").match_color(slope_line)
deriv_label_update = ContinualUpdateFromFunc(
deriv_label, position_deriv_label
)
slope_decimal = DecimalNumber(slope_line.get_slope())
slope_decimal.match_color(slope_line)
slope_decimal_update = ContinualChangingDecimal(
slope_decimal, lambda dt: slope_line.get_slope(),
position_update_func=lambda m: m.next_to(
deriv_label, RIGHT, SMALL_BUFF
).shift(0.2 * SMALL_BUFF * DOWN)
)
self.play(
ShowCreation(slope_line),
Write(deriv_label),
Write(slope_decimal),
run_time=1
)
self.wait()
self.add(
slope_line_update,
# deriv_label_update,
slope_decimal_update,
)
for x in 9, 2, 4:
self.play(
input_tracker.set_value, x,
deriv_input_tracker.set_value, x,
run_time=3
)
self.wait()
self.deriv_input_tracker = deriv_input_tracker
def encourage_not_to_think_of_slope_as_definition(self):
morty = Mortimer(height=2)
morty.to_corner(DR)
self.play(FadeIn(morty))
self.play(PiCreatureSays(
morty, "Don't think of \\\\ this as the definition",
bubble_kwargs={"height": 2, "width": 4}
))
self.play(Blink(morty))
self.wait()
self.play(
RemovePiCreatureBubble(morty),
UpdateFromAlphaFunc(
morty, lambda m, a: m.set_fill(opacity=1 - a),
remover=True
)
)
def show_sensitivity(self):
input_tracker = self.input_tracker
deriv_input_tracker = self.deriv_input_tracker
self.wiggle_input()
for x in 9, 7, 2:
self.play(
input_tracker.set_value, x,
deriv_input_tracker.set_value, x,
run_time=3
)
self.wiggle_input()
###
def wiggle_input(self, dx=0.5, run_time=3):
input_tracker = self.input_tracker
x = input_tracker.get_value()
x_min = x - dx
x_max = x + dx
y, y_min, y_max = map(
self.graph.underlying_function,
[x, x_min, x_max]
)
x_line = Line(
self.coords_to_point(x_min, 0),
self.coords_to_point(x_max, 0),
)
y_line = Line(
self.coords_to_point(0, y_min),
self.coords_to_point(0, y_max),
)
x_rect, y_rect = rects = VGroup(Rectangle(), Rectangle())
rects.set_stroke(width=0)
rects.set_fill(YELLOW, 0.5)
x_rect.match_width(x_line)
x_rect.stretch_to_fit_height(0.25)
x_rect.move_to(x_line)
y_rect.match_height(y_line)
y_rect.stretch_to_fit_width(0.25)
y_rect.move_to(y_line)
self.play(
ApplyMethod(
input_tracker.set_value, input_tracker.get_value() + dx,
rate_func=lambda t: wiggle(t, 6)
),
FadeIn(
rects,
rate_func=squish_rate_func(smooth, 0, 0.33),
remover=True,
),
run_time=run_time,
)
self.play(FadeOut(rects))
class EoCWrapper(Scene):
def construct(self):
title = Title("Essence of calculus")
self.play(Write(title))
self.wait()

View File

@ -1,15 +1,17 @@
from utils.config_ops import digest_config
# Currently, this is only used by both Scene and MOBject.
# Currently, this is only used by both Scene and Mobject.
# Still, we abstract its functionality here, albeit purely nominally.
# All actual implementation has to be handled by derived classes for now.
#
# Note that although the prototypical instances add and remove MObjects,
# Note that although the prototypical instances add and remove Mobjects,
# there is also the possibility to add ContinualAnimations to Scenes. Thus,
# in the Container class in general, we do not make any presumptions about
# what types of objects may be added; this is again dependent on the specific
# derived instance.
# TODO: Move the "remove" functionality of Scene to this class
class Container(object):
def __init__(self, *submobjects, **kwargs):

View File

@ -616,23 +616,19 @@ class Mobject(Container):
##
def reduce_across_dimension(self, points_func, reduce_func, dim):
try:
points = self.get_points_defining_boundary()
values = [points_func(points[:, dim])]
except:
values = []
values += [
mob.reduce_across_dimension(points_func, reduce_func, dim)
for mob in self.nonempty_submobjects()
]
try:
return reduce_func(values)
except:
points = self.get_all_points()
if points is None or len(points) == 0:
# Note, this default means things like empty VGroups
# will appear to have a center at [0, 0, 0]
return 0
values = points_func(points[:, dim])
return reduce_func(values)
def nonempty_submobjects(self):
return [submob for submob in self.submobjects
if len(submob.submobjects) != 0 or len(submob.points) != 0]
return [
submob for submob in self.submobjects
if len(submob.submobjects) != 0 or len(submob.points) != 0
]
def get_merged_array(self, array_attr):
result = None

View File

@ -13,7 +13,7 @@ from utils.config_ops import digest_config
class NumberLine(VMobject):
CONFIG = {
"color": BLUE,
"color": LIGHT_GREY,
"x_min": -FRAME_X_RADIUS,
"x_max": FRAME_X_RADIUS,
"unit_size": 1,
@ -21,6 +21,7 @@ class NumberLine(VMobject):
"tick_frequency": 1,
"leftmost_tick": None, # Defaults to value near x_min s.t. 0 is a tick
"numbers_with_elongated_ticks": [0],
"include_numbers": False,
"numbers_to_show": None,
"longer_tick_multiple": 2,
"number_at_center": 0,
@ -39,6 +40,8 @@ class NumberLine(VMobject):
VMobject.__init__(self, **kwargs)
if self.include_tip:
self.add_tip()
if self.include_numbers:
self.add_numbers()
def generate_points(self):
self.main_line = Line(self.x_min * RIGHT, self.x_max * RIGHT)
@ -133,7 +136,7 @@ class NumberLine(VMobject):
self.numbers = self.get_number_mobjects(
*numbers, **kwargs
)
self.add(*self.numbers)
self.add(self.numbers)
return self
def add_tip(self):

View File

@ -175,7 +175,7 @@ class CompleteGraph(Graph):
return Graph.__str__(self) + str(self.num_vertices)
class GraphScene(Scene):
class DiscreteGraphScene(Scene):
args_list = [
(CubeGraph(),),
(SampleGraph(),),

View File

@ -50,8 +50,8 @@ class GraphScene(Scene):
"default_input_color": YELLOW,
"default_riemann_start_color": BLUE,
"default_riemann_end_color": GREEN,
"area_opacity" : 0.8,
"num_rects" : 50,
"area_opacity": 0.8,
"num_rects": 50,
}
def setup(self):
@ -294,17 +294,16 @@ class GraphScene(Scene):
for n in range(n_iterations)
]
def get_area(self, graph, t_min, t_max):
numerator = max(t_max - t_min, 0.0001)
dx = float(numerator) / self.num_rects
return self.get_riemann_rectangles(
graph,
x_min = t_min,
x_max = t_max,
dx = dx,
stroke_width = 0,
).set_fill(opacity = self.area_opacity)
x_min=t_min,
x_max=t_max,
dx=dx,
stroke_width=0,
).set_fill(opacity=self.area_opacity)
def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs):
transform_kwargs = {
@ -443,29 +442,28 @@ class GraphScene(Scene):
return group
def add_T_label(self, x_val, side = RIGHT, label = None, color = WHITE, animated = False, **kwargs):
triangle = RegularPolygon(n=3, start_angle = np.pi/2)
def add_T_label(self, x_val, side=RIGHT, label=None, color=WHITE, animated=False, **kwargs):
triangle = RegularPolygon(n=3, start_angle=np.pi / 2)
triangle.scale_to_fit_height(MED_SMALL_BUFF)
triangle.move_to(self.coords_to_point(x_val, 0), UP)
triangle.set_fill(color, 1)
triangle.set_stroke(width = 0)
triangle.set_stroke(width=0)
if label == None:
T_label = TexMobject(self.variable_point_label, fill_color = color)
T_label = TexMobject(self.variable_point_label, fill_color=color)
else:
T_label = TexMobject(label, fill_color = color)
T_label = TexMobject(label, fill_color=color)
T_label.next_to(triangle, DOWN)
v_line = self.get_vertical_line_to_graph(
x_val, self.v_graph,
color = YELLOW
color=YELLOW
)
if animated:
self.play(
DrawBorderThenFill(triangle),
ShowCreation(v_line),
Write(T_label, run_time = 1),
Write(T_label, run_time=1),
**kwargs
)
@ -478,16 +476,13 @@ class GraphScene(Scene):
self.right_v_line = v_line
self.add(self.right_T_label_group, self.right_v_line)
def get_animation_integral_bounds_change(
self,
graph,
new_t_min,
new_t_max,
fade_close_to_origin = True,
run_time = 1.0
fade_close_to_origin=True,
run_time=1.0
):
curr_t_min = self.x_axis.point_to_number(self.area.get_left())
curr_t_max = self.x_axis.point_to_number(self.area.get_right())
@ -506,8 +501,8 @@ class GraphScene(Scene):
area, left_v_line, left_T_label, right_v_line, right_T_label = group
t_min = interpolate(curr_t_min, new_t_min, alpha)
t_max = interpolate(curr_t_max, new_t_max, alpha)
new_area = self.get_area(graph,t_min, t_max)
new_area = self.get_area(graph, t_min, t_max)
new_left_v_line = self.get_vertical_line_to_graph(
t_min, graph
)
@ -523,19 +518,16 @@ class GraphScene(Scene):
# Fade close to 0
if fade_close_to_origin:
if len(left_T_label) > 0:
left_T_label[0].set_fill(opacity = min(1, np.abs(t_min)))
left_T_label[0].set_fill(opacity=min(1, np.abs(t_min)))
if len(right_T_label) > 0:
right_T_label[0].set_fill(opacity = min(1, np.abs(t_max)))
right_T_label[0].set_fill(opacity=min(1, np.abs(t_max)))
Transform(area, new_area).update(1)
Transform(left_v_line, new_left_v_line).update(1)
Transform(right_v_line, new_right_v_line).update(1)
return group
return UpdateFromAlphaFunc(group, update_group, run_time = run_time)
return UpdateFromAlphaFunc(group, update_group, run_time=run_time)
def animate_secant_slope_group_change(
self, secant_slope_group,
@ -578,23 +570,3 @@ class GraphScene(Scene):
)
secant_slope_group.kwargs["x"] = target_x
secant_slope_group.kwargs["dx"] = target_dx

81
utils/tex_file_writing.py Normal file
View File

@ -0,0 +1,81 @@
import os
from constants import TEX_DIR
from constants import TEX_TEXT_TO_REPLACE
def tex_hash(expression, template_tex_file):
return str(hash(expression + template_tex_file))
def tex_to_svg_file(expression, template_tex_file):
tex_file = generate_tex_file(expression, template_tex_file)
dvi_file = tex_to_dvi(tex_file)
return dvi_to_svg(dvi_file)
def generate_tex_file(expression, template_tex_file):
result = os.path.join(
TEX_DIR,
tex_hash(expression, template_tex_file)
) + ".tex"
if not os.path.exists(result):
print("Writing \"%s\" to %s" % (
"".join(expression), result
))
with open(template_tex_file, "r") as infile:
body = infile.read()
body = body.replace(TEX_TEXT_TO_REPLACE, expression)
with open(result, "w") as outfile:
outfile.write(body)
return result
def get_null():
if os.name == "nt":
return "NUL"
return "/dev/null"
def tex_to_dvi(tex_file):
result = tex_file.replace(".tex", ".dvi")
if not os.path.exists(result):
commands = [
"latex",
"-interaction=batchmode",
"-halt-on-error",
"-output-directory=" + TEX_DIR,
tex_file,
">",
get_null()
]
exit_code = os.system(" ".join(commands))
if exit_code != 0:
log_file = tex_file.replace(".tex", ".log")
raise Exception(
"Latex error converting to dvi. "
"See log output above or the log file: %s" % log_file)
return result
def dvi_to_svg(dvi_file, regen_if_exists=False):
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
result = dvi_file.replace(".dvi", ".svg")
if not os.path.exists(result):
commands = [
"dvisvgm",
dvi_file,
"-n",
"-v",
"0",
"-o",
result,
">",
get_null()
]
os.system(" ".join(commands))
return result