From 42a8e166f0e1772f22bb4172733fd4fad43a12f8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 30 Sep 2015 14:22:17 -0700 Subject: [PATCH] First draft of music and measure theory --- mobject/function_graphs.py | 66 +++- mobject/image_mobject.py | 19 +- mobject/simple_mobjects.py | 2 +- sample_script.py | 14 +- scene/__init__.py | 3 +- scene/number_line.py | 65 +++- scripts/music_and_measure.py | 638 ++++++++++++++++++++++++++++++++--- 7 files changed, 739 insertions(+), 68 deletions(-) diff --git a/mobject/function_graphs.py b/mobject/function_graphs.py index 202bdc99..ef84f086 100644 --- a/mobject/function_graphs.py +++ b/mobject/function_graphs.py @@ -92,38 +92,44 @@ class Grid(Mobject1D): class NumberLine(Mobject1D): DEFAULT_CONFIG = { - "radius" : SPACE_WIDTH, + "color" : "skyblue", + "numerical_radius" : SPACE_WIDTH, "unit_length_to_spacial_width" : 1, "tick_size" : 0.1, "tick_frequency" : 0.5, + "leftmost_tick" : -int(SPACE_WIDTH), "number_at_center" : 0, "numbers_with_elongated_ticks" : [0], "longer_tick_multiple" : 2, } def __init__(self, **kwargs): digest_config(self, NumberLine, kwargs) - numerical_radius = float(self.radius) / self.unit_length_to_spacial_width - self.left_num = self.number_at_center - numerical_radius - self.right_num = self.number_at_center + numerical_radius + self.left_num = self.number_at_center - self.numerical_radius + self.right_num = self.number_at_center + self.numerical_radius Mobject1D.__init__(self, **kwargs) def generate_points(self): + spacial_radius = self.numerical_radius*self.unit_length_to_spacial_width self.add_points([ (b*x, 0, 0) - for x in np.arange(0, self.radius, self.epsilon) + for x in np.arange(0, spacial_radius, self.epsilon) for b in [-1, 1] ]) self.index_of_left = np.argmin(self.points[:,0]) self.index_of_right = np.argmax(self.points[:,0]) spacial_tick_frequency = self.tick_frequency*self.unit_length_to_spacial_width self.add_points([ - (b*x, y, 0) - for x in np.arange(0, self.radius, spacial_tick_frequency) + (x, y, 0) + for num in self.get_tick_numbers() for y in np.arange(-self.tick_size, self.tick_size, self.epsilon) - for b in ([1, -1] if x > 0 else [1]) + for x in [self.number_to_point(num)[0]] ]) for number in self.numbers_with_elongated_ticks: self.elongate_tick_at(number, self.longer_tick_multiple) + self.number_of_points_without_numbers = self.get_num_points() + + def get_tick_numbers(self): + return np.arange(self.leftmost_tick, self.right_num, self.tick_frequency) def elongate_tick_at(self, number, multiple = 2): x = self.number_to_point(number)[0] @@ -144,21 +150,51 @@ class NumberLine(Mobject1D): float(number-self.left_num)/(self.right_num - self.left_num) ) - def add_numbers(self, *numbers): + def point_to_number(self, point): + return self.number_at_center + point[0]/self.unit_length_to_spacial_width + + def default_numbers_to_display(self): + return self.get_tick_numbers()[::2] + + def get_vertical_number_offset(self): + return 4*DOWN*self.tick_size + + def get_number_mobjects(self, *numbers): if len(numbers) == 0: - numbers = range(int(self.left_num), int(self.right_num+1)) + numbers = self.default_numbers_to_display() + log_spacing = int(np.log10(self.tick_frequency)) + if log_spacing < 0: + num_decimal_places = 2-log_spacing + else: + num_decimal_places = 1+log_spacing + result = [] for number in numbers: - mob = tex_mobject(str(number)).scale(0.5) + if number < 0: + num_string = str(number)[:num_decimal_places+1] + else: + num_string = str(number)[:num_decimal_places] + mob = tex_mobject(num_string) + vert_scale = 2*self.tick_size/mob.get_height() + hori_scale = self.tick_frequency*self.unit_length_to_spacial_width/mob.get_width() + mob.scale(min(vert_scale, hori_scale)) mob.shift(self.number_to_point(number)) - mob.shift(DOWN*4*self.tick_size) - self.add(mob) - return self + mob.shift(self.get_vertical_number_offset()) + result.append(mob) + return result + + def add_numbers(self, *numbers): + self.add(*self.get_number_mobjects(*numbers)) + + def remove_numbers(self): + self.points = self.points[:self.number_of_points_without_numbers] + self.rgbs = self.rgbs[:self.number_of_points_without_numbers] class UnitInterval(NumberLine): DEFAULT_CONFIG = { - "radius" : SPACE_WIDTH-1, + "numerical_radius" : 0.5, "unit_length_to_spacial_width" : 2*(SPACE_WIDTH-1), "tick_frequency" : 0.1, + "leftmost_tick" : 0, "number_at_center" : 0.5, "numbers_with_elongated_ticks" : [0, 1], } diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index 051c38c4..b3ed4f88 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -16,7 +16,8 @@ class ImageMobject(Mobject): "invert" : True, "use_cache" : True, "should_buffer_points" : False, - "scale_value" : 1.0 + "scale_value" : 1.0, + "should_center" : True } def __init__(self, image_file, **kwargs): digest_config(self, ImageMobject, kwargs, locals()) @@ -35,7 +36,8 @@ class ImageMobject(Mobject): if os.path.exists(path): self.generate_points_from_file(path) self.scale(self.scale_value) - self.center() + if self.should_center: + self.center() return raise IOError("File not Found") @@ -135,12 +137,19 @@ def tex_mobject(expression, size = "\\large" #Todo, make this more sophisticated. image_files = tex_to_image(expression, size, template_tex_file) + config = { + "should_buffer_points" : True, + "should_center" : False, + } if isinstance(image_files, list): #TODO, is checking listiness really the best here? - result = CompoundMobject(*map(ImageMobject, image_files)) + result = CompoundMobject(*[ + ImageMobject(f, **config) + for f in image_files + ]) else: - result = ImageMobject(image_files, should_buffer_points = True) - return result.highlight("white") + result = ImageMobject(image_files, **config) + return result.center().highlight("white") diff --git a/mobject/simple_mobjects.py b/mobject/simple_mobjects.py index 8fc64f66..bd5f7c4e 100644 --- a/mobject/simple_mobjects.py +++ b/mobject/simple_mobjects.py @@ -16,7 +16,7 @@ class Point(Mobject): Mobject.__init__(self, **kwargs) def generate_points(self): - self.add_points(self.location) + self.add_points([self.location]) class Dot(Mobject1D): #Use 1D density, even though 2D diff --git a/sample_script.py b/sample_script.py index 106bc3b0..cf44a9db 100644 --- a/sample_script.py +++ b/sample_script.py @@ -10,16 +10,18 @@ from animation import * from mobject import * from constants import * from region import * -from scene import Scene +from scene import Scene, NumberLineScene from script_wrapper import command_line_create_scene -class SampleScene(Scene): +class SampleScene(NumberLineScene): def construct(self): - square = Square() - self.add(square) - self.dither() - self.play(Flash(square)) + NumberLineScene.construct(self) + arrow = Arrow(2*RIGHT+UP, 2*RIGHT) + self.add(arrow) + self.dither(2) + self.zoom_in_on(2.4, zoom_factor = 10) + self.dither(2) if __name__ == "__main__": command_line_create_scene() \ No newline at end of file diff --git a/scene/__init__.py b/scene/__init__.py index ed367da8..2c2ea355 100644 --- a/scene/__init__.py +++ b/scene/__init__.py @@ -3,4 +3,5 @@ from sub_scenes import * from arithmetic_scenes import * from counting_scene import * from pascals_triangle import * -from scene_from_video import * \ No newline at end of file +from scene_from_video import * +from number_line import * \ No newline at end of file diff --git a/scene/number_line.py b/scene/number_line.py index 6423c72f..2a58cd5b 100644 --- a/scene/number_line.py +++ b/scene/number_line.py @@ -12,4 +12,67 @@ from helpers import * class NumberLineScene(Scene): def construct(self): - pass \ No newline at end of file + self.number_line = NumberLine() + self.displayed_numbers = self.number_line.default_numbers_to_display() + self.number_mobs = self.number_line.get_number_mobjects(*self.displayed_numbers) + self.add(self.number_line, *self.number_mobs) + + def zoom_in_on(self, number, zoom_factor, run_time = 2.0): + unit_length_to_spacial_width = self.number_line.unit_length_to_spacial_width*zoom_factor + radius = SPACE_WIDTH/unit_length_to_spacial_width + tick_frequency = 10**(np.floor(np.log10(radius))) + left_tick = tick_frequency*(np.ceil((number-radius)/tick_frequency)) + new_number_line = NumberLine( + numerical_radius = radius, + unit_length_to_spacial_width = unit_length_to_spacial_width, + tick_frequency = tick_frequency, + leftmost_tick = left_tick, + number_at_center = number + ) + new_displayed_numbers = new_number_line.default_numbers_to_display() + new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers) + + transforms = [] + additional_mobjects = [] + squished_new_line = deepcopy(new_number_line) + squished_new_line.scale(1.0/zoom_factor) + squished_new_line.shift(self.number_line.number_to_point(number)) + squished_new_line.points[:,1] = self.number_line.number_to_point(0)[1] + transforms.append(Transform(squished_new_line, new_number_line)) + for mob, num in zip(new_number_mobs, new_displayed_numbers): + point = Point(self.number_line.number_to_point(num)) + point.shift(new_number_line.get_vertical_number_offset()) + transforms.append(Transform(point, mob)) + for mob in self.mobjects: + if mob == self.number_line: + new_mob = deepcopy(mob) + new_mob.shift(-self.number_line.number_to_point(number)) + new_mob.stretch(zoom_factor, 0) + transforms.append(Transform(mob, new_mob)) + continue + mob_center = mob.get_center() + number_under_center = self.number_line.point_to_number(mob_center) + new_point = new_number_line.number_to_point(number_under_center) + new_point += mob_center[1]*UP + if mob in self.number_mobs: + transforms.append(Transform(mob, Point(new_point))) + else: + transforms.append(ApplyMethod(mob.shift, new_point - mob_center)) + additional_mobjects.append(mob) + line_to_hide_pixelation = Line( + self.number_line.get_left(), + self.number_line.get_right(), + color = self.number_line.get_color() + ) + self.add(line_to_hide_pixelation) + self.play(*transforms, run_time = run_time) + self.clear() + self.number_line = new_number_line + self.displayed_numbers = new_displayed_numbers + self.number_mobs = new_number_mobs + self.add(self.number_line, *self.number_mobs) + self.add(*additional_mobjects) + + + + diff --git a/scripts/music_and_measure.py b/scripts/music_and_measure.py index 76bc92da..77607ebf 100644 --- a/scripts/music_and_measure.py +++ b/scripts/music_and_measure.py @@ -10,7 +10,7 @@ from animation import * from mobject import * from constants import * from region import * -from scene import Scene +from scene import Scene, NumberLineScene from script_wrapper import command_line_create_scene from inventing_math import underbrace @@ -64,15 +64,32 @@ def zero_to_one_interval(): interval.add(tex_mobject("1").shift(INTERVAL_RADIUS*RIGHT+DOWN)) return interval -class OpenInterval(Mobject): +class LeftParen(Mobject): + def generate_points(self): + self.add(tex_mobject("(")) + self.center() + + def get_center(self): + return Mobject.get_center(self) + 0.04*LEFT + +class RightParen(Mobject): + def generate_points(self): + self.add(tex_mobject(")")) + self.center() + + def get_center(self): + return Mobject.get_center(self) + 0.04*RIGHT + + +class OpenInterval(CompoundMobject): def __init__(self, center_point = ORIGIN, width = 2, **kwargs): digest_config(self, OpenInterval, kwargs, locals()) - Mobject.__init__(self, **kwargs) - self.add(tex_mobject("(").shift(LEFT)) - self.add(tex_mobject(")").shift(RIGHT)) - scale_factor = width / 2.0 - self.stretch(scale_factor, 0) - self.stretch(0.5+0.5*scale_factor, 1) + left = LeftParen().shift(LEFT*width/2) + right = RightParen().shift(RIGHT*width/2) + CompoundMobject.__init__(self, left, right, **kwargs) + # scale_factor = width / 2.0 + # self.stretch(scale_factor, 0) + # self.stretch(0.5+0.5*scale_factor, 1) self.shift(center_point) class Piano(ImageMobject): @@ -138,10 +155,12 @@ class VibratingString(Animation): self.mobject.shift(self.center) -class IntervalScene(Scene): +class IntervalScene(NumberLineScene): def construct(self): - self.interval = UnitInterval() - self.add(self.interval) + self.number_line = UnitInterval() + self.displayed_numbers = [0, 1] + self.number_mobs = self.number_line.get_number_mobjects(*self.displayed_numbers) + self.add(self.number_line, *self.number_mobs) def show_all_fractions(self, num_fractions = 27, @@ -155,8 +174,8 @@ class IntervalScene(Scene): self.remove(frac_mob, tick) def add_fraction(self, fraction, shrink = False): - point = self.num_to_point(fraction) - tick_rad = self.interval.tick_size*TICK_STRETCH_FACTOR + point = self.number_line.number_to_point(fraction) + tick_rad = self.number_line.tick_size*TICK_STRETCH_FACTOR frac_mob = fraction_mobject(fraction) if shrink: scale_factor = 2.0/fraction.denominator @@ -168,27 +187,60 @@ class IntervalScene(Scene): self.add(frac_mob, tick) return frac_mob, tick + def add_fraction_ticks(self, num_fractions = 1000, run_time = 0): + long_tick_size = self.number_line.tick_size*TICK_STRETCH_FACTOR + all_ticks = [] + for frac, count in zip(rationals(), range(num_fractions)): + point = self.number_line.number_to_point(frac) + tick_rad = 2.0*long_tick_size/frac.denominator + tick = Line(point+tick_rad*DOWN, point+tick_rad*UP) + tick.highlight("yellow") + all_ticks.append(tick) + all_ticks = CompoundMobject(*all_ticks) + if run_time > 0: + self.play(ShowCreation(all_ticks)) + else: + self.add(all_ticks) + return all_ticks + + def cover_fractions(self, epsilon = 0.3, - num_fractions = 9, + num_fractions = 10, run_time_per_interval = 0.5): - for fraction, count in zip(rationals(), range(num_fractions)): - self.add_open_interval( + intervals = [] + lines = [] + num_intervals = 0 + all_rationals = rationals() + count = 0 + while True: + fraction = all_rationals.next() + count += 1 + if num_intervals >= num_fractions: + break + if fraction < self.number_line.left_num or fraction > self.number_line.right_num: + continue + num_intervals += 1 + interval, line = self.add_open_interval( fraction, - epsilon / 2**(count+1), + epsilon / min(2**count, 2**30), run_time = run_time_per_interval ) + intervals.append(interval) + lines.append(line) + return intervals, lines def add_open_interval(self, num, width, color = None, run_time = 0): - width *= self.interval.unit_length_to_spacial_width - center_point = self.interval.number_to_point(num) - open_interval = OpenInterval(center_point, width) - if color: - open_interval.highlight(color) - interval_line = Line(open_interval.get_left(), open_interval.get_right()) - interval_line.scale_in_place(0.9)#Silliness + spacial_width = width*self.number_line.unit_length_to_spacial_width + center_point = self.number_line.number_to_point(num) + open_interval = OpenInterval(center_point, spacial_width) + color = color or "yellow" + interval_line = Line( + center_point+spacial_width*LEFT/2, + center_point+spacial_width*RIGHT/2 + ) interval_line.do_in_place(interval_line.sort_points, np.linalg.norm) - interval_line.highlight("yellow") + interval_line.highlight(color) if run_time > 0: squished_interval = deepcopy(open_interval).stretch_to_fit_width(0) self.play( @@ -647,55 +699,123 @@ class PowersOfTwelfthRoot(Scene): self.play(ShimmerIn(CompoundMobject(*mob_list), run_time = 3.0)) -class AllValuesBetween1And2(Scene): +class SupposeThereIsASavant(Scene): def construct(self): + words = "Suppose there is a musical savant " + \ + "who find pleasure in all pairs of " + \ + "notes whose frequencies have a rational ratio" + words = text_mobject(words.split(" ")).split() + words[4].highlight() + words[5].highlight() + for word in words: + self.add(word) + self.dither(0.2) + +class AllValuesBetween1And2(NumberLineScene): + def construct(self): + NumberLineScene.construct(self) irrational = 1.2020569031595942 cont_frac = [1, 4, 1, 18, 1, 1, 1, 4, 1, 9, 9, 2, 1, 1, 1, 2] - number_line = NumberLine(interval_size = 1).add_numbers() - one, two = 2*RIGHT, 4*RIGHT + one, two, irr = map( + self.number_line.number_to_point, + [1, 2, irrational] + ) top_arrow = Arrow(one+UP, one) - bot_arrow = Arrow(2*irrational*RIGHT+DOWN, 2*irrational*RIGHT) + bot_arrow = Arrow(irr+2*DOWN, irr) r = tex_mobject("r").next_to(top_arrow, UP) - irr_mob = tex_mobject(str(irrational)).next_to(bot_arrow, DOWN) + irr_mob = tex_mobject(str(irrational)+"\\dots").next_to(bot_arrow, DOWN) approximations = [ continued_fraction(cont_frac[:k]) - for k in range(1, len(cont_frac)) + for k in range(1, len(cont_frac))[:8] ] - approx_mobs = [fraction_mobject(a) for a in approximations] - self.add(number_line) kwargs = { "run_time" : 3.0, "alpha_func" : there_and_back } self.play(*[ - ApplyMethod(mob.shift, 2*RIGHT, **kwargs) + ApplyMethod(mob.shift, RIGHT, **kwargs) for mob in r, top_arrow ]) + self.dither() self.remove(r, top_arrow) self.play( ShimmerIn(irr_mob), ShowCreation(bot_arrow) ) + self.dither() + self.add(irr_mob, bot_arrow) + frac_mob = Point(top_arrow.get_top()) + max_num_zooms = 4 + num_zooms = 0 for approx in approximations: - point = 2*float(approx)*RIGHT + point = self.number_line.number_to_point(approx) new_arrow = Arrow(point+UP, point) mob = fraction_mobject(approx).next_to(new_arrow, UP) self.play( Transform(top_arrow, new_arrow), Transform(frac_mob, mob), - run_time = 0.2 + run_time = 0.5 ) self.dither(0.5) + points = map(self.number_line.number_to_point, [approx, irrational]) + distance = np.linalg.norm(points[1]-points[0]) + if distance < 0.3*SPACE_WIDTH and num_zooms < max_num_zooms: + num_zooms += 1 + new_distance = 0.75*SPACE_WIDTH + self.zoom_in_on(irrational, new_distance/distance) + for mob in irr_mob, bot_arrow: + mob.shift(mob.get_center()[0]*LEFT) + self.dither(0.5) - -class SampleIntervalScene(IntervalScene): +class CoveringSetsWithOpenIntervals(IntervalScene): def construct(self): IntervalScene.construct(self) - self.cover_fractions() + dots = CompoundMobject(*[ + Dot().shift(self.number_line.number_to_point(num)+UP) + for num in set([0.2, 0.25, 0.45, 0.6, 0.65]) + ]) + theorems = [ + text_mobject(words).shift(UP) + for words in [ + "Heine-Borel Theorem", + "Lebesgue's Number Lemma", + "Vitali Covering Lemma", + "Besicovitch Covering Theorem", + "$\\vdots$" + ] + ] + + self.add(dots) + self.play(DelayByOrder(ApplyMethod(dots.shift, DOWN, run_time = 2))) + self.dither() + for center in 0.225, 0.475, 0.625: + self.add_open_interval(center, 0.1, run_time = 1.0) + self.dither() + for x in range(2*len(theorems)): + self.play(*[ + ApplyMethod(th.shift, UP, alpha_func = None) + for th in theorems[:x+1] + ]) + +class DefineOpenInterval(IntervalScene): + def construct(self): + IntervalScene.construct(self) + open_interval, line = self.add_open_interval(0.5, 0.05, run_time = 1.0) + left, right = open_interval.get_left(), open_interval.get_right() + a, less_than1, x, less_than2, b = \ + tex_mobject(["a", "<", "x", "<", "b"]).shift(UP).split() + left_arrow = Arrow(a, left) + right_arrow = Arrow(b, right) + + self.play(*[ShimmerIn(mob) for mob in a, less_than1, x]) + self.play(ShowCreation(left_arrow)) + self.dither() + self.play(*[ShimmerIn(mob) for mob in less_than2, b]) + self.play(ShowCreation(right_arrow)) self.dither() @@ -707,6 +827,446 @@ class ShowAllFractions(IntervalScene): remove_as_you_go = False, pause_time = 0.3 ) + self.dither() + self.play(*[ + CounterclockwiseTransform(mob, Point(mob.get_center())) + for mob in self.mobjects + if isinstance(mob, ImageMobject) + ]) + self.dither() + +class NaiveFractionCover(IntervalScene): + def construct(self): + IntervalScene.construct(self) + self.add_fraction_ticks(100) + self.add_fraction_ticks(run_time = 5.0) + last_interval = None + centers = np.arange(0, 1.1, .1) + random.shuffle(centers) + for num, count in zip(centers, it.count()): + if count == 0: + kwargs = { + "run_time" : 1.0, + "alpha_func" : rush_into + } + elif count == 10: + kwargs = { + "run_time" : 1.0, + "alpha_func" : rush_from + } + else: + kwargs = { + "run_time" : 0.25, + "alpha_func" : None + } + open_interval, line = self.add_open_interval(num, 0.1) + open_interval.shift(UP) + self.remove(line) + anims = [ApplyMethod(open_interval.shift, DOWN, **kwargs)] + last_interval = open_interval + self.play(*anims) + self.dither() + +class CoverFractionsWithWholeInterval(IntervalScene): + def construct(self): + IntervalScene.construct(self) + self.add_fraction_ticks() + self.dither() + self.add_open_interval(0.5, 1, color = "red", run_time = 2.0) + self.dither() + +class SumOfIntervalsMustBeLessThan1(IntervalScene): + def construct(self): + IntervalScene.construct(self) + self.add_fraction_ticks() + anims = [] + last_plus = Point((SPACE_WIDTH-0.5)*LEFT+2*UP) + for num in np.arange(0, 1.1, .1): + open_interval, line = self.add_open_interval(num, 0.1) + self.remove(line) + int_copy = deepcopy(open_interval) + int_copy.scale(0.6).next_to(last_plus, buff = 0.1) + last_plus = tex_mobject("+").scale(0.3) + last_plus.next_to(int_copy, buff = 0.1) + anims.append(Transform(open_interval, int_copy)) + if num < 1.0: + anims.append(FadeIn(last_plus)) + less_than1 = tex_mobject("<1").scale(0.5) + less_than1.next_to(int_copy) + dots = tex_mobject("\\dots").replace(int_copy) + words = text_mobject("Use infinitely many intervals") + words.shift(UP) + + self.dither() + self.play(*anims) + self.play(ShimmerIn(less_than1)) + self.dither() + self.play(Transform(anims[-1].mobject, dots)) + self.play(ShimmerIn(words)) + self.dither() + +class RationalsAreDense(IntervalScene): + def construct(self): + IntervalScene.construct(self) + words = text_mobject(["Rationals are", "\\emph{dense}", "in the reals"]) + words.shift(2*UP) + words = words.split() + words[1].highlight() + + self.add(words[0]) + self.play(ShimmerIn(words[1])) + self.add(words[2]) + self.dither() + ticks = self.add_fraction_ticks(run_time = 5.0) + self.dither() + for center, width in [(0.5, 0.1), (0.2, 0.05), (0.7, 0.01)]: + oi, line = self.add_open_interval(center, width, run_time = 1) + self.remove(line) + self.dither() + for compound in oi, ticks: + self.remove(compound) + self.add(*compound.split()) + self.zoom_in_on(0.7, 100) + self.play(ShowCreation(ticks, run_time = 2.0)) + self.dither() + + + +class HowCanYouNotCoverEntireInterval(IntervalScene): + def construct(self): + IntervalScene.construct(self) + words = text_mobject(""" + In case you are wondering, it is indeed true + that if you cover all real numbers between 0 + and 1 with a set of open intervals, the sum + of the lengths of those intervals must be at + least 1. While intuitive, this is not actually + as obvious as it might seem, just try to prove + it for yourself! + """) + words.scale(0.5).to_corner(UP+RIGHT) + ticks = self.add_fraction_ticks() + left = self.number_line.number_to_point(0) + right = self.number_line.number_to_point(1) + full_line = Line(left, right) + full_line.highlight("red") + # dot = Dot().shift(left).highlight("red") + # point = Point(left).highlight("red") + intervals = [] + for num in np.arange(0, 1.1, .1): + open_interval, line = self.add_open_interval(num, 0.1) + self.remove(line) + intervals.append(open_interval) + self.dither() + self.remove(ticks) + self.play(*[ + Transform(tick, full_line) + for tick in ticks.split() + ]) + # self.play(DelayByOrder(FadeToColor(full_line, "red"))) + self.play(ShimmerIn(words)) + self.dither() + + +class StepsToSolution(IntervalScene): + def construct(self): + IntervalScene.construct(self) + self.spacing = 0.7 + steps = map(text_mobject, [ + "Enumerate all rationals in (0, 1)", + "Assign one interval to each rational", + "Choose sum of the form $\\sum_{n=1}^\\infty a_n = 1$", + "Pick any $\\epsilon$ such that $0 < \\epsilon < 1$", + "Stretch the $n$th interval to have length $\\epsilon/2^n$", + ]) + for step in steps: + step.shift(DOWN) + for step, count in zip(steps, it.count()): + self.add(step) + self.dither() + if count == 0: + self.enumerate_rationals() + elif count == 1: + self.assign_intervals_to_rationals() + elif count == 2: + self.add_terms() + elif count == 3: + self.multiply_by_epsilon() + elif count == 4: + self.stretch_intervals() + self.remove(step) + + def enumerate_rationals(self): + ticks = self.add_fraction_ticks(run_time = 2.0) + anims = [] + commas = Mobject() + denom_to_mobs = {} + for frac, count in zip(rationals(), range(1,28)): + mob, tick = self.add_fraction(frac, shrink = True) + self.dither(0.1) + self.remove(tick) + if frac.denominator not in denom_to_mobs: + denom_to_mobs[frac.denominator] = [] + denom_to_mobs[frac.denominator].append(mob) + mob_copy = deepcopy(mob).center() + mob_copy.shift((2.4-mob_copy.get_bottom()[1])*UP) + mob_copy.shift((-SPACE_WIDTH+self.spacing*count)*RIGHT) + comma = text_mobject(",").next_to(mob_copy, buff = 0.1, aligned_edge = DOWN) + anims.append(Transform(mob, mob_copy)) + commas.add(comma) + anims.append(ShimmerIn(commas)) + new_ticks = [] + for tick, count in zip(ticks.split(), it.count(1)): + tick_copy = deepcopy(tick).center().shift(1.6*UP) + tick_copy.shift((-SPACE_WIDTH+self.spacing*count)*RIGHT) + new_ticks.append(tick_copy) + new_ticks = CompoundMobject(*new_ticks) + anims.append(DelayByOrder(Transform(ticks, new_ticks))) + self.dither() + self.play(*anims) + self.dither() + for denom in range(2, 10): + for mob in denom_to_mobs[denom]: + mob.highlight("green") + self.dither() + for mob in denom_to_mobs[denom]: + mob.to_original_color() + self.ticks = ticks.split()[:20] + + def assign_intervals_to_rationals(self): + anims = [] + for tick in self.ticks: + interval = OpenInterval(tick.get_center(), self.spacing/2) + squished = deepcopy(interval).stretch_to_fit_width(0) + anims.append(Transform(squished, interval)) + self.play(*anims) + self.dither() + self.intervals = [a.mobject for a in anims] + kwargs = { + "run_time" : 2.0, + "alpha_func" : there_and_back + } + self.play(*[ + ApplyMethod(mob.scale_in_place, 0.5*random.random(), **kwargs) + for mob in self.intervals + ]) + self.dither() + + def add_terms(self): + self.ones = [] + scale_val = 0.3 + for count in range(1, 10): + frac_bottom = tex_mobject("\\over %d"%(2**count)) + frac_bottom.scale(scale_val) + frac_bottom.shift(0.5*UP + (-SPACE_WIDTH+self.spacing*count)*RIGHT) + one = tex_mobject("1").scale(scale_val) + one.next_to(frac_bottom, UP, buff = 0.1) + self.ones.append(one) + plus = tex_mobject("+").scale(scale_val) + plus.next_to(frac_bottom, buff = self.spacing/4) + self.add(frac_bottom, one, plus) + self.dither(0.2) + dots = tex_mobject("\\dots").scale(scale_val).next_to(plus) + arrow = Arrow(ORIGIN, RIGHT).next_to(dots) + one = tex_mobject("1").next_to(arrow) + self.ones.append(one) + self.play(*[ShowCreation(mob) for mob in dots, arrow, one]) + self.dither() + + def multiply_by_epsilon(self): + self.play(*[ + CounterclockwiseTransform( + one, + tex_mobject("\\epsilon").replace(one) + ) + for one in self.ones + ]) + self.dither() + + def stretch_intervals(self): + for interval, count in zip(self.intervals, it.count(1)): + self.play( + ApplyMethod(interval.scale_in_place, 1.0/(count**2)), + run_time = 1.0/count + ) + self.dither() + + +class OurSumCanBeArbitrarilySmall(Scene): + def construct(self): + step_size = 0.01 + epsilon = tex_mobject("\\epsilon") + equals = tex_mobject("=").next_to(epsilon) + self.add(epsilon, equals) + for num in np.arange(1, 0, -step_size): + parts = map(tex_mobject, str(num)) + parts[0].next_to(equals) + for i in range(1, len(parts)): + parts[i].next_to(parts[i-1], buff = 0.1, aligned_edge = DOWN) + self.add(*parts) + self.dither(0.05) + self.remove(*parts) + self.dither() + self.clear() + words = text_mobject([ + "Not only can the sum be $< 1$,\\\\", + "it can be \\emph{arbitrarily small} !" + ]).split() + self.add(words[0]) + self.dither() + self.play(ShimmerIn(words[1].highlight())) + self.dither() + + +class StillFeelsCounterintuitive(IntervalScene): + def construct(self): + IntervalScene.construct(self) + ticks = self.add_fraction_ticks(run_time = 1.0) + epsilon, equals, num = map(tex_mobject, ["\\epsilon", "=", "0.3"]) + epsilon.shift(2*UP) + equals.next_to(epsilon) + num.next_to(equals) + self.add(epsilon, equals, num) + self.cover_fractions() + self.remove(ticks) + self.add(*ticks.split()) + self.zoom_in_on(np.sqrt(2)/2, 100) + self.play(ShowCreation(ticks)) + self.dither() + + +class ZoomInOnSqrt2Over2(IntervalScene): + def construct(self): + IntervalScene.construct(self) + epsilon, equals, num = map(tex_mobject, ["\\epsilon", "=", "0.3"]) + equals.next_to(epsilon) + num.next_to(equals) + self.add(CompoundMobject(epsilon, equals, num).center().shift(2*UP)) + intervals, lines = self.cover_fractions() + self.remove(*lines) + irr = tex_mobject("\\frac{\\sqrt{2}}{2}") + point = self.number_line.number_to_point(np.sqrt(2)/2) + arrow = Arrow(point+UP, point) + irr.next_to(arrow, UP) + self.play(ShimmerIn(irr), ShowCreation(arrow)) + for count in range(4): + self.remove(*intervals) + self.remove(*lines) + self.zoom_in_on(np.sqrt(2)/2, 20) + for mob in irr, arrow: + mob.shift(mob.get_center()[0]*LEFT) + intervals, lines = self.cover_fractions() + +class NotCoveredMeansCacophonous(Scene): + def construct(self): + statement1 = text_mobject("$\\frac{\\sqrt{2}}{2}$ is not covered") + implies = tex_mobject("\\Rightarrow") + statement2 = text_mobject("Rationals which are close to $\\frac{\\sqrt{2}}{2}$ must have large denominators") + statement1.to_edge(LEFT) + implies.next_to(statement1) + statement2.next_to(implies) + implies.sort_points() + + self.add(statement1) + self.dither() + self.play(ShowCreation(implies)) + self.play(ShimmerIn(statement2)) + self.dither() + +class ShiftSetupByOne(IntervalScene): + def construct(self): + IntervalScene.construct(self) + new_interval = UnitInterval( + number_at_center = 1.5, + leftmost_tick = 1, + numbers_with_elongated_ticks = [1, 2], + ) + new_interval.add_numbers(1, 2) + side_shift_val = self.number_line.number_to_point(1) + side_shift_val -= new_interval.number_to_point(1) + new_interval.shift(side_shift_val) + self.add(new_interval) + self.number_line.add_numbers(0) + self.remove(*self.number_mobs) + epsilon_mob = tex_mobject("\\epsilon = 0.01").to_edge(UP) + self.add(epsilon_mob) + fraction_ticks = self.add_fraction_ticks() + self.remove(fraction_ticks) + all_intervals, all_lines = self.cover_fractions( + epsilon = 0.01, + num_fractions = 150, + run_time_per_interval = 0, + ) + self.remove(*all_intervals+all_lines) + + intervals, lines = self.cover_fractions(epsilon = 0.01) + self.dither() + mobs_shifts = [ + (intervals+lines, UP), + ([self.number_line, new_interval], side_shift_val*LEFT), + (intervals+lines, DOWN) + ] + for mobs, shift_val in mobs_shifts: + self.play(*[ + ApplyMethod(mob.shift, shift_val) + for mob in mobs + ]) + self.number_line = new_interval + self.dither() + words = text_mobject( + "Almost all the covered numbers are harmonious!", + size = "\\small" + ).shift(2*UP) + self.play(ShimmerIn(words)) + self.dither() + for num in [7, 5]: + point = self.number_line.number_to_point(2**(num/12.)) + arrow = Arrow(point+DOWN, point) + mob = tex_mobject( + "2^{\\left(\\frac{%d}{12}\\right)}"%num + ).next_to(arrow, DOWN) + self.play(ShimmerIn(mob), ShowCreation(arrow)) + self.dither() + self.remove(mob, arrow) + self.remove(words) + words = text_mobject( + "Cacophonous covered numbers:", + size = "\\small" + ) + words.shift(2*UP) + answer1 = text_mobject("Complicated rationals,", size = "\\small") + answer2 = text_mobject( + "real numbers \\emph{very very very} close to them", + size = "\\small" + ) + compound = CompoundMobject(answer1, answer2.next_to(answer1)) + compound.next_to(words, DOWN) + answer1, answer2 = compound.split() + + self.add(words) + self.dither() + self.remove(*intervals+lines) + self.add(answer1) + self.play(ShowCreation(fraction_ticks, run_time = 5.0)) + self.add(answer2) + self.dither() + self.remove(words, answer1, answer2) + words = text_mobject([ + "To a", "savant,", "harmonious numbers could be ", + "\\emph{precisely}", "those 1\\% covered by the intervals" + ]).shift(2*UP) + words = words.split() + words[1].highlight() + words[3].highlight() + self.add(*words) + for interval, frac in zip(all_intervals, rationals()): + interval.scale_in_place(2.0/frac.denominator) + self.add(interval) + self.dither(0.1) + self.dither() + + + if __name__ == "__main__":