from manim_imports_ext import * import scipy.stats CASE_DATA = [ 9, 15, 30, 40, 56, 66, 84, 102, 131, 159, 173, 186, 190, 221, 248, 278, 330, 354, 382, 461, 481, 526, 587, 608, 697, 781, 896, 999, 1124, 1212, 1385, 1715, 2055, 2429, 2764, 3323, 4288, 5364, 6780, 8555, 10288, 12742, 14901, 17865, 21395, # 25404, # 29256, # 33627, # 38170, # 45421, # 53873, ] SICKLY_GREEN = "#9BBD37" class IntroducePlot(Scene): def construct(self): axes = self.get_axes() self.add(axes) # Dots dots = VGroup() for day, nc in zip(it.count(1), CASE_DATA): dot = Dot() dot.set_height(0.075) dot.x = day dot.y = nc dot.axes = axes dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y))) dots.add(dot) dots.set_color(YELLOW) # Rescale y axis origin = axes.c2p(0, 0) axes.y_axis.tick_marks.save_state() for tick in axes.y_axis.tick_marks: tick.match_width(axes.y_axis.tick_marks[0]) axes.y_axis.add( axes.h_lines, axes.small_h_lines, axes.tiny_h_lines, axes.tiny_ticks, ) axes.y_axis.stretch(25, 1, about_point=origin) dots.update() self.add(axes.small_y_labels) self.add(axes.tiny_y_labels) # Add title title = self.get_title(axes) self.add(title) # Introduce the data day = 10 self.add(*dots[:day + 1]) dot = Dot() dot.match_style(dots[day]) dot.replace(dots[day]) count = Integer(CASE_DATA[day]) count.add_updater(lambda m: m.next_to(dot, UP)) count.add_updater(lambda m: m.set_stroke(BLACK, 5, background=True)) v_line = Line(DOWN, UP) v_line.set_stroke(YELLOW, 1) v_line.add_updater( lambda m: m.put_start_and_end_on( axes.c2p( axes.x_axis.p2n(dot.get_center()), 0, ), dot.get_bottom(), ) ) self.add(dot) self.add(count) self.add(v_line) for new_day in range(day + 1, len(dots)): new_dot = dots[new_day] new_dot.update() line = Line(dot.get_center(), new_dot.get_center()) line.set_stroke(PINK, 3) self.add(line, dot) self.play( dot.move_to, new_dot.get_center(), dot.set_color, RED, ChangeDecimalToValue(count, CASE_DATA[new_day]), ShowCreation(line), ) line.rotate(PI) self.play( dot.set_color, YELLOW, Uncreate(line), run_time=0.5 ) self.add(dots[new_day]) day = new_day if day == 27: self.add( axes.y_axis, axes.tiny_y_labels, axes.tiny_h_lines, axes.tiny_ticks, title ) self.play( axes.y_axis.stretch, 0.2, 1, {"about_point": origin}, VFadeOut(axes.tiny_y_labels), VFadeOut(axes.tiny_h_lines), VFadeOut(axes.tiny_ticks), MaintainPositionRelativeTo(dot, dots[new_day]), run_time=2, ) self.add(axes, title, *dots[:new_day]) if day == 36: self.add(axes.y_axis, axes.small_y_labels, axes.small_h_lines, title) self.play( axes.y_axis.stretch, 0.2, 1, {"about_point": origin}, VFadeOut(axes.small_y_labels), VFadeOut(axes.small_h_lines), MaintainPositionRelativeTo(dot, dots[new_day]), run_time=2, ) self.add(axes, title, *dots[:new_day]) count.add_background_rectangle() count.background_rectangle.stretch(1.1, 0) self.add(count) # Show multiplications last_label = VectorizedPoint(dots[25].get_center()) last_line = VMobject() for d1, d2 in zip(dots[25:], dots[26:]): line = Line( d1.get_top(), d2.get_corner(UL), path_arc=-90 * DEGREES, ) line.set_stroke(PINK, 2) label = VGroup( TexMobject("\\times"), DecimalNumber( axes.y_axis.p2n(d2.get_center()) / axes.y_axis.p2n(d1.get_center()), ) ) label.arrange(RIGHT, buff=SMALL_BUFF) label.set_height(0.25) label.next_to(line.point_from_proportion(0.5), UL, SMALL_BUFF) label.match_color(line) label.add_background_rectangle() label.save_state() label.move_to(last_label) label.set_opacity(0) self.play( ShowCreation(line), Restore(label), last_label.move_to, label.saved_state, VFadeOut(last_label), FadeOut(last_line), ) last_line = line last_label = label self.wait() self.play( FadeOut(last_label), FadeOut(last_line), ) # def get_title(self, axes): title = TextMobject( "Recorded COVID-19 cases\\\\outside mainland China", tex_to_color_map={"COVID-19": RED} ) title.next_to(axes.c2p(0, 1e3), RIGHT, LARGE_BUFF) title.to_edge(UP) title.add_background_rectangle() return title def get_axes(self, width=12, height=6): n_cases = len(CASE_DATA) axes = Axes( x_range=(0, n_cases), y_range=(0, 25000, 1000), width=width, height=height, ) axes.center() axes.to_edge(DOWN, buff=LARGE_BUFF) # Add dates text_pos_pairs = [ ("Mar 6", 0), ("Feb 23", -12), ("Feb 12", -23), ("Feb 1", -34), ("Jan 22", -44), ] labels = VGroup() extra_ticks = VGroup() for text, pos in text_pos_pairs: label = TextMobject(text) label.set_height(0.2) label.rotate(45 * DEGREES) axis_point = axes.c2p(n_cases + pos, 0) label.move_to(axis_point, UR) label.shift(MED_SMALL_BUFF * DOWN) label.shift(SMALL_BUFF * RIGHT) labels.add(label) tick = Line(UP, DOWN) tick.set_stroke(GREEN, 3) tick.set_height(0.25) tick.move_to(axis_point) extra_ticks.add(tick) axes.x_labels = labels axes.extra_x_ticks = extra_ticks axes.add(labels, extra_ticks) # Adjust y ticks axes.y_axis.ticks.stretch(0.5, 0) axes.y_axis.ticks[0::5].stretch(2, 0) # Add y_axis_labels def get_y_labels(axes, y_values): labels = VGroup() for y in y_values: try: label = TextMobject(f"{y}k") label.set_height(0.25) tick = axes.y_axis.ticks[y] always(label.next_to, tick, LEFT, SMALL_BUFF) labels.add(label) except IndexError: pass return labels main_labels = get_y_labels(axes, range(5, 30, 5)) axes.y_labels = main_labels axes.add(main_labels) axes.small_y_labels = get_y_labels(axes, range(1, 6)) tiny_labels = VGroup() tiny_ticks = VGroup() for y in range(200, 1000, 200): tick = axes.y_axis.ticks[0].copy() point = axes.c2p(0, y) tick.move_to(point) label = Integer(y) label.set_height(0.25) always(label.next_to, tick, LEFT, SMALL_BUFF) tiny_labels.add(label) tiny_ticks.add(tick) axes.tiny_y_labels = tiny_labels axes.tiny_ticks = tiny_ticks # Horizontal lines axes.h_lines = VGroup() axes.small_h_lines = VGroup() axes.tiny_h_lines = VGroup() group_range_pairs = [ (axes.h_lines, 5e3 * np.arange(1, 6)), (axes.small_h_lines, 1e3 * np.arange(1, 5)), (axes.tiny_h_lines, 200 * np.arange(1, 5)), ] for group, _range in group_range_pairs: for y in _range: group.add( Line( axes.c2p(0, y), axes.c2p(n_cases, y), ) ) group.set_stroke(WHITE, 1, opacity=0.5) return axes class Thumbnail(IntroducePlot): def construct(self): axes = self.get_axes() self.add(axes) dots = VGroup() data = CASE_DATA data.append(25398) for day, nc in zip(it.count(1), CASE_DATA): dot = Dot() dot.set_height(0.2) dot.x = day dot.y = nc dot.axes = axes dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y))) dots.add(dot) dots.set_color(YELLOW) dots.set_submobject_colors_by_gradient(BLUE, GREEN, RED) self.add(dots) title = TextMobject("COVID-19") title.set_height(1) title.set_color(RED) title.to_edge(UP, buff=LARGE_BUFF) subtitle = TextMobject("and exponential growth") subtitle.match_width(title) subtitle.next_to(title, DOWN) # self.add(title) # self.add(subtitle) title = TextMobject("The early warning\\\\of ", "COVID-19\\\\") title.set_color_by_tex("COVID", RED) title.set_height(2.5) title.to_edge(UP, buff=LARGE_BUFF) self.add(title) # self.remove(words) # words = TextMobject("Exponential growth") # words.move_to(ORIGIN, DL) # words.apply_function( # lambda p: [ # p[0], p[1] + np.exp(0.2 * p[0]), p[2] # ] # ) # self.add(words) self.embed() class IntroQuestion(Scene): def construct(self): questions = VGroup( TextMobject("What is exponential growth?"), TextMobject("Where does it come from?"), TextMobject("What does it imply?"), TextMobject("When does it stop?"), ) questions.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) for question in questions: self.play(FadeIn(question, RIGHT)) self.wait() self.play(LaggedStartMap( FadeOutAndShift, questions, lambda m: (m, DOWN), )) class ViralSpreadModel(Scene): CONFIG = { "num_neighbors": 5, "infection_probability": 0.3, "random_seed": 1, } def construct(self): # Init population randys = self.get_randys() self.add(*randys) # Show the sicko self.show_patient0(randys) # Repeatedly spread for x in range(20): self.spread_infection(randys) def get_randys(self): randys = VGroup(*[ Randolph() for x in range(150) ]) for randy in randys: randy.set_height(0.5) randys.arrange_in_grid(10, 15, buff=0.5) randys.set_height(FRAME_HEIGHT - 1) for i in range(0, 10, 2): randys[i * 15:(i + 1) * 15].shift(0.25 * RIGHT) for randy in randys: randy.shift(0.2 * random.random() * RIGHT) randy.shift(0.2 * random.random() * UP) randy.infected = False randys.center() return randys def show_patient0(self, randys): patient0 = random.choice(randys) patient0.infected = True circle = Circle() circle.set_stroke(SICKLY_GREEN) circle.replace(patient0) circle.scale(1.5) self.play( patient0.change, "sick", patient0.set_color, SICKLY_GREEN, ShowCreationThenFadeOut(circle), ) def spread_infection(self, randys): E = self.num_neighbors inf_p = self.infection_probability cough_anims = [] new_infection_anims = [] for randy in randys: if randy.infected: cough_anims.append(Flash( randy, color=SICKLY_GREEN, num_lines=16, line_stroke_width=1, flash_radius=0.5, line_length=0.1, )) random.shuffle(cough_anims) self.play(LaggedStart( *cough_anims, run_time=1, lag_ratio=1 / len(cough_anims), )) newly_infected = [] for randy in randys: if randy.infected: distances = [ get_norm(r2.get_center() - randy.get_center()) for r2 in randys ] for i in np.argsort(distances)[1:E + 1]: r2 = randys[i] if random.random() < inf_p and not r2.infected and r2 not in newly_infected: newly_infected.append(r2) r2.generate_target() r2.target.change("sick") r2.target.set_color(SICKLY_GREEN) new_infection_anims.append(MoveToTarget(r2)) random.shuffle(new_infection_anims) self.play(LaggedStart(*new_infection_anims, run_time=1)) for randy in newly_infected: randy.infected = True class GrowthEquation(Scene): def construct(self): # Add labels N_label = TextMobject("$N_d$", " = Number of cases on a given day", ) E_label = TextMobject("$E$", " = Average number of people someone infected is exposed to each day") p_label = TextMobject("$p$", " = Probability of each exposure becoming an infection") N_label[0].set_color(YELLOW) E_label[0].set_color(BLUE) p_label[0].set_color(TEAL) labels = VGroup( N_label, E_label, p_label ) labels.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) labels.set_width(FRAME_WIDTH - 1) labels.to_edge(UP) for label in labels: self.play(FadeInFromDown(label)) self.wait() delta_N = TexMobject("\\Delta", "N_d") delta_N.set_color(YELLOW) eq = TexMobject("=") eq.center() delta_N.next_to(eq, LEFT) delta_N_brace = Brace(delta_N, DOWN) delta_N_text = delta_N_brace.get_text("Change over a day") nep = TexMobject("E", "\\cdot", "p", "\\cdot", "N_d") nep[4].match_color(N_label[0]) nep[0].match_color(E_label[0]) nep[2].match_color(p_label[0]) nep.next_to(eq, RIGHT) self.play(FadeIn(delta_N), FadeIn(eq)) self.play( GrowFromCenter(delta_N_brace), FadeIn(delta_N_text, 0.5 * UP), ) self.wait() self.play(LaggedStart( TransformFromCopy(N_label[0], nep[4]), TransformFromCopy(E_label[0], nep[0]), TransformFromCopy(p_label[0], nep[2]), FadeIn(nep[1]), FadeIn(nep[3]), lag_ratio=0.2, run_time=2, )) self.wait() self.play(ShowCreationThenFadeAround( nep[-1], surrounding_rectangle_config={"color": RED}, )) # Recursive equation lhs = TexMobject("N_{d + 1}", "=") lhs[0].set_color(YELLOW) lhs.move_to(eq, RIGHT) lhs.shift(DOWN) rhs = VGroup( nep[-1].copy(), TexMobject("+"), nep.copy(), ) rhs.arrange(RIGHT) rhs.next_to(lhs, RIGHT) self.play( FadeOut(delta_N_brace), FadeOut(delta_N_text), FadeIn(lhs, UP), ) self.play(FadeIn(rhs[:2])) self.play(TransformFromCopy(nep, rhs[2])) self.wait() alt_rhs = TexMobject( "(", "1", "+", "E", "\\cdot", "p", ")", "N_d", tex_to_color_map={ "E": BLUE, "p": TEAL, "N_d": YELLOW, } ) new_lhs = lhs.copy() new_lhs.shift(DOWN) alt_rhs.next_to(new_lhs, RIGHT) self.play(TransformFromCopy(lhs, new_lhs)) rhs.unlock_triangulation() self.play( TransformFromCopy(rhs[0], alt_rhs[7].copy()), TransformFromCopy(rhs[2][4], alt_rhs[7]), ) self.play( TransformFromCopy(rhs[1][0], alt_rhs[2]), TransformFromCopy(rhs[2][0], alt_rhs[3]), TransformFromCopy(rhs[2][1], alt_rhs[4]), TransformFromCopy(rhs[2][2], alt_rhs[5]), TransformFromCopy(rhs[2][3], alt_rhs[6]), FadeIn(alt_rhs[0]), FadeIn(alt_rhs[1]), ) self.wait() # Comment on factor brace = Brace(alt_rhs[:7], DOWN) text = TextMobject("For example, ", "1.15") text.next_to(brace, DOWN) self.play( GrowFromCenter(brace), FadeIn(text, 0.5 * UP) ) self.wait() # Show exponential eq_group = VGroup( delta_N, eq, nep, lhs, rhs, new_lhs, alt_rhs, brace, text, ) self.clear() self.add(labels, eq_group) self.play(ShowCreationThenFadeAround( VGroup(delta_N, eq, nep), surrounding_rectangle_config={"color": RED}, )) self.play(ShowCreationThenFadeAround( VGroup(new_lhs, alt_rhs, brace, text), surrounding_rectangle_config={"color": RED}, )) self.wait() self.play(eq_group.to_edge, LEFT, LARGE_BUFF) exp_eq = TexMobject( "N_d = (1 + E \\cdot p)^{d} \\cdot N_0", tex_to_color_map={ "N_d": YELLOW, "E": BLUE, "p": TEAL, "{d}": YELLOW, "N_0": YELLOW, } ) exp_eq.next_to(alt_rhs, RIGHT, buff=3) arrow = Arrow(alt_rhs.get_right(), exp_eq.get_left()) self.play( GrowArrow(arrow), FadeIn(exp_eq, 2 * LEFT) ) self.wait() # Discuss factor in front of N ep = nep[:3] ep_rect = SurroundingRectangle(ep) ep_rect.set_stroke(RED, 2) ep_label = TextMobject("This factor will decrease") ep_label.next_to(ep_rect, UP, aligned_edge=LEFT) ep_label.set_color(RED) self.play( ShowCreation(ep_rect), FadeIn(ep_label, lag_ratio=0.1), ) self.wait() self.play( FadeOut(ep_rect), FadeOut(ep_label), ) # Add carrying capacity factor to p p_factors = TexMobject( "\\left(1 - {N_d \\over \\text{pop. size}} \\right)", tex_to_color_map={"N_d": YELLOW}, ) p_factors.next_to(nep, RIGHT, buff=3) p_factors_rect = SurroundingRectangle(p_factors) p_factors_rect.set_stroke(TEAL, 2) p_arrow = Arrow( p_factors_rect.get_corner(UL), nep[2].get_top(), path_arc=75 * DEGREES, color=TEAL, ) self.play( ShowCreation(p_factors_rect), ShowCreation(p_arrow) ) self.wait() self.play(Write(p_factors)) self.wait() self.play( FadeOut(p_factors), FadeOut(p_arrow), FadeOut(p_factors_rect), ) # Ask about ep shrinking ep_question = TextMobject("What makes this shrink?") ep_question.set_color(RED) ep_question.next_to(ep_rect, UP, aligned_edge=LEFT) E_line = Underline(E_label) E_line.set_color(BLUE) p_line = Underline(p_label) p_line.set_color(TEAL) self.play( ShowCreation(ep_rect), FadeIn(ep_question, LEFT) ) self.wait() for line in E_line, p_line: self.play(ShowCreation(line)) self.wait() self.play(FadeOut(line)) self.wait() # Show alternate projections ep_value = DecimalNumber(0.15) ep_value.next_to(ep_rect, UP) self.play( FadeOut(ep_question), FadeIn(ep_value), FadeOut(text[0]), text[1].next_to, brace, DOWN, ) eq1 = TexMobject("(", "1.15", ")", "^{61}", "\\cdot", "21{,}000", "=") eq2 = TexMobject("(", "1.05", ")", "^{61}", "\\cdot", "21{,}000", "=") eq1_rhs = Integer((1.15**61) * (21000)) eq2_rhs = Integer((1.05**61) * (21000)) for eq, rhs in (eq1, eq1_rhs), (eq2, eq2_rhs): eq[1].set_color(RED) eq.move_to(nep) eq.to_edge(RIGHT, buff=3) rhs.next_to(eq, RIGHT) rhs.align_to(eq[-2], UP) self.play(FadeIn(eq1)) for tex in ["21{,}000", "61"]: self.play(ShowCreationThenFadeOut( Underline( eq1.get_part_by_tex(tex), stroke_color=YELLOW, stroke_width=2, buff=SMALL_BUFF, ), run_time=2, )) value = eq1_rhs.get_value() eq1_rhs.set_value(0) self.play(ChangeDecimalToValue(eq1_rhs, value)) self.wait() eq1.add(eq1_rhs) self.play( eq1.shift, DOWN, FadeIn(eq2), ) new_text = TextMobject("1.05") new_text.move_to(text[1]) self.play( ChangeDecimalToValue(ep_value, 0.05), FadeOut(text[1]), FadeIn(new_text), ) self.wait() eq2_rhs.align_to(eq1_rhs, RIGHT) value = eq2_rhs.get_value() eq2_rhs.set_value(0) self.play(ChangeDecimalToValue(eq2_rhs, value)) self.wait() # Pi creature quote morty = Mortimer() morty.set_height(1) morty.next_to(eq2_rhs, UP) bubble = SpeechBubble( direction=RIGHT, height=2.5, width=5, ) bubble.next_to(morty, UL, buff=0) bubble.write("The only thing to fear\\\\is the lack of fear itself.") self.add(morty) self.add(bubble) self.add(bubble.content) self.play( labels.set_opacity, 0.5, VFadeIn(morty), morty.change, "speaking", FadeIn(bubble), Write(bubble.content), ) self.play(Blink(morty)) self.wait() class RescaleToLogarithmic(IntroducePlot): def construct(self): # Setup axes axes = self.get_axes(width=10) title = self.get_title(axes) dots = VGroup() for day, nc in zip(it.count(1), CASE_DATA): dot = Dot() dot.set_height(0.075) dot.move_to(axes.c2p(day, nc)) dots.add(dot) dots.set_color(YELLOW) self.add(axes, axes.h_lines, dots, title) # Create logarithmic y axis log_y_axis = NumberLine( x_min=0, x_max=9, ) log_y_axis.rotate(90 * DEGREES) log_y_axis.move_to(axes.c2p(0, 0), DOWN) labels_text = [ "10", "100", "1k", "10k", "100k", "1M", "10M", "100M", "1B", ] log_y_labels = VGroup() for text, tick in zip(labels_text, log_y_axis.tick_marks[1:]): label = TextMobject(text) label.set_height(0.25) always(label.next_to, tick, LEFT, SMALL_BUFF) log_y_labels.add(label) # Animate the rescaling to a logarithmic plot logarithm_title = TextMobject("(Logarithmic scale)") logarithm_title.set_color(TEAL) logarithm_title.next_to(title, DOWN) logarithm_title.add_background_rectangle() def scale_logarithmically(p): result = np.array(p) y = axes.y_axis.p2n(p) result[1] = log_y_axis.n2p(np.log10(y))[1] return result log_h_lines = VGroup() for exponent in range(0, 9): for mult in range(2, 12, 2): y = mult * 10**exponent line = Line( axes.c2p(0, y), axes.c2p(axes.x_max, y), ) log_h_lines.add(line) log_h_lines.set_stroke(WHITE, 0.5, opacity=0.5) log_h_lines[4::5].set_stroke(WHITE, 1, opacity=1) movers = [dots, axes.y_axis.tick_marks, axes.h_lines, log_h_lines] for group in movers: group.generate_target() for mob in group.target: mob.move_to(scale_logarithmically(mob.get_center())) log_y_labels.suspend_updating() log_y_labels.save_state() for exponent, label in zip(it.count(1), log_y_labels): label.set_y(axes.y_axis.n2p(10**exponent)[1]) label.set_opacity(0) self.add(log_y_axis) log_y_axis.save_state() log_y_axis.tick_marks.set_opacity(0) log_h_lines.set_opacity(0) self.wait() self.add(log_h_lines, title, logarithm_title) self.play( MoveToTarget(dots), MoveToTarget(axes.y_axis.tick_marks), MoveToTarget(axes.h_lines), MoveToTarget(log_h_lines), VFadeOut(axes.y_labels), VFadeOut(axes.y_axis.tick_marks), VFadeOut(axes.h_lines), Restore(log_y_labels), FadeIn(logarithm_title), run_time=2, ) self.play(Restore(log_y_axis)) self.wait() # Walk up y axis brace = Brace( log_y_axis.tick_marks[1:3], RIGHT, buff=SMALL_BUFF, ) brace_label = brace.get_tex( "\\times 10", buff=SMALL_BUFF ) VGroup(brace, brace_label).set_color(TEAL) brace_label.set_stroke(BLACK, 8, background=True) self.play( GrowFromCenter(brace), FadeIn(brace_label) ) brace.add(brace_label) for i in range(2, 5): self.play( brace.next_to, log_y_axis.tick_marks[i:i + 2], {"buff": SMALL_BUFF} ) self.wait(0.5) self.play(FadeOut(brace)) self.wait() # Show order of magnitude jumps remove_anims = [] for i, j in [(7, 27), (27, 40)]: line = Line(dots[i].get_center(), dots[j].get_center()) rect = Rectangle() rect.set_fill(TEAL, 0.5) rect.set_stroke(width=0) rect.replace(line, stretch=True) label = TextMobject(f"{j - i} days") label.next_to(rect, UP, SMALL_BUFF) label.set_color(TEAL) rect.save_state() rect.stretch(0, 0, about_edge=LEFT) self.play( Restore(rect), FadeIn(label, LEFT) ) self.wait() remove_anims += [ ApplyMethod( rect.stretch, 0, 0, {"about_edge": RIGHT}, remover=True, ), FadeOut(label, RIGHT), ] self.wait() # Linear regression def c2p(x, y): xp = axes.x_axis.n2p(x) yp = log_y_axis.n2p(np.log10(y)) return np.array([xp[0], yp[1], 0]) reg = scipy.stats.linregress( range(7, len(CASE_DATA)), np.log10(CASE_DATA[7:]) ) x_max = axes.x_max axes.y_axis = log_y_axis reg_line = Line( c2p(0, 10**reg.intercept), c2p(x_max, 10**(reg.intercept + reg.slope * x_max)), ) reg_line.set_stroke(TEAL, 3) self.add(reg_line, dots) dots.set_stroke(BLACK, 3, background=True) self.play( LaggedStart(*remove_anims), ShowCreation(reg_line) ) # Describe linear regression reg_label = TextMobject("Linear regression") reg_label.move_to(c2p(25, 10), DOWN) reg_arrows = VGroup() for prop in [0.4, 0.6, 0.5]: reg_arrows.add( Arrow( reg_label.get_top(), reg_line.point_from_proportion(prop), buff=SMALL_BUFF, ) ) reg_arrow = reg_arrows[0].copy() self.play( Write(reg_label, run_time=1), Transform(reg_arrow, reg_arrows[1], run_time=2), VFadeIn(reg_arrow), ) self.play(Transform(reg_arrow, reg_arrows[2])) self.wait() # Label slope slope_label = TextMobject("$\\times 10$ every $16$ days (on average)") slope_label.set_color(BLUE) slope_label.set_stroke(BLACK, 8, background=True) slope_label.rotate(reg_line.get_angle()) slope_label.move_to(reg_line.get_center()) slope_label.shift(MED_LARGE_BUFF * UP) self.play(FadeIn(slope_label, lag_ratio=0.1)) self.wait() # R^2 label R2_label = VGroup( TexMobject("R^2 = "), DecimalNumber(0, num_decimal_places=3) ) R2_label.arrange(RIGHT, aligned_edge=DOWN) R2_label.next_to(reg_label[0][-1], RIGHT, LARGE_BUFF, aligned_edge=DOWN) self.play( ChangeDecimalToValue(R2_label[1], reg.rvalue**2, run_time=2), UpdateFromAlphaFunc( R2_label, lambda m, a: m.set_opacity(a), ) ) self.wait() rect = SurroundingRectangle(R2_label, buff=0.15) rect.set_stroke(YELLOW, 3) rect.set_fill(BLACK, 0) self.add(rect, R2_label) self.play(ShowCreation(rect)) self.play( rect.set_stroke, WHITE, 2, rect.set_fill, GREY_E, 1, ) self.wait() self.play( FadeOut(rect), FadeOut(R2_label), FadeOut(reg_label), FadeOut(reg_arrow), ) # Zoom out extended_x_axis = NumberLine( x_min=axes.x_axis.x_max, x_max=axes.x_axis.x_max + 90, unit_size=get_norm( axes.x_axis.n2p(1) - axes.x_axis.n2p(0) ), numbers_with_elongated_ticks=[], ) extended_x_axis.move_to(axes.x_axis.get_right(), LEFT) self.play( self.camera.frame.scale, 2, {"about_edge": DL}, self.camera.frame.shift, 2.5 * DOWN + RIGHT, log_h_lines.stretch, 3, 0, {"about_edge": LEFT}, ShowCreation(extended_x_axis, rate_func=squish_rate_func(smooth, 0.5, 1)), run_time=3, ) self.play( reg_line.scale, 3, {"about_point": reg_line.get_start()} ) self.wait() # Show future projections target_ys = [1e6, 1e7, 1e8, 1e9] last_point = dots[-1].get_center() last_label = None last_rect = None date_labels_text = [ "Apr 5", "Apr 22", "May 9", "May 26", ] for target_y, date_label_text in zip(target_ys, date_labels_text): log_y = np.log10(target_y) x = (log_y - reg.intercept) / reg.slope line = Line(last_point, c2p(x, target_y)) rect = Rectangle().replace(line, stretch=True) rect.set_stroke(width=0) rect.set_fill(TEAL, 0.5) label = TextMobject(f"{int(x) - axes.x_max} days") label.scale(1.5) label.next_to(rect, UP, SMALL_BUFF) date_label = TextMobject(date_label_text) date_label.set_height(0.25) date_label.rotate(45 * DEGREES) axis_point = axes.c2p(int(x), 0) date_label.move_to(axis_point, UR) date_label.shift(MED_SMALL_BUFF * DOWN) date_label.shift(SMALL_BUFF * RIGHT) v_line = DashedLine( axes.c2p(x, 0), c2p(x, target_y), ) v_line.set_stroke(WHITE, 2) if target_y is target_ys[-1]: self.play(self.camera.frame.scale, 1.1, {"about_edge": LEFT}) if last_label: last_label.unlock_triangulation() self.play( ReplacementTransform(last_label, label), ReplacementTransform(last_rect, rect), ) else: rect.save_state() rect.stretch(0, 0, about_edge=LEFT) self.play(Restore(rect), FadeIn(label, LEFT)) self.wait() self.play( ShowCreation(v_line), FadeIn(date_label), ) last_label = label last_rect = rect self.wait() self.play( FadeOut(last_label, RIGHT), ApplyMethod( last_rect.stretch, 0, 0, {"about_edge": RIGHT}, remover=True ), ) # Show alternate petering out possibilities def get_dots_along_curve(curve): x_min = int(axes.x_axis.p2n(curve.get_start())) x_max = int(axes.x_axis.p2n(curve.get_end())) result = VGroup() for x in range(x_min, x_max): prop = binary_search( lambda p: axes.x_axis.p2n( curve.point_from_proportion(p), ), x, 0, 1, ) prop = prop or 0 point = curve.point_from_proportion(prop) dot = Dot(point) dot.shift(0.02 * (random.random() - 0.5)) dot.set_height(0.075) dot.set_color(RED) result.add(dot) dots.remove(dots[0]) return result def get_point_from_y(y): log_y = np.log10(y) x = (log_y - reg.intercept) / reg.slope return c2p(x, 10**log_y) p100k = get_point_from_y(1e5) p100M = get_point_from_y(1e8) curve1 = VMobject() curve1.append_points([ dots[-1].get_center(), p100k, p100k + 5 * RIGHT, ]) curve2 = VMobject() curve2.append_points([ dots[-1].get_center(), p100M, p100M + 5 * RIGHT + 0.25 * UP, ]) proj_dots1 = get_dots_along_curve(curve1) proj_dots2 = get_dots_along_curve(curve2) for proj_dots in [proj_dots1, proj_dots2]: self.play(FadeIn(proj_dots, lag_ratio=0.1)) self.wait() self.play(FadeOut(proj_dots, lag_ratio=0.1)) class LinRegNote(Scene): def construct(self): text = TextMobject("(Starting from when\\\\there were 100 cases)") text.set_stroke(BLACK, 8, background=True) self.add(text) class CompareCountries(Scene): def construct(self): # Introduce sk_flag = ImageMobject(os.path.join("flags", "kr")) au_flag = ImageMobject(os.path.join("flags", "au")) flags = Group(sk_flag, au_flag) flags.set_height(3) flags.arrange(RIGHT, buff=LARGE_BUFF) flags.next_to(ORIGIN, UP) labels = VGroup() case_numbers = [6593, 64] for flag, cn in zip(flags, case_numbers): label = VGroup(Integer(cn), TextMobject("cases")) label.arrange(RIGHT, buff=MED_SMALL_BUFF) label[1].align_to(label[0][-1], DOWN) label.scale(1.5) label.next_to(flag, DOWN, MED_LARGE_BUFF) label[0].set_value(0) labels.add(label) self.play(LaggedStartMap(FadeInFromDown, flags, lag_ratio=0.25)) self.play( ChangeDecimalToValue(labels[0][0], case_numbers[0]), ChangeDecimalToValue(labels[1][0], case_numbers[1]), UpdateFromAlphaFunc( labels, lambda m, a: m.set_opacity(a), ) ) self.wait() # Compare arrow = Arrow( labels[1][0].get_bottom(), labels[0][0].get_bottom(), path_arc=-90 * DEGREES, ) arrow_label = TextMobject("100x better") arrow_label.set_color(YELLOW) arrow_label.next_to(arrow, DOWN) alt_arrow_label = TextMobject("1 month behind") alt_arrow_label.set_color(RED) alt_arrow_label.next_to(arrow, DOWN) self.play(ShowCreation(arrow)) self.play(FadeIn(arrow_label, 0.5 * UP)) self.wait(2) self.play( FadeIn(alt_arrow_label, 0.5 * UP), FadeOut(arrow_label, 0.5 * DOWN), ) self.wait(2) class SARSvs1918(Scene): def construct(self): titles = VGroup( TextMobject("2002 SARS outbreak"), TextMobject("1918 Spanish flu"), ) images = Group( ImageMobject("sars_icon"), ImageMobject("spanish_flu"), ) for title, vect, color, image in zip(titles, [LEFT, RIGHT], [YELLOW, RED], images): image.set_height(4) image.move_to(vect * FRAME_WIDTH / 4) image.to_edge(UP) title.scale(1.25) title.next_to(image, DOWN, MED_LARGE_BUFF) title.set_color(color) title.underline = Underline(title) title.underline.set_stroke(WHITE, 1) title.add_to_back(title.underline) titles[1].underline.match_y(titles[0].underline) n_cases_labels = VGroup( TextMobject("8,096 cases"), TextMobject("$\\sim$513{,}000{,}000 cases"), ) for n_cases_label, title in zip(n_cases_labels, titles): n_cases_label.scale(1.25) n_cases_label.next_to(title, DOWN, MED_LARGE_BUFF) for image, title, label in zip(images, titles, n_cases_labels): self.play( FadeIn(image, DOWN), Write(title), run_time=1, ) self.play(FadeIn(label, UP)) self.wait() class ViralSpreadModelWithShuffling(ViralSpreadModel): def construct(self): # Init population randys = self.get_randys() self.add(*randys) # Show the sicko self.show_patient0(randys) # Repeatedly spread for x in range(15): self.spread_infection(randys) self.shuffle_randys(randys) def shuffle_randys(self, randys): indices = list(range(len(randys))) np.random.shuffle(indices) anims = [] for i, randy in zip(indices, randys): randy.generate_target() randy.target.move_to(randys[i]) anims.append(MoveToTarget( randy, path_arc=30 * DEGREES, )) self.play(LaggedStart( *anims, lag_ratio=1 / len(randys), run_time=3 )) class SneezingOnNeighbors(Scene): def construct(self): randys = VGroup(*[PiCreature() for x in range(3)]) randys.set_height(1) randys.arrange(RIGHT) self.add(randys) self.play( randys[1].change, "sick", randys[1].set_color, SICKLY_GREEN, ) self.play( Flash( randys[1], color=SICKLY_GREEN, flash_radius=0.8, ), randys[0].change, "sassy", randys[1], randys[2].change, "angry", randys[1], ) self.play( randys[0].change, "sick", randys[0].set_color, SICKLY_GREEN, randys[2].change, "sick", randys[2].set_color, SICKLY_GREEN, ) self.play( Flash( randys[1], color=SICKLY_GREEN, flash_radius=0.8, ) ) self.play( randys[0].change, "sad", randys[1], randys[2].change, "tired", randys[1], ) self.play( Flash( randys[1], color=SICKLY_GREEN, flash_radius=0.8, ) ) self.play( randys[0].change, "angry", randys[1], randys[2].change, "angry", randys[1], ) self.wait() class ViralSpreadModelWithClusters(ViralSpreadModel): def construct(self): randys = self.get_randys() self.add(*randys) self.show_patient0(randys) for x in range(6): self.spread_infection(randys) def get_randys(self): cluster = VGroup(*[Randolph() for x in range(16)]) cluster.arrange_in_grid(4, 4) cluster.set_height(1) cluster.space_out_submobjects(1.3) clusters = VGroup(*[cluster.copy() for x in range(12)]) clusters.arrange_in_grid(3, 4, buff=LARGE_BUFF) clusters.set_height(FRAME_HEIGHT - 1) for cluster in clusters: for randy in cluster: randy.infected = False self.add(clusters) self.clusters = clusters return VGroup(*it.chain(*clusters)) class ViralSpreadModelWithClustersAndTravel(ViralSpreadModelWithClusters): CONFIG = { "random_seed": 2, } def construct(self): randys = self.get_randys() self.add(*randys) self.show_patient0(randys) for x in range(20): self.spread_infection(randys) self.travel_between_clusters() self.update_frame(ignore_skipping=True) def travel_between_clusters(self): reps = VGroup(*[ random.choice(cluster) for cluster in self.clusters ]) targets = list(reps) random.shuffle(targets) anims = [] for rep, target in zip(reps, targets): rep.generate_target() rep.target.move_to(target) anims.append(MoveToTarget( rep, path_arc=30 * DEGREES, )) self.play(LaggedStart(*anims, run_time=3)) class ShowLogisticCurve(Scene): def construct(self): # Init axes axes = self.get_axes() self.add(axes) # Add ODE ode = TexMobject( "{dN \\over dt} =", "c", "\\left(1 - {N \\over \\text{pop.}}\\right)", "N", tex_to_color_map={"N": YELLOW} ) ode.set_height(0.75) ode.center() ode.to_edge(RIGHT) ode.shift(1.5 * UP) self.add(ode) # Show curve curve = axes.get_graph( lambda x: 8 * smooth(x / 10) + 0.2, ) curve.set_stroke(YELLOW, 3) curve_title = TextMobject("Logistic curve") curve_title.set_height(0.75) curve_title.next_to(curve.get_end(), UL) self.play(ShowCreation(curve, run_time=3)) self.play(FadeIn(curve_title, lag_ratio=0.1)) self.wait() # Early part line = Line( curve.point_from_proportion(0), curve.point_from_proportion(0.25), ) rect = Rectangle() rect.set_stroke(width=0) rect.set_fill(TEAL, 0.5) rect.replace(line, stretch=True) exp_curve = axes.get_graph( lambda x: 0.15 * np.exp(0.68 * x) ) exp_curve.set_stroke(RED, 3) rect.save_state() rect.stretch(0, 0, about_edge=LEFT) self.play(Restore(rect)) self.play(ShowCreation(exp_curve, run_time=4)) # Show capacity line = DashedLine( axes.c2p(0, 8.2), axes.c2p(axes.x_max, 8.2), ) line.set_stroke(BLUE, 2) self.play(ShowCreation(line)) self.wait() self.play(FadeOut(rect), FadeOut(exp_curve)) # Show inflection point infl_point = axes.input_to_graph_point(5, curve) infl_dot = Dot(infl_point) infl_dot.set_stroke(WHITE, 3) curve_up_part = curve.copy() curve_up_part.pointwise_become_partial(curve, 0, 0.4) curve_up_part.set_stroke(GREEN) curve_down_part = curve.copy() curve_down_part.pointwise_become_partial(curve, 0.4, 1) curve_down_part.set_stroke(RED) for part in curve_up_part, curve_down_part: part.save_state() part.stretch(0, 1) part.set_y(axes.c2p(0, 0)[1]) pre_dot = curve.copy() pre_dot.pointwise_become_partial(curve, 0.375, 0.425) pre_dot.unlock_triangulation() infl_name = TextMobject("Inflection point") infl_name.next_to(infl_dot, LEFT) self.play(ReplacementTransform(pre_dot, infl_dot, path_arc=90 * DEGREES)) self.add(curve_up_part, infl_dot) self.play(Restore(curve_up_part)) self.add(curve_down_part, infl_dot) self.play(Restore(curve_down_part)) self.wait() self.play(Write(infl_name, run_time=1)) self.wait() # Show tangent line x_tracker = ValueTracker(0) tan_line = Line(LEFT, RIGHT) tan_line.set_width(5) tan_line.set_stroke(YELLOW, 2) def update_tan_line(line): x1 = x_tracker.get_value() x2 = x1 + 0.001 p1 = axes.input_to_graph_point(x1, curve) p2 = axes.input_to_graph_point(x2, curve) angle = angle_of_vector(p2 - p1) line.rotate(angle - line.get_angle()) line.move_to(p1) tan_line.add_updater(update_tan_line) dot = Dot() dot.scale(0.75) dot.set_fill(BLUE, 0.75) dot.add_updater( lambda m: m.move_to(axes.input_to_graph_point( x_tracker.get_value(), curve )) ) self.play( ShowCreation(tan_line), FadeInFromLarge(dot), ) self.play( x_tracker.set_value, 5, run_time=6, ) self.wait() self.play( x_tracker.set_value, 9.9, run_time=6, ) self.wait() # Define growth factor gf_label = TexMobject( "\\text{Growth factor} =", "{\\Delta N_d \\over \\Delta N_{d - 1}}", tex_to_color_map={ "\\Delta": WHITE, "N_d": YELLOW, "N_{d - 1}": BLUE, } ) gf_label.next_to(infl_dot, RIGHT, LARGE_BUFF) numer_label = TextMobject("New cases one day") denom_label = TextMobject("New cases the\\\\previous day") for label, tex, vect in (numer_label, "N_d", UL), (denom_label, "N_{d - 1}", DL): part = gf_label.get_part_by_tex(tex) label.match_color(part) label.next_to(part, vect, LARGE_BUFF) label.shift(2 * RIGHT) arrow = Arrow( label.get_corner(vect[1] * DOWN), part.get_corner(vect[1] * UP) + 0.25 * LEFT, buff=0.1, ) arrow.match_color(part) label.add_to_back(arrow) self.play( FadeIn(gf_label[0], RIGHT), FadeIn(gf_label[1:], LEFT), FadeOut(ode) ) self.wait() for label in numer_label, denom_label: self.play(FadeIn(label, lag_ratio=0.1)) self.wait() # Show example growth factors self.play(x_tracker.set_value, 1) eq = TexMobject("=") eq.next_to(gf_label, RIGHT) gf = DecimalNumber(1.15) gf.set_height(0.4) gf.next_to(eq, RIGHT) def get_growth_factor(): x1 = x_tracker.get_value() x0 = x1 - 0.2 x2 = x1 + 0.2 p0, p1, p2 = [ axes.input_to_graph_point(x, curve) for x in [x0, x1, x2] ] return (p2[1] - p1[1]) / (p1[1] - p0[1]) gf.add_updater(lambda m: m.set_value(get_growth_factor())) self.add(eq, gf) self.play( x_tracker.set_value, 5, run_time=6, rate_func=linear, ) self.wait() self.play( x_tracker.set_value, 9, run_time=6, rate_func=linear, ) def get_axes(self): axes = Axes( x_min=0, x_max=13, y_min=0, y_max=10, y_axis_config={ "unit_size": 0.7, "include_tip": False, } ) axes.center() axes.to_edge(DOWN) x_label = TextMobject("Time") x_label.next_to(axes.x_axis, UP, aligned_edge=RIGHT) y_label = TextMobject("N cases") y_label.next_to(axes.y_axis, RIGHT, aligned_edge=UP) axes.add(x_label, y_label) return axes class SubtltyOfGrowthFactorShift(Scene): def construct(self): # Set up totals total_title = TextMobject("Totals") total_title.add(Underline(total_title)) total_title.to_edge(UP) total_title.scale(1.25) total_title.shift(LEFT) total_title.set_color(YELLOW) total_title.shift(LEFT) data = CASE_DATA[-4:] data.append(int(data[-1] + 1.15 * (data[-1] - data[-2]))) totals = VGroup(*[Integer(value) for value in data]) totals.scale(1.25) totals.arrange(DOWN, buff=0.6, aligned_edge=LEFT) totals.next_to(total_title, DOWN, buff=0.6) totals[-1].set_color(BLUE) # Set up dates dates = VGroup( TextMobject("March 3, 2020"), TextMobject("March 4, 2020"), TextMobject("March 5, 2020"), TextMobject("March 6, 2020"), ) for date, total in zip(dates, totals): date.scale(0.75) date.set_color(LIGHT_GREY) date.next_to(total, LEFT, buff=0.75, aligned_edge=DOWN) # Set up changes change_arrows = VGroup() change_labels = VGroup() for t1, t2 in zip(totals, totals[1:]): arrow = Arrow( t1.get_right(), t2.get_right(), path_arc=-150 * DEGREES, buff=0.1, max_tip_length_to_length_ratio=0.15, ) arrow.shift(MED_SMALL_BUFF * RIGHT) arrow.set_stroke(width=3) change_arrows.add(arrow) diff = t2.get_value() - t1.get_value() label = Integer(diff, include_sign=True) label.set_color(GREEN) label.next_to(arrow, RIGHT) change_labels.add(label) change_labels[-1].set_color(BLUE) change_title = TextMobject("Changes") change_title.add(Underline(change_title).shift(0.128 * UP)) change_title.scale(1.25) change_title.set_color(GREEN) change_title.move_to(change_labels) change_title.align_to(total_title, UP) # Set up growth factors gf_labels = VGroup() gf_arrows = VGroup() for c1, c2 in zip(change_labels, change_labels[1:]): arrow = Arrow( c1.get_right(), c2.get_right(), path_arc=-150 * DEGREES, buff=0.1, max_tip_length_to_length_ratio=0.15, ) arrow.set_stroke(width=1) gf_arrows.add(arrow) line = Line(LEFT, RIGHT) line.match_width(c2) line.set_stroke(WHITE, 2) numer = c2.deepcopy() denom = c1.deepcopy() frac = VGroup(numer, line, denom) frac.arrange(DOWN, buff=SMALL_BUFF) frac.scale(0.7) frac.next_to(arrow, RIGHT) eq = TexMobject("=") eq.next_to(frac, RIGHT) gf = DecimalNumber(c2.get_value() / c1.get_value()) gf.next_to(eq, RIGHT) gf_labels.add(VGroup(frac, eq, gf)) gf_title = TextMobject("Growth factors") gf_title.add(Underline(gf_title)) gf_title.scale(1.25) gf_title.move_to(gf_labels[0][-1]) gf_title.align_to(total_title, DOWN) # Add things self.add(dates, total_title) self.play( LaggedStartMap( FadeInFrom, totals[:-1], lambda m: (m, UP), ) ) self.wait() self.play( ShowCreation(change_arrows[:-1]), LaggedStartMap( FadeInFrom, change_labels[:-1], lambda m: (m, LEFT), ), FadeIn(change_title), ) self.wait() self.play( ShowCreation(gf_arrows[:-1]), LaggedStartMap(FadeIn, gf_labels[:-1]), FadeIn(gf_title), ) self.wait() # Show hypothetical new value self.play(LaggedStart( FadeIn(gf_labels[-1]), FadeIn(gf_arrows[-1]), FadeIn(change_labels[-1]), FadeIn(change_arrows[-1]), FadeIn(totals[-1]), )) self.wait() # Change it alt_change = data[-2] - data[-3] alt_total = data[-2] + alt_change alt_gf = 1 self.play( ChangeDecimalToValue(gf_labels[-1][-1], alt_gf), ChangeDecimalToValue(gf_labels[-1][0][0], alt_change), ChangeDecimalToValue(change_labels[-1], alt_change), ChangeDecimalToValue(totals[-1], alt_total), ) self.wait() class ContrastRandomShufflingWithClustersAndTravel(Scene): def construct(self): background = FullScreenFadeRectangle() background.set_fill(GREY_E) self.add(background) squares = VGroup(*[Square() for x in range(2)]) squares.set_width(FRAME_WIDTH / 2 - 1) squares.arrange(RIGHT, buff=0.75) squares.to_edge(DOWN) squares.set_fill(BLACK, 1) squares.stretch(0.8, 1) self.add(squares) titles = VGroup( TextMobject("Random shuffling"), TextMobject("Clusters with travel"), ) for title, square in zip(titles, squares): title.scale(1.4) title.next_to(square, UP) titles[1].align_to(titles[0], UP) self.play(LaggedStartMap( FadeInFrom, titles, lambda m: (m, 0.25 * DOWN), )) self.wait() class ShowVaryingExpFactor(Scene): def construct(self): factor = DecimalNumber(0.15) rect = BackgroundRectangle(factor, buff=SMALL_BUFF) rect.set_fill(BLACK, 1) arrow = Arrow( factor.get_right(), factor.get_right() + 4 * RIGHT + 0.5 * DOWN, ) self.add(rect, factor, arrow) for value in [0.05, 0.25, 0.15]: self.play( ChangeDecimalToValue(factor, value), run_time=3, ) self.wait() class ShowVaryingBaseFactor(ShowLogisticCurve): def construct(self): factor = DecimalNumber(1.15) rect = BackgroundRectangle(factor, buff=SMALL_BUFF) rect.set_fill(BLACK, 1) self.add(rect, factor) for value in [1.05, 1.25, 1.15]: self.play( ChangeDecimalToValue(factor, value), run_time=3, ) self.wait() class ShowVaryingExpCurve(ShowLogisticCurve): def construct(self): axes = self.get_axes() self.add(axes) curve = axes.get_graph(lambda x: np.exp(0.15 * x)) curve.set_stroke([BLUE, YELLOW, RED]) curve.make_jagged() self.add(curve) self.camera.frame.scale(2, about_edge=DOWN) self.camera.frame.shift(DOWN) rect = FullScreenFadeRectangle() rect.set_stroke(WHITE, 3) rect.set_fill(opacity=0) self.add(rect) for value in [0.05, 0.25, 0.15]: new_curve = axes.get_graph(lambda x: np.exp(value * x)) new_curve.set_stroke([BLUE, YELLOW, RED]) new_curve.make_jagged() self.play( Transform(curve, new_curve), run_time=3, ) class EndScreen(PatreonEndScreen): CONFIG = { "specific_patrons": [ "1stViewMaths", "Adam Dřínek", "Aidan Shenkman", "Alan Stein", "Alex Mijalis", "Alexis Olson", "Ali Yahya", "Andrew Busey", "Andrew Cary", "Andrew R. Whalley", "Aravind C V", "Arjun Chakroborty", "Arthur Zey", "Austin Goodman", "Avi Finkel", "Awoo", "AZsorcerer", "Barry Fam", "Bernd Sing", "Boris Veselinovich", "Bradley Pirtle", "Brian Staroselsky", "Britt Selvitelle", "Britton Finley", "Burt Humburg", "Calvin Lin", "Charles Southerland", "Charlie N", "Chenna Kautilya", "Chris Connett", "Christian Kaiser", "cinterloper", "Clark Gaebel", "Colwyn Fritze-Moor", "Cooper Jones", "Corey Ogburn", "D. Sivakumar", "Daniel Herrera C", "Dave B", "Dave Kester", "dave nicponski", "David B. Hill", "David Clark", "David Gow", "Delton Ding", "Dominik Wagner", "Douglas Cantrell", "emptymachine", "Eric Younge", "Eryq Ouithaqueue", "Federico Lebron", "Frank R. Brown, Jr.", "Giovanni Filippi", "Hal Hildebrand", "Hitoshi Yamauchi", "Ivan Sorokin", "Jacob Baxter", "Jacob Harmon", "Jacob Hartmann", "Jacob Magnuson", "Jake Vartuli - Schonberg", "Jameel Syed", "Jason Hise", "Jayne Gabriele", "Jean-Manuel Izaret", "Jeff Linse", "Jeff Straathof", "John C. Vesey", "John Haley", "John Le", "John V Wertheim", "Jonathan Heckerman", "Jonathan Wilson", "Joseph John Cox", "Joseph Kelly", "Josh Kinnear", "Joshua Claeys", "Juan Benet", "Kai-Siang Ang", "Kanan Gill", "Karl Niu", "Kartik Cating-Subramanian", "Kaustuv DeBiswas", "Killian McGuinness", "Kros Dai", "L0j1k", "Lambda GPU Workstations", "Lee Redden", "Linh Tran", "Luc Ritchie", "Ludwig Schubert", "Lukas Biewald", "Magister Mugit", "Magnus Dahlström", "Manoj Rewatkar - RITEK SOLUTIONS", "Mark Heising", "Mark Mann", "Martin Price", "Mathias Jansson", "Matt Godbolt", "Matt Langford", "Matt Roveto", "Matt Russell", "Matteo Delabre", "Matthew Bouchard", "Matthew Cocke", "Mia Parent", "Michael Hardel", "Michael W White", "Mirik Gogri", "Mustafa Mahdi", "Márton Vaitkus", "Nicholas Cahill", "Nikita Lesnikov", "Oleg Leonov", "Oliver Steele", "Omar Zrien", "Owen Campbell-Moore", "Patrick Lucas", "Peter Ehrnstrom", "Peter Mcinerney", "Pierre Lancien", "Quantopian", "Randy C. Will", "rehmi post", "Rex Godby", "Ripta Pasay", "Rish Kundalia", "Roman Sergeychik", "Roobie", "Ryan Atallah", "Samuel Judge", "SansWord Huang", "Scott Gray", "Scott Walter, Ph.D.", "Sebastian Garcia", "soekul", "Solara570", "Steve Huynh", "Steve Sperandeo", "Steven Braun", "Steven Siddals", "Stevie Metke", "supershabam", "Suteerth Vishnu", "Suthen Thomas", "Tal Einav", "Tauba Auerbach", "Ted Suzman", "Thomas J Sargent", "Thomas Tarler", "Tianyu Ge", "Tihan Seale", "Tyler VanValkenburg", "Vassili Philippov", "Veritasium", "Vinicius Reis", "Xuanji Li", "Yana Chernobilsky", "Yavor Ivanov", "YinYangBalance.Asia", "Yu Jun", "Yurii Monastyrshyn", ] }