From 2ce0b72c440fadbfba990bdac8bc9c2ea8bff4e7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Mar 2020 12:04:42 -0700 Subject: [PATCH] Final scenes for SIR video --- from_3b1b/active/sir.py | 2706 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 2651 insertions(+), 55 deletions(-) diff --git a/from_3b1b/active/sir.py b/from_3b1b/active/sir.py index 8060a4b4..66515786 100644 --- a/from_3b1b/active/sir.py +++ b/from_3b1b/active/sir.py @@ -1,5 +1,7 @@ from manimlib.imports import * -# import scipy.stats +from from_3b1b.active.bayes.beta_helpers import fix_percent +from from_3b1b.active.bayes.beta_helpers import XMARK_TEX +from from_3b1b.active.bayes.beta_helpers import CMARK_TEX SICKLY_GREEN = "#9BBD37" @@ -26,17 +28,22 @@ class Person(VGroup): }, "infection_radius": 0.5, "infection_animation_period": 2, + "symptomatic": False, + "p_symptomatic_on_infection": 1, "max_speed": 1, "dl_bound": [-FRAME_WIDTH / 2, -FRAME_HEIGHT / 2], "ur_bound": [FRAME_WIDTH / 2, FRAME_HEIGHT / 2], "gravity_well": None, "gravity_strength": 1, + "wall_buffer": 1, "wander_step_size": 1, "wander_step_duration": 1, "social_distance_factor": 0, + "social_distance_color_threshold": 2, "n_repulsion_points": 10, "social_distance_color": YELLOW, "max_social_distance_stroke_width": 5, + "asymptomatic_color": YELLOW, } def __init__(self, **kwargs): @@ -49,6 +56,7 @@ class Person(VGroup): self.infection_start_time = np.inf self.infection_end_time = np.inf self.repulsion_points = [] + self.num_infected = 0 self.center_point = VectorizedPoint() self.add(self.center_point) @@ -77,6 +85,19 @@ class Person(VGroup): def set_status(self, status, run_time=1): start_color = self.color_map[self.status] end_color = self.color_map[status] + + if status == "I": + self.infection_start_time = self.time + self.infection_ring.set_stroke(width=0, opacity=0) + if random.random() < self.p_symptomatic_on_infection: + self.symptomatic = True + else: + self.infection_ring.set_color(self.asymptomatic_color) + end_color = self.asymptomatic_color + if self.status == "I": + self.infection_end_time = self.time + self.symptomatic = False + anims = [ UpdateFromAlphaFunc( self.body, @@ -89,15 +110,10 @@ class Person(VGroup): for anim in anims: self.push_anim(anim) - if status == "I": - self.infection_start_time = self.time - self.infection_ring.set_stroke(width=0, opacity=0) - if self.status == "I": - self.infection_end_time = self.time - self.status = status def push_anim(self, anim): + anim.suspend_mobject_updating = False anim.begin() anim.start_time = self.time self.change_anims.append(anim) @@ -141,13 +157,19 @@ class Person(VGroup): for point in self.repulsion_points: to_point = point - center dist = get_norm(to_point) - if dist < min_dist: + if 0 < dist < min_dist: min_dist = dist if dist > 0: repulsion_force -= self.social_distance_factor * to_point / (dist**3) + sdct = self.social_distance_color_threshold self.body.set_stroke( self.social_distance_color, - width=clip(4 * min_dist - 1, 0, self.max_social_distance_stroke_width), + width=clip( + (sdct / min_dist) - sdct, + # 2 * (sdct / min_dist), + 0, + self.max_social_distance_stroke_width + ), background=True, ) total_force += repulsion_force @@ -161,12 +183,14 @@ class Person(VGroup): # Bounce if to_lower < 0: self.velocity[i] = abs(self.velocity[i]) + self.set_coord(self.dl_bound[i], i) if to_upper < 0: self.velocity[i] = -abs(self.velocity[i]) + self.set_coord(self.ur_bound[i], i) # Repelling force - wall_force[i] += max((-1 + 1 / to_lower), 0) - wall_force[i] -= max((-1 + 1 / to_upper), 0) + wall_force[i] += max((-1 / self.wall_buffer + 1 / to_lower), 0) + wall_force[i] -= max((-1 / self.wall_buffer + 1 / to_upper), 0) total_force += wall_force # Apply force @@ -177,7 +201,7 @@ class Person(VGroup): if speed > self.max_speed: self.velocity *= self.max_speed / speed - # Update velocity + # Update position self.shift(self.velocity * dt) def update_infection_ring(self, dt): @@ -213,7 +237,7 @@ class Person(VGroup): alpha = 1 else: alpha = (self.time - anim.start_time) / anim.run_time - anim.update(alpha) + anim.interpolate(alpha) if alpha >= 1: self.pop_anim(anim) @@ -276,6 +300,7 @@ class SIRSimulation(VGroup): "p_infection_per_day": 0.2, "infection_time": 5, "travel_rate": 0, + "limit_social_distancing_to_infectious": False, } def __init__(self, **kwargs): @@ -304,6 +329,7 @@ class SIRSimulation(VGroup): for box in self.boxes: dl_bound = box.get_corner(DL) ur_bound = box.get_corner(UR) + box.people = VGroup() for x in range(self.city_population): person = self.person_type( dl_bound=dl_bound, @@ -315,6 +341,7 @@ class SIRSimulation(VGroup): for lower, upper in zip(dl_bound, ur_bound) ]) person.box = box + box.people.add(person) people.add(person) # Choose a patient zero @@ -323,22 +350,24 @@ class SIRSimulation(VGroup): self.people = people def update_statusses(self, dt): - s_group, i_group = [ - list(filter( - lambda m: m.status == status, - self.people - )) - for status in ["S", "I"] - ] + for box in self.boxes: + s_group, i_group = [ + list(filter( + lambda m: m.status == status, + box.people + )) + for status in ["S", "I"] + ] - for s_person in s_group: + for s_person in s_group: + for i_person in i_group: + dist = get_norm(i_person.get_center() - s_person.get_center()) + if dist < s_person.infection_radius and random.random() < self.p_infection_per_day * dt: + s_person.set_status("I") + i_person.num_infected += 1 for i_person in i_group: - dist = get_norm(i_person.get_center() - s_person.get_center()) - if dist < s_person.infection_radius and random.random() < self.p_infection_per_day * dt: - s_person.set_status("I") - for i_person in i_group: - if (i_person.time - i_person.infection_start_time) > self.infection_time: - i_person.set_status("R") + if (i_person.time - i_person.infection_start_time) > self.infection_time: + i_person.set_status("R") # Travel if self.travel_rate > 0: @@ -346,9 +375,12 @@ class SIRSimulation(VGroup): for person in self.people: if random.random() < self.travel_rate * dt: new_box = random.choice(self.boxes) + person.box.people.remove(person) + new_box.people.add(person) person.box = new_box person.dl_bound = new_box.get_corner(DL) person.ur_bound = new_box.get_corner(UR) + person.old_center = person.get_center() person.new_center = new_box.get_center() anim = UpdateFromAlphaFunc( @@ -356,16 +388,26 @@ class SIRSimulation(VGroup): lambda m, a: m.move_to(path_func( m.old_center, m.new_center, a, )), - run_time=3, + run_time=1, ) person.push_anim(anim) # Social distancing centers = np.array([person.get_center() for person in self.people]) - for center, person in zip(centers, self.people): - if person.social_distance_factor > 0: - diffs = np.linalg.norm(centers - center, axis=1) - person.repulsion_points = centers[np.argsort(diffs)[1:person.n_repulsion_points + 1]] + if self.limit_social_distancing_to_infectious: + repelled_centers = np.array([ + person.get_center() + for person in self.people + if person.symptomatic + ]) + else: + repelled_centers = centers + + if len(repelled_centers) > 0: + for center, person in zip(centers, self.people): + if person.social_distance_factor > 0: + diffs = np.linalg.norm(repelled_centers - center, axis=1) + person.repulsion_points = repelled_centers[np.argsort(diffs)[1:person.n_repulsion_points + 1]] def get_status_counts(self): return np.array([ @@ -501,6 +543,8 @@ class SIRGraph(VGroup): tick_range = range(5, int(self.time) + 1, 5) elif self.time < 100: tick_range = range(10, int(self.time) + 1, 10) + else: + tick_range = range(20, int(self.time) + 1, 20) for x in tick_range: tick = get_tick(x) @@ -508,12 +552,38 @@ class SIRGraph(VGroup): self.x_ticks.add(tick) self.x_labels.add(label) + # TODO, if I care, refactor if 10 < self.time < 15: alpha = (self.time - 10) / 5 for tick, label in zip(self.x_ticks, self.x_labels): if label.get_value() % 5 != 0: label.set_opacity(1 - alpha) tick.set_opacity(1 - alpha) + if 45 < self.time < 50: + alpha = (self.time - 45) / 5 + for tick, label in zip(self.x_ticks, self.x_labels): + if label.get_value() % 10 == 5: + label.set_opacity(1 - alpha) + tick.set_opacity(1 - alpha) + + def add_v_line(self, line_time=None, color=YELLOW, stroke_width=3): + if line_time is None: + line_time = self.time + + axes = self.axes + v_line = Line( + axes.c2p(1, 0), axes.c2p(1, 1), + stroke_color=color, + stroke_width=stroke_width, + ) + v_line.add_updater( + lambda m: m.move_to( + axes.c2p(line_time / max(self.time, 1e-6), 0), + DOWN, + ) + ) + + self.add(v_line) class GraphBraces(VGroup): @@ -535,7 +605,7 @@ class GraphBraces(VGroup): self.labels = VGroup( TextMobject("Susceptible", color=COLOR_MAP["S"]), TextMobject("Infectious", color=COLOR_MAP["I"]), - TextMobject("Recovered", color=COLOR_MAP["R"]), + TextMobject("Removed", color=COLOR_MAP["R"]), ) self.max_label_height = graph.get_height() * 0.05 @@ -543,9 +613,10 @@ class GraphBraces(VGroup): self.add(self.braces, self.labels) self.time = 0 - self.last_update_time = 0 + self.last_update_time = -1 self.add_updater(update_time) self.add_updater(lambda m: m.update_braces()) + self.update(0) def update_braces(self): if (self.time - self.last_update_time) <= self.update_frequency: @@ -604,9 +675,9 @@ class ValueSlider(NumberLine): self.marker.set_color(self.marker_color) self.add(self.marker) - self.label = DecimalNumber(value) - self.label.next_to(self.marker, UP) - self.add(self.label) + # self.label = DecimalNumber(value) + # self.label.next_to(self.marker, UP) + # self.add(self.label) self.name = TextMobject(name) self.name.scale(1.25) @@ -615,20 +686,47 @@ class ValueSlider(NumberLine): self.add(self.name) def get_change_anim(self, new_value, **kwargs): - start_value = self.label.get_value() - m2l = self.label.get_center() - self.marker.get_center() + start_value = self.p2n(self.marker.get_bottom()) + # m2l = self.label.get_center() - self.marker.get_center() def update(mob, alpha): interim_value = interpolate(start_value, new_value, alpha) mob.marker.move_to(mob.n2p(interim_value), DOWN) - mob.label.move_to(mob.marker.get_center() + m2l) - mob.label.set_value(interim_value) + # mob.label.move_to(mob.marker.get_center() + m2l) + # mob.label.set_value(interim_value) return UpdateFromAlphaFunc(self, update, **kwargs) # Scenes +class Test(Scene): + def construct(self): + path_func = path_along_arc(45 * DEGREES) + person = PiPerson(height=1, gravity_strength=0.2) + person.gravity_strength = 0 + + person.old_center = person.get_center() + person.new_center = 4 * RIGHT + + self.add(person) + self.wait() + + self.play(UpdateFromAlphaFunc( + person, + lambda m, a: m.move_to(path_func( + m.old_center, + m.new_center, + a, + )), + run_time=3, + rate_func=there_and_back, + )) + + self.wait(3) + self.wait(3) + + class RunSimpleSimulation(Scene): CONFIG = { "simulation_config": { @@ -645,7 +743,7 @@ class RunSimpleSimulation(Scene): "infection_time": 5, }, "graph_config": { - "update_frequency": 0.25, + "update_frequency": 1 / 15, }, "graph_height_to_frame_height": 0.5, "graph_width_to_frame_height": 0.75, @@ -657,10 +755,72 @@ class RunSimpleSimulation(Scene): self.position_camera() self.add_graph() self.add_sliders() + self.add_R_label() + self.add_total_cases_label() def construct(self): - for x in range(5): + self.run_until_zero_infections() + + def wait_until_infection_threshold(self, threshold): + self.wait_until(lambda: self.simulation.get_status_counts()[1] > threshold) + + def run_until_zero_infections(self): + while True: self.wait(5) + if self.simulation.get_status_counts()[1] == 0: + self.wait(5) + break + + def add_R_label(self): + label = VGroup( + TexMobject("R = "), + DecimalNumber(), + ) + label.arrange(RIGHT) + boxes = self.simulation.boxes + label.set_width(0.25 * boxes.get_width()) + label.next_to(boxes.get_corner(DL), DR) + self.add(label) + + all_R0_values = [] + + def update_label(label): + if (self.time - label.last_update_time) < label.update_period: + return + label.last_update_time = self.time + + values = [] + for person in self.simulation.people: + if person.status == "I": + prop = (person.time - person.infection_start_time) / self.simulation.infection_time + if prop > 0.1: + values.append(person.num_infected / prop) + if len(values) > 0: + all_R0_values.append(np.mean(values)) + average = np.mean(all_R0_values[-20:]) + label[1].set_value(average) + + label.last_update_time = 0 + label.update_period = 1 + label.add_updater(update_label) + + def add_total_cases_label(self): + label = VGroup( + TextMobject("\\# Active cases = "), + Integer(1), + ) + label.arrange(RIGHT) + label[1].align_to(label[0][0][1], DOWN) + label.set_color(RED) + boxes = self.simulation.boxes + label.set_width(0.5 * boxes.get_width()) + label.next_to(boxes, UP, buff=0.03 * boxes.get_width()) + + label.add_updater( + lambda m: m[1].set_value(self.simulation.get_status_counts()[1]) + ) + self.total_cases_label = label + self.add(label) def add_simulation(self): self.simulation = SIRSimulation(**self.simulation_config) @@ -706,7 +866,52 @@ class RunSimpleSimulation(Scene): class RunSimpleSimulationWithDots(RunSimpleSimulation): CONFIG = { - "person_type": DotPerson, + "simulation_config": { + "person_type": DotPerson, + } + } + + +class LargerCity(RunSimpleSimulation): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + "city_population": 1000, + "person_config": { + "infection_radius": 0.25, + "social_distance_factor": 0, + "gravity_strength": 0.2, + "max_speed": 0.25, + "height": 0.2 / 3, + "wall_buffer": 1 / 3, + "social_distance_color_threshold": 2 / 3, + }, + } + } + + +class LargerCity2(LargerCity): + CONFIG = { + "random_seed": 1, + } + + +class LargeCityHighInfectionRadius(LargerCity): + CONFIG = { + "simulation_config": { + "person_config": { + "infection_radius": 0.5, + }, + }, + "graph_config": { + "update_frequency": 1 / 60, + }, + } + + +class LargeCityLowerInfectionRate(LargerCity): + CONFIG = { + "p_infection_per_day": 0.1, } @@ -726,9 +931,149 @@ class SimpleSocialDistancing(RunSimpleSimulation): }, } + +class DelayedSocialDistancing(RunSimpleSimulation): + CONFIG = { + "delay_time": 8, + "target_sd_factor": 2, + "sd_probability": 1, + "random_seed": 1, + } + def construct(self): - for x in range(5): - self.wait(5) + self.wait(self.delay_time) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + def change_social_distance_factor(self, new_factor, prob): + for person in self.simulation.people: + if random.random() < prob: + person.social_distance_factor = new_factor + + def add_sliders(self): + slider = ValueSlider( + self.get_sd_slider_name(), + value=0, + x_min=0, + x_max=2, + tick_frequency=0.5, + numbers_with_elongated_ticks=[], + numbers_to_show=range(3), + decimal_number_config={ + "num_decimal_places": 0, + } + ) + fix_percent(slider.name[0][23 + int(self.sd_probability == 1)]) # So dumb + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.sd_slider = slider + + def get_sd_slider_name(self): + return f"Social Distance Factor\\\\({int(100 * self.sd_probability)}$\\%$ of population)" + + +class DelayedSocialDistancingDot(DelayedSocialDistancing): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + } + } + + +class DelayedSocialDistancingLargeCity(DelayedSocialDistancing, LargerCity): + CONFIG = { + "trigger_infection_count": 50, + "simulation_config": { + 'city_population': 900, + } + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + +class DelayedSocialDistancingLargeCity90p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.9, + } + + +class DelayedSocialDistancingLargeCity90pAlt(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.9, + "random_seed": 5, + } + + +class DelayedSocialDistancingLargeCity70p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.7, + } + + +class DelayedSocialDistancingLargeCity50p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.5, + } + + +class DelayedSocialDistancingWithDots(DelayedSocialDistancing): + CONFIG = { + "person_type": DotPerson, + } + + +class DelayedSocialDistancingProbHalf(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.5, + } + + +class ReduceInfectionDuration(LargerCity): + CONFIG = { + "trigger_infection_count": 50, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.play(self.slider.get_change_anim(1)) + self.simulation.infection_time = 1 + self.graph.add_v_line() + self.run_until_zero_infections() + + def add_sliders(self): + slider = ValueSlider( + "Infection duration", + value=5, + x_min=0, + x_max=5, + tick_frequency=1, + numbers_with_elongated_ticks=[], + numbers_to_show=range(6), + decimal_number_config={ + "num_decimal_places": 0, + }, + marker_color=RED, + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.slider = slider class SimpleTravel(RunSimpleSimulation): @@ -747,24 +1092,2275 @@ class SimpleTravel(RunSimpleSimulation): }, } - def construct(self): - for x in range(10): - self.wait(5) - def add_sliders(self): slider = ValueSlider( "Travel rate", self.simulation.travel_rate, x_min=0, - x_max=0.1, - tick_frequency=0.01, + x_max=0.02, + tick_frequency=0.005, numbers_with_elongated_ticks=[], - numbers_to_show=np.linspace(0, 0.1, 6), + numbers_to_show=np.arange(0, 0.03, 0.01), decimal_number_config={ "num_decimal_places": 2, } ) slider.match_width(self.graph) - slider.next_to(self.graph, DOWN, buff=5) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) self.add(slider) - self.slider = slider + self.tr_slider = slider + + +class SimpleTravel2(SimpleTravel): + CONFIG = { + "random_seed": 1, + } + + +class SimpleTravelLongInfectionPeriod(SimpleTravel): + CONFIG = { + "simulation_config": { + "infection_time": 10, + } + } + + +class SimpleTravelDelayedSocialDistancing(DelayedSocialDistancing, SimpleTravel): + CONFIG = { + "target_sd_factor": 2, + "sd_probability": 0.7, + "delay_time": 15, + "trigger_infection_count": 50, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + def add_sliders(self): + SimpleTravel.add_sliders(self) + DelayedSocialDistancing.add_sliders(self) + + buff = 0.1 * self.graph.get_height() + + self.tr_slider.scale(0.8, about_edge=UP) + self.tr_slider.next_to(self.graph, DOWN, buff=buff) + + self.sd_slider.scale(0.8) + self.sd_slider.marker.set_color(YELLOW) + self.sd_slider.name.set_color(YELLOW) + self.sd_slider.next_to(self.tr_slider, DOWN, buff=buff) + + +class SimpleTravelDelayedSocialDistancing70p(SimpleTravelDelayedSocialDistancing): + pass + + +class SimpleTravelDelayedSocialDistancing99p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.99, + } + + +class SimpleTravelDelayedSocialDistancing20p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.20, + } + + +class SimpleTravelDelayedSocialDistancing50p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.50, + "random_seed": 1, + } + + +class SimpleTravelDelayedSocialDistancing50pThreshold100(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.50, + "trigger_infection_count": 100, + "random_seed": 5, + } + + +class SimpleTravelDelayedSocialDistancing70pThreshold100(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.70, + "trigger_infection_count": 100, + } + + +class SimpleTravelSocialDistancePlusZeroTravel(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 1, + "target_travel_rate": 0, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.simulation.travel_rate = self.target_travel_rate + self.graph.add_v_line() + self.play( + self.tr_slider.get_change_anim(self.simulation.travel_rate), + self.sd_slider.get_change_anim(self.target_sd_factor), + ) + + self.run_until_zero_infections() + + +class SecondWave(SimpleTravelSocialDistancePlusZeroTravel): + def run_until_zero_infections(self): + self.wait_until(lambda: self.simulation.get_status_counts()[1] < 10) + self.change_social_distance_factor(0, 1) + self.simulation.travel_rate = 0.02 + self.graph.add_v_line() + self.play( + self.tr_slider.get_change_anim(0.02), + self.sd_slider.get_change_anim(0), + ) + super().run_until_zero_infections() + + +class SimpleTravelSocialDistancePlusZeroTravel99p(SimpleTravelSocialDistancePlusZeroTravel): + CONFIG = { + "sd_probability": 0.99, + } + + +class SimpleTravelDelayedTravelReduction(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "trigger_infection_count": 50, + "target_travel_rate": 0.002, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.simulation.travel_rate = self.target_travel_rate + self.graph.add_v_line() + self.play(self.tr_slider.get_change_anim(self.simulation.travel_rate)) + self.run_until_zero_infections() + + +class SimpleTravelDelayedTravelReductionThreshold100(SimpleTravelDelayedTravelReduction): + CONFIG = { + "random_seed": 2, + "trigger_infection_count": 100, + } + + +class SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent(SimpleTravelDelayedTravelReduction): + CONFIG = { + "random_seed": 2, + "trigger_infection_count": 100, + "target_travel_rate": 0.005, + } + + +class SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent2(SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent): + CONFIG = { + "random_seed": 1, + "sd_probability": 0.5, + } + + def setup(self): + super().setup() + for x in range(2): + random.choice(self.simulation.people).set_status("I") + + +class SimpleTravelLargeCity(SimpleTravel, LargerCity): + CONFIG = { + "simulation_config": { + "n_cities": 12, + "travel_rate": 0.02, + } + } + + +class SimpleTravelLongerDelayedSocialDistancing(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "trigger_infection_count": 100, + } + + +class SimpleTravelLongerDelayedTravelReduction(SimpleTravelDelayedTravelReduction): + CONFIG = { + "trigger_infection_count": 100, + } + + +class SocialDistanceAfterFiveDays(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.7, + "delay_time": 5, + "simulation_config": { + "travel_rate": 0 + }, + } + + +class QuarantineInfectious(RunSimpleSimulation): + CONFIG = { + "trigger_infection_count": 10, + "target_sd_factor": 3, + "infection_time_before_quarantine": 1, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.add_quarantine_box() + self.set_quarantine_updaters() + self.run_until_zero_infections() + + def add_quarantine_box(self): + boxes = self.simulation.boxes + q_box = boxes[0].copy() + q_box.set_color(RED_E) + q_box.set_width(boxes.get_width() / 3) + q_box.next_to( + boxes, LEFT, + aligned_edge=DOWN, + buff=0.25 * q_box.get_width() + ) + + label = TextMobject("Quarantine zone") + label.set_color(RED) + label.match_width(q_box) + label.next_to(q_box, DOWN, buff=0.1 * q_box.get_width()) + + self.add(q_box) + self.add(label) + self.q_box = q_box + + def set_quarantine_updaters(self): + def quarantine_if_ready(simulation): + for person in simulation.people: + send_to_q_box = all([ + not person.is_quarantined, + person.symptomatic, + (person.time - person.infection_start_time) > self.infection_time_before_quarantine, + ]) + if send_to_q_box: + person.box = self.q_box + person.dl_bound = self.q_box.get_corner(DL) + person.ur_bound = self.q_box.get_corner(UR) + person.old_center = person.get_center() + person.new_center = self.q_box.get_center() + point = VectorizedPoint(person.get_center()) + person.push_anim(ApplyMethod(point.move_to, self.q_box.get_center(), run_time=0.5)) + person.push_anim(MaintainPositionRelativeTo(person, point)) + person.move_to(self.q_box.get_center()) + person.is_quarantined = True + + for person in self.simulation.people: + person.is_quarantined = False + # person.add_updater(quarantine_if_ready) + self.simulation.add_updater(quarantine_if_ready) + + +class QuarantineInfectiousLarger(QuarantineInfectious, LargerCity): + CONFIG = { + "trigger_infection_count": 50, + } + + +class QuarantineInfectiousLargerWithTail(QuarantineInfectiousLarger): + def construct(self): + super().construct() + self.simulation.clear_updaters() + self.wait(25) + + +class QuarantineInfectiousTravel(QuarantineInfectious, SimpleTravel): + CONFIG = { + "trigger_infection_count": 50, + } + + def add_sliders(self): + pass + + +class QuarantineInfectious80p(QuarantineInfectious): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectiousLarger80p(QuarantineInfectiousLarger): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectiousTravel80p(QuarantineInfectiousTravel): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectious50p(QuarantineInfectious): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class QuarantineInfectiousLarger50p(QuarantineInfectiousLarger): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class QuarantineInfectiousTravel50p(QuarantineInfectiousTravel): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class CentralMarket(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.7, + "delay_time": 5, + "simulation_config": { + "person_type": DotPerson, + "travel_rate": 0 + }, + "shopping_frequency": 0.05, + "shopping_time": 1, + } + + def setup(self): + super().setup() + for person in self.simulation.people: + person.last_shopping_trip = -3 + person.is_shopping = False + + square = Square() + square.set_height(0.2) + square.set_color(WHITE) + square.move_to(self.simulation.boxes[0].get_center()) + self.add(square) + + self.simulation.add_updater( + lambda m, dt: self.add_travel_anims(m, dt) + ) + + def construct(self): + self.run_until_zero_infections() + + def add_travel_anims(self, simulation, dt): + shopping_time = self.shopping_time + for person in simulation.people: + time_since_trip = person.time - person.last_shopping_trip + if time_since_trip > shopping_time: + if random.random() < dt * self.shopping_frequency: + person.last_shopping_trip = person.time + + point = VectorizedPoint(person.get_center()) + anim1 = ApplyMethod( + point.move_to, person.box.get_center(), + path_arc=45 * DEGREES, + run_time=shopping_time, + rate_func=there_and_back_with_pause, + ) + anim2 = MaintainPositionRelativeTo(person, point, run_time=shopping_time) + + person.push_anim(anim1) + person.push_anim(anim2) + + def add_sliders(self): + pass + + +class CentralMarketLargePopulation(CentralMarket, LargerCity): + pass + + +class CentralMarketLowerInfection(CentralMarketLargePopulation): + CONFIG = { + "simulation_config": { + "p_infection_per_day": 0.1, + } + } + + +class CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing(CentralMarketLowerInfection): + CONFIG = { + "sd_probability": 0.7, + "trigger_infection_count": 25, + "simulation_config": { + "person_type": DotPerson, + }, + "target_sd_factor": 2, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.graph.add_v_line() + for person in self.simulation.people: + person.social_distance_factor = self.target_sd_factor + self.run_until_zero_infections() + + +class CentralMarketLessFrequent(CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing): + CONFIG = { + "target_shopping_frequency": 0.01, + "trigger_infection_count": 100, + "random_seed": 1, + "simulation_config": { + 'city_population': 900, + }, + } + + def construct(self): + self.wait_until(lambda: self.simulation.get_status_counts()[1] > self.trigger_infection_count) + for person in self.simulation.people: + person.social_distance_factor = 2 + # Decrease shopping rate + self.graph.add_v_line() + self.change_slider() + self.run_until_zero_infections() + + def change_slider(self): + self.play(self.shopping_slider.get_change_anim(self.target_shopping_frequency)) + self.shopping_frequency = self.target_shopping_frequency + + def add_sliders(self): + slider = ValueSlider( + "Shopping frequency", + value=self.shopping_frequency, + x_min=0, + x_max=0.05, + tick_frequency=0.01, + numbers_with_elongated_ticks=[], + numbers_to_show=np.arange(0, 0.06, 0.01), + decimal_number_config={ + "num_decimal_places": 2, + } + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.shopping_slider = slider + + +class CentralMarketDownToZeroFrequency(CentralMarketLessFrequent): + CONFIG = { + "target_shopping_frequency": 0, + } + + +class CentralMarketQuarantine(QuarantineInfectiousLarger, CentralMarketLowerInfection): + CONFIG = { + "random_seed": 1, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.graph.add_v_line() + self.add_quarantine_box() + self.set_quarantine_updaters() + self.run_until_zero_infections() + + +class CentralMarketQuarantine80p(CentralMarketQuarantine): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class CentralMarketTransitionToLowerInfection(CentralMarketLessFrequent): + CONFIG = { + "target_p_infection_per_day": 0.05, # From 0.1 + "trigger_infection_count": 100, + "random_seed": 1, + "simulation_config": { + 'city_population': 900, + }, + } + + def change_slider(self): + self.play(self.infection_slider.get_change_anim(self.target_p_infection_per_day)) + self.simulation.p_infection_per_day = self.target_p_infection_per_day + + def add_sliders(self): + slider = ValueSlider( + "Infection rate", + value=self.simulation.p_infection_per_day, + x_min=0, + x_max=0.2, + tick_frequency=0.05, + numbers_with_elongated_ticks=[], + numbers_to_show=np.arange(0, 0.25, 0.05), + decimal_number_config={ + "num_decimal_places": 2, + }, + marker_color=RED, + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.infection_slider = slider + + +class CentralMarketTransitionToLowerInfectionAndLowerFrequency(CentralMarketTransitionToLowerInfection): + CONFIG = { + "random_seed": 2, + } + + def change_slider(self): + super().change_slider() + self.shopping_frequency = self.target_shopping_frequency + + +# Filler animations + +class TableOfContents(Scene): + def construct(self): + chapters = VGroup( + TextMobject("Basic setup"), + TextMobject("Identify and isolate"), + TextMobject("Social distancing"), + TextMobject("Travel restrictions"), + TextMobject("$R$"), + TextMobject("Central hubs"), + ) + chapters.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + chapters.set_height(FRAME_HEIGHT - 1) + chapters.to_edge(LEFT, buff=LARGE_BUFF) + + for chapter in chapters: + dot = Dot() + dot.next_to(chapter, LEFT, SMALL_BUFF) + chapter.add(dot) + chapter.save_state() + + self.add(chapters) + + for chapter in chapters: + non_chapter = [c for c in chapters if c is not chapter] + chapter.target = chapter.saved_state.copy() + chapter.target.scale(1.5, about_edge=LEFT) + chapter.target.set_fill(YELLOW, 1) + for nc in non_chapter: + nc.target = nc.saved_state.copy() + nc.target.scale(0.8, about_edge=LEFT) + nc.target.set_fill(WHITE, 0.5) + self.play(*map(MoveToTarget, chapters)) + self.wait() + self.play(*map(Restore, chapters)) + self.wait() + + +class DescribeModel(Scene): + def construct(self): + # Setup words + words = TextMobject( + "Susceptible", + "Infectious", + "Recovered", + ) + words.scale(0.8 / words[0][0].get_height()) + + colors = [ + COLOR_MAP["S"], + COLOR_MAP["I"], + interpolate_color(COLOR_MAP["R"], WHITE, 0.5), + ] + + initials = VGroup() + for i, word, color in zip(it.count(), words, colors): + initials.add(word[0][0]) + word.set_color(color) + word.move_to(2 * i * DOWN) + word.to_edge(LEFT) + words.to_corner(UL) + + # Rearrange initials + initials.save_state() + initials.arrange(RIGHT, buff=SMALL_BUFF) + initials.set_color(WHITE) + + title = VGroup(initials, TextMobject("Model")) + title[1].match_height(title[0]) + title.arrange(RIGHT, buff=MED_LARGE_BUFF) + title.center() + title.to_edge(UP) + + self.play(FadeInFromDown(title)) + self.wait() + self.play( + Restore( + initials, + path_arc=-90 * DEGREES, + ), + FadeOut(title[1]) + ) + self.wait() + + # Show each category + pi = PiPerson( + height=3, + max_speed=0, + infection_radius=5, + ) + pi.color_map["R"] = words[2].get_color() + pi.center() + pi.body.change("pondering", initials[0]) + + word_anims = [] + for word in words: + word_anims.append(LaggedStartMap( + FadeInFrom, word[1:], + lambda m: (m, 0.2 * LEFT), + )) + + self.play( + Succession( + FadeInFromDown(pi), + ApplyMethod(pi.body.change, "guilty"), + ), + word_anims[0], + run_time=2, + ) + words[0].pi = pi.copy() + self.play( + words[0].pi.set_height, 1, + words[0].pi.next_to, words[0], RIGHT, + ) + self.play(Blink(pi.body)) + + pi.set_status("I") + point = VectorizedPoint(pi.get_center()) + self.play( + point.shift, 3 * RIGHT, + MaintainPositionRelativeTo(pi, point), + word_anims[1], + run_time=2, + ) + words[1].pi = pi.copy() + self.play( + words[1].pi.set_height, 1, + words[1].pi.next_to, words[1], RIGHT, + ) + self.wait(3) + + pi.set_status("R") + self.play( + word_anims[2], + Animation(pi, suspend_mobject_updating=False) + ) + words[2].pi = pi.copy() + self.play( + words[2].pi.set_height, 1, + words[2].pi.next_to, words[2], RIGHT, + ) + self.wait() + + # Show rules + i_pi = PiPerson( + height=1.5, + max_speed=0, + infection_radius=6, + status="S", + ) + i_pi.set_status("I") + s_pis = VGroup() + for vect in [RIGHT, UP, LEFT, DOWN]: + s_pi = PiPerson( + height=1.5, + max_speed=0, + infection_radius=6, + status="S", + ) + s_pi.next_to(i_pi, vect, MED_LARGE_BUFF) + s_pis.add(s_pi) + + VGroup(i_pi, s_pis).to_edge(RIGHT) + + circle = Circle(radius=3) + circle.move_to(i_pi) + dashed_circle = DashedVMobject(circle, num_dashes=30) + dashed_circle.set_color(RED) + + self.play( + FadeOut(pi), + FadeIn(s_pis), + FadeIn(i_pi), + ) + anims = [] + for s_pi in s_pis: + anims.append(ApplyMethod(s_pi.body.look_at, i_pi.body.eyes)) + self.play(*anims) + self.add(VGroup(i_pi, *s_pis)) + self.wait() + self.play(ShowCreation(dashed_circle)) + self.wait() + shuffled = list(s_pis) + random.shuffle(shuffled) + for s_pi in shuffled: + s_pi.set_status("I") + self.wait(3 * random.random()) + self.wait(2) + self.play(FadeOut(s_pis), FadeOut(dashed_circle)) + + # Let time pass + clock = Clock() + clock.next_to(i_pi.body, UP, buff=LARGE_BUFF) + + self.play( + VFadeIn(clock), + ClockPassesTime( + clock, + run_time=5, + hours_passed=5, + ), + ) + i_pi.set_status("R") + self.wait(1) + self.play(Blink(i_pi.body)) + self.play(FadeOut(clock)) + + # Removed + removed = TextMobject("Removed") + removed.match_color(words[2]) + removed.match_height(words[2]) + removed.move_to(words[2], DL) + + self.play( + FadeOutAndShift(words[2], UP), + FadeInFrom(removed, DOWN), + ) + self.play( + i_pi.body.change, 'pleading', removed, + ) + self.play(Blink(i_pi.body)) + self.wait() + + +class SubtlePangolin(Scene): + def construct(self): + pangolin = SVGMobject(file_name="pangolin") + pangolin.set_height(0.5) + pangolin.set_fill(GREY_BROWN, opacity=0) + pangolin.set_stroke(GREY_BROWN, width=1) + self.play(ShowCreationThenFadeOut(pangolin)) + self.play(FadeOut(pangolin)) + + self.embed() + + +class DoubleRadiusInGroup(Scene): + def construct(self): + radius = 1 + + pis = VGroup(*[ + PiPerson( + height=0.5, + max_speed=0, + wander_step_size=0, + infection_radius=4 * radius, + ) + for x in range(49) + ]) + pis.arrange_in_grid() + pis.set_height(FRAME_HEIGHT - 1) + sicky = pis[24] + sicky.set_status("I") + + circle = Circle(radius=radius) + circle.move_to(sicky) + dashed_circle = DashedVMobject(circle, num_dashes=30) + dashed_circle2 = dashed_circle.copy() + dashed_circle2.scale(2) + + self.add(pis) + self.play(ShowCreation(dashed_circle, lag_ratio=0)) + self.play(ShowCreation(dashed_circle2, lag_ratio=0)) + anims = [] + for pi in pis: + if pi.status == "S": + anims.append(ApplyMethod( + pi.body.change, "pleading", sicky.body.eyes + )) + random.shuffle(anims) + self.play(LaggedStart(*anims)) + self.wait(10) + + +class CutPInfectionInHalf(Scene): + def construct(self): + # Add people + sicky = PiPerson( + height=1, + infection_radius=4, + max_speed=0, + wander_step_size=0, + ) + normy = sicky.deepcopy() + normy.next_to(sicky, RIGHT) + normy.body.look_at(sicky.body.eyes) + + circ = Circle(radius=4) + d_circ = DashedVMobject(circ, num_dashes=30) + d_circ.set_color(RED) + d_circ.move_to(sicky) + + sicky.set_status("I") + self.add(sicky, normy) + self.add(d_circ) + self.play(d_circ.scale, 0.5) + self.wait() + + # Prob label + eq = VGroup( + TexMobject("P(\\text{Infection}) = "), + DecimalNumber(0.2), + ) + eq.arrange(RIGHT, buff=0.2) + eq.to_edge(UP) + + arrow = Vector(0.5 * RIGHT) + arrow.next_to(eq, RIGHT) + new_rhs = eq[1].copy() + new_rhs.next_to(arrow, RIGHT) + new_rhs.set_color(YELLOW) + + self.play(FadeIn(eq)) + self.play( + TransformFromCopy(eq[1], new_rhs), + GrowArrow(arrow) + ) + self.play(ChangeDecimalToValue(new_rhs, 0.1)) + self.wait(2) + + # Each day + clock = Clock() + clock.set_height(1) + clock.next_to(normy, UR, buff=0.7) + + def get_clock_run(clock): + return ClockPassesTime( + clock, + hours_passed=1, + run_time=1, + ) + + self.play( + VFadeIn(clock), + get_clock_run(clock), + ) + + # Random choice + choices = VGroup() + for x in range(9): + choices.add(TexMobject(CMARK_TEX, color=BLUE)) + for x in range(1): + choices.add(TexMobject(XMARK_TEX, color=RED)) + choices.arrange(DOWN) + choices.set_height(3) + choices.next_to(clock, DOWN) + + rect = SurroundingRectangle(choices[0]) + self.add(choices, rect) + + def show_random_choice(scene, rect, choices): + for x in range(10): + rect.move_to(random.choice(choices[:-1])) + scene.wait(0.1) + + show_random_choice(self, rect, choices) + + for x in range(6): + self.play(get_clock_run(clock)) + show_random_choice(self, rect, choices) + rect.move_to(choices[-1]) + normy.set_status("I") + self.add(normy) + self.play( + FadeOut(clock), + FadeOut(choices), + FadeOut(rect), + ) + self.wait(4) + + +class KeyTakeaways(Scene): + def construct(self): + takeaways = VGroup(*[ + TextMobject( + text, + alignment="", + ) + for text in [ + """ + Changes in how many people slip through the tests\\\\ + cause disproportionately large changes to the total\\\\ + number of people infected. + """, + """ + Social distancing slows the spread, but\\\\ + even small imperfections prolong it. + """, + """ + Reducing transit between communities, late in the game,\\\\ + has a very limited effect. + """, + """ + Shared central locations dramatically speed up\\\\ + the spread. + """, + ] + ]) + takeaway = TextMobject( + "The growth rate is very sensitive to:\\\\", + "- \\# Daily interactions\\\\", + "- Probability of infection\\\\", + "- Duration of illness\\\\", + alignment="", + ) + takeaway[1].set_color(GREEN_D) + takeaway[2].set_color(GREEN_C) + takeaway[3].set_color(GREEN_B) + takeaway.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + takeaways.add_to_back(takeaway) + + takeaways.scale(1.25) + + titles = VGroup() + for i in range(len(takeaways)): + title = TextMobject("Key takeaway \\#") + num = Integer(i + 1) + num.next_to(title, RIGHT, buff=SMALL_BUFF) + title.add(num) + titles.add(title) + + titles.arrange(DOWN, buff=LARGE_BUFF, aligned_edge=LEFT) + titles.to_edge(LEFT) + titles.set_color(GREY_D) + + h_line = Line(LEFT, RIGHT) + h_line.set_width(FRAME_WIDTH - 1) + h_line.to_edge(UP, buff=1.5) + + for takeaway in takeaways: + takeaway.next_to(h_line, DOWN, buff=0.75) + max_width = FRAME_WIDTH - 2 + if takeaway.get_width() > max_width: + takeaway.set_width(max_width) + + takeaways[3].shift(0.25 * UP) + + self.add(titles) + self.wait() + + for title, takeaway in zip(titles, takeaways): + other_titles = VGroup(*titles) + other_titles.remove(title) + self.play(title.set_color, WHITE, run_time=0.5) + title.save_state() + temp_h_line = h_line.copy() + self.play( + title.scale, 1.5, + title.to_edge, UP, + title.set_x, 0, + title.set_color, YELLOW, + FadeOut(other_titles), + ShowCreation(temp_h_line), + ) + + self.play(FadeIn(takeaway, lag_ratio=0.1, run_time=2, rate_func=linear)) + self.wait(2) + temp_h_line.rotate(PI) + self.play( + Restore(title), + FadeIn(other_titles), + Uncreate(temp_h_line), + FadeOutAndShift(takeaway, DOWN, lag_ratio=0.25 / len(takeaway.family_members_with_points())) + ) + self.wait() + + self.embed() + + +class AsymptomaticCases(Scene): + def construct(self): + pis = VGroup(*[ + PiPerson( + height=1, + infection_radius=2, + wander_step_size=0, + max_speed=0, + ) + for x in range(5) + ]) + pis.arrange(RIGHT, buff=2) + pis.to_edge(DOWN, buff=2) + + sneaky = pis[1] + sneaky.p_symptomatic_on_infection = 0 + + self.add(pis) + + for pi in pis: + if pi is sneaky: + pi.color_map["I"] = YELLOW + pi.mode_map["I"] = "coin_flip_1" + else: + pi.color_map["I"] = RED + pi.mode_map["I"] = "sick" + pi.unlock_triangulation() + pi.set_status("I") + self.wait(0.1) + self.wait(2) + + label = TextMobject("Never isolated") + label.set_height(0.8) + label.to_edge(UP) + label.set_color(YELLOW) + + arrow = Arrow( + label.get_bottom(), + sneaky.body.get_top(), + buff=0.5, + max_tip_length_to_length_ratio=0.5, + stroke_width=6, + max_stroke_width_to_length_ratio=10, + ) + + self.play( + FadeInFromDown(label), + GrowArrow(arrow), + ) + self.wait(13) + + +class WeDontHaveThat(TeacherStudentsScene): + def construct(self): + self.student_says( + "But we don't\\\\have that!", + target_mode="angry", + added_anims=[self.teacher.change, "guilty"] + ) + self.change_all_student_modes( + "angry", + look_at_arg=self.teacher.eyes + ) + self.wait(5) + + +class IntroduceSocialDistancing(Scene): + def construct(self): + pis = VGroup(*[ + PiPerson( + height=2, + wander_step_size=0, + gravity_well=None, + social_distance_color_threshold=5, + max_social_distance_stroke_width=10, + dl_bound=[-FRAME_WIDTH / 2 + 1, -2], + ur_bound=[FRAME_WIDTH / 2 - 1, 2], + ) + for x in range(3) + ]) + pis.arrange(RIGHT, buff=0.25) + pis.move_to(DOWN) + pi1, pi2, pi3 = pis + + slider = ValueSlider( + "Social distance factor", + 0, + x_min=0, + x_max=5, + tick_frequency=1, + numbers_to_show=range(6), + marker_color=YELLOW, + ) + slider.center() + slider.to_edge(UP) + self.add(slider) + + def update_pi(pi): + pi.social_distance_factor = 4 * slider.p2n(slider.marker.get_center()) + + for pi in pis: + pi.add_updater(update_pi) + pi.repulsion_points = [ + pi2.get_center() + for pi2 in pis + if pi2 is not pi + ] + + self.add(pis) + self.play( + FadeIn(slider), + *[ + ApplyMethod(pi1.body.look_at, pi2.body.eyes) + for pi1, pi2 in zip(pis, [*pis[1:], pis[0]]) + ] + ) + self.add(*pis) + self.wait() + self.play(slider.get_change_anim(3)) + self.wait(4) + + for i, vect in (0, RIGHT), (2, LEFT): + pis.suspend_updating() + pis[1].generate_target() + pis[1].target.next_to(pis[i], vect, SMALL_BUFF) + pis[1].target.body.look_at(pis[i].body.eyes) + self.play( + MoveToTarget(pis[1]), + path_arc=PI, + ) + pis.resume_updating() + self.wait(5) + self.wait(5) + + self.embed() + + +class FastForwardBy2(Scene): + CONFIG = { + "n": 2, + } + + def construct(self): + n = self.n + triangles = VGroup(*[ + ArrowTip(start_angle=0) + for x in range(n) + ]) + triangles.arrange(RIGHT, buff=0.01) + + label = VGroup(TexMobject("\\times"), Integer(n)) + label.set_height(0.4) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.next_to(triangles, RIGHT, buff=SMALL_BUFF) + + for mob in triangles, label: + mob.set_color(GREY_A) + mob.set_stroke(BLACK, 4, background=True) + + self.play( + LaggedStartMap( + FadeInFrom, triangles, + lambda m: (m, 0.4 * LEFT), + ), + FadeInFrom(label, 0.2 * LEFT), + run_time=1, + ) + self.play( + FadeOut(label), + FadeOut(triangles), + ) + + +class FastForwardBy4(FastForwardBy2): + CONFIG = { + "n": 4, + } + + +class DontLetThisHappen(Scene): + def construct(self): + text = TextMobject("Don't let\\\\this happen!") + text.scale(1.5) + text.set_stroke(BLACK, 5, background=True) + arrow = Arrow( + text.get_top(), + text.get_top() + 2 * UR + 0.5 * RIGHT, + path_arc=-120 * DEGREES, + ) + + self.add(text) + self.play( + Write(text, run_time=1), + ShowCreation(arrow), + ) + self.wait() + + +class ThatsNotHowIBehave(TeacherStudentsScene): + def construct(self): + self.student_says( + "That's...not\\\\how I behave.", + target_mode="sassy", + look_at_arg=self.screen, + ) + self.play( + self.teacher.change, "guilty", + self.get_student_changes("erm", "erm", "sassy") + ) + self.look_at(self.screen) + self.wait(20) + + +class BetweenNothingAndQuarantineWrapper(Scene): + def construct(self): + self.add(FullScreenFadeRectangle( + fill_color=GREY_E, + fill_opacity=1, + )) + rects = VGroup(*[ + ScreenRectangle() + for x in range(3) + ]) + rects.arrange(RIGHT) + rects.set_width(FRAME_WIDTH - 1) + self.add(rects) + + rects.set_fill(BLACK, 1) + rects.set_stroke(WHITE, 2) + + titles = VGroup( + TextMobject("Do nothing"), + TextMobject("Quarantine\\\\", "80$\\%$ of cases"), + TextMobject("Quarantine\\\\", "all cases"), + ) + fix_percent(titles[1][1][2]) + for title, rect in zip(titles, rects): + title.next_to(rect, UP) + + q_marks = TexMobject("???") + q_marks.scale(2) + q_marks.move_to(rects[1]) + + self.add(rects) + self.play(LaggedStartMap( + FadeInFromDown, + VGroup(titles[0], titles[2], titles[1]), + lag_ratio=0.3, + )) + self.play(Write(q_marks)) + self.wait() + + +class DarkerInterpretation(Scene): + def construct(self): + qz = TextMobject("Quarantine zone") + qz.set_color(RED) + qz.set_width(2) + line = Line(qz.get_left(), qz.get_right()) + + new_words = TextMobject("Deceased") + new_words.replace(qz, dim_to_match=1) + new_words.set_color(RED) + + self.add(qz) + self.wait() + self.play(ShowCreation(line)) + self.play( + FadeOut(qz), + FadeOut(line), + FadeIn(new_words) + ) + self.wait() + + +class SARS2002(TeacherStudentsScene): + def construct(self): + image = ImageMobject("sars_icon") + image.set_height(3.5) + image.move_to(self.hold_up_spot, DR) + image.shift(RIGHT) + + name = TextMobject("2002 SARS Outbreak") + name.next_to(image, LEFT, MED_LARGE_BUFF, aligned_edge=UP) + + n_cases = VGroup( + Integer(0, edge_to_fix=UR), + TextMobject("total cases") + ) + n_cases.arrange(RIGHT) + n_cases.scale(1.25) + n_cases.next_to(name, DOWN, buff=2) + + arrow = Arrow(name.get_bottom(), n_cases.get_top()) + + n_cases.shift(MED_SMALL_BUFF * RIGHT) + + self.play( + self.teacher.change, "raise_right_hand", + FadeInFrom(image, DOWN, run_time=2), + self.get_student_changes( + "pondering", "thinking", "pondering", + look_at_arg=image, + ) + ) + self.play( + FadeInFrom(name, RIGHT), + ) + self.play( + GrowArrow(arrow), + UpdateFromAlphaFunc( + n_cases, + lambda m, a: m.set_opacity(a), + ), + ChangeDecimalToValue(n_cases[0], 8098), + self.get_student_changes(look_at_arg=n_cases), + ) + self.wait() + self.change_all_student_modes( + "thinking", look_at_arg=n_cases, + ) + self.play(self.teacher.change, "tease") + self.wait(6) + + self.embed() + + +class QuarteringLines(Scene): + def construct(self): + lines = VGroup( + Line(UP, DOWN), + Line(LEFT, RIGHT), + ) + lines.set_width(FRAME_WIDTH) + lines.set_height(FRAME_HEIGHT, stretch=True) + lines.set_stroke(WHITE, 3) + self.play(ShowCreation(lines)) + self.wait() + + +class Eradicated(Scene): + def construct(self): + word = TextMobject("Eradicated") + word.set_color(GREEN) + self.add(word) + + +class LeftArrow(Scene): + def construct(self): + arrow = Vector(2 * LEFT) + self.play(GrowArrow(arrow)) + self.wait() + self.play(FadeOut(arrow)) + + +class IndicationArrow(Scene): + def construct(self): + vect = Vector( + 0.5 * DR, + max_tip_length_to_length_ratio=0.4, + max_stroke_width_to_length_ratio=10, + stroke_width=5, + ) + vect.set_color(YELLOW) + self.play(GrowArrow(vect)) + self.play(FadeOut(vect)) + + +class REq(Scene): + def construct(self): + mob = TexMobject("R_0 = ")[0] + mob[1].set_color(BLACK) + mob[2].shift(mob[1].get_width() * LEFT * 0.7) + self.add(mob) + + +class IntroduceR0(Scene): + def construct(self): + # Infect + pis = VGroup(*[ + PiPerson( + height=0.5, + infection_radius=1.5, + wander_step_size=0, + max_speed=0, + ) + for x in range(5) + ]) + + pis[:4].arrange(RIGHT, buff=2) + pis[:4].to_edge(DOWN, buff=2) + sicky = pis[4] + sicky.move_to(2 * UP) + sicky.set_status("I") + + pis[1].set_status("R") + for anim in pis[1].change_anims: + pis[1].pop_anim(anim) + + count = VGroup( + TextMobject("Infection count: "), + Integer(0) + ) + count.arrange(RIGHT, aligned_edge=DOWN) + count.to_corner(UL) + + self.add(pis) + self.add(count) + self.wait(2) + + for pi in pis[:4]: + point = VectorizedPoint(sicky.get_center()) + self.play( + point.move_to, pi.get_right() + 0.25 * RIGHT, + MaintainPositionRelativeTo(sicky, point), + run_time=0.5, + ) + if pi.status == "S": + count[1].increment_value() + count[1].set_color(WHITE) + pi.set_status("I") + self.play( + Flash( + sicky.get_center(), + color=RED, + line_length=0.3, + flash_radius=0.7, + ), + count[1].set_color, RED, + ) + self.wait() + + # Attach count to sicky + self.play( + point.move_to, 2 * UP, + MaintainPositionRelativeTo(sicky, point), + ) + count_copy = count[1].copy() + self.play( + count_copy.next_to, sicky.body, UR, + count_copy.set_color, WHITE, + ) + self.wait() + + # Zeros + zeros = VGroup() + for pi in pis[:4]: + if pi.status == "I": + zero = Integer(0) + zero.next_to(pi.body, UR) + zeros.add(zero) + + # R label + R_label = TextMobject("Average :=", " $R$") + R_label.set_color_by_tex("R", YELLOW) + R_label.next_to(count, DOWN, buff=1.5) + + arrow = Arrow(R_label[0].get_top(), count.get_bottom()) + + self.play( + LaggedStartMap(FadeInFromDown, zeros), + GrowArrow(arrow), + FadeIn(R_label), + ) + + brace = Brace(R_label[1], DOWN) + name = TextMobject("``Effective reproductive number''") + name.set_color(YELLOW) + name.next_to(brace, DOWN) + name.to_edge(LEFT) + self.play( + GrowFromCenter(brace), + FadeInFrom(name, 0.5 * UP), + ) + self.wait(5) + + # R0 + brr = TextMobject("``Basic reproductive number''") + brr.set_color(TEAL) + brr.move_to(name, LEFT) + R0 = TexMobject("R_0") + R0.move_to(R_label[1], UL) + R0.set_color(TEAL) + + for pi in pis[:4]: + pi.set_status("S") + + self.play( + FadeOutAndShift(R_label[1], UP), + FadeOutAndShift(name, UP), + FadeInFrom(R0, DOWN), + FadeInFrom(brr, DOWN), + FadeOut(zeros), + FadeOut(count_copy), + brace.match_width, R0, {"stretch": True}, + brace.match_x, R0, + ) + self.wait() + + # Copied from above + count[1].set_value(0) + + for pi in pis[:4]: + point = VectorizedPoint(sicky.get_center()) + self.play( + point.move_to, pi.body.get_right() + 0.25 * RIGHT, + MaintainPositionRelativeTo(sicky, point), + run_time=0.5, + ) + count[1].increment_value() + count[1].set_color(WHITE) + pi.set_status("I") + self.play( + Flash( + sicky.get_center(), + color=RED, + line_length=0.3, + flash_radius=0.7, + ), + count[1].set_color, RED, + ) + self.play( + point.move_to, 2 * UP, + MaintainPositionRelativeTo(sicky, point), + ) + self.wait(4) + + +class HowR0IsCalculatedHere(Scene): + def construct(self): + words = VGroup( + TextMobject("Count ", "\\# transfers"), + TextMobject("for every infectious case") + ) + words.arrange(DOWN) + words[1].set_color(RED) + words.to_edge(UP) + + estimate = TextMobject("Estimate") + estimate.move_to(words[0][0], RIGHT) + + lp, rp = parens = TexMobject("(", ")") + parens.match_height(words) + lp.next_to(words, LEFT, SMALL_BUFF) + rp.next_to(words, RIGHT, SMALL_BUFF) + average = TextMobject("Average") + average.scale(1.5) + average.next_to(parens, LEFT, SMALL_BUFF) + + words.save_state() + words[1].move_to(words[0]) + words[0].set_opacity(0) + + self.add(FullScreenFadeRectangle(fill_color=GREY_E, fill_opacity=1).scale(2)) + self.wait() + self.play(Write(words[1], run_time=1)) + self.wait() + self.add(words) + self.play(Restore(words)) + self.wait() + self.play( + FadeOutAndShift(words[0][0], UP), + FadeInFrom(estimate, DOWN), + ) + self.wait() + + self.play( + Write(parens), + FadeInFrom(average, 0.5 * RIGHT), + self.camera.frame.shift, LEFT, + ) + self.wait() + + self.embed() + + +class R0Rect(Scene): + def construct(self): + rect = SurroundingRectangle(TextMobject("R0 = 2.20")) + rect.set_stroke(YELLOW, 4) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + + +class DoubleInfectionRadius(Scene): + CONFIG = { + "random_seed": 1, + } + + def construct(self): + c1 = Circle(radius=0.25, color=RED) + c2 = Circle(radius=0.5, color=RED) + arrow = Vector(RIGHT) + c1.next_to(arrow, LEFT) + c2.next_to(arrow, RIGHT) + + title = TextMobject("Double the\\\\infection radius") + title.next_to(VGroup(c1, c2), UP) + + self.add(c1, title) + self.wait() + self.play( + GrowArrow(arrow), + TransformFromCopy(c1, c2), + ) + self.wait() + + c2.label = TextMobject("4x area") + c2.label.scale(0.5) + c2.label.next_to(c2, DOWN) + + for circ, count in (c1, 4), (c2, 16): + dots = VGroup() + for x in range(count): + dot = Dot(color=BLUE) + dot.set_stroke(BLACK, 2, background=True) + dot.set_height(0.05) + vect = rotate_vector(RIGHT, TAU * random.random()) + vect *= 0.9 * random.random() * circ.get_height() / 2 + dot.move_to(circ.get_center() + vect) + dots.add(dot) + circ.dot = dots + anims = [ShowIncreasingSubsets(dots)] + if hasattr(circ, "label"): + anims.append(FadeInFrom(circ.label, 0.5 * UP)) + self.play(*anims) + self.wait() + + +class PInfectionSlider(Scene): + def construct(self): + slider = ValueSlider( + "Probability of infection", + 0.2, + x_min=0, + x_max=0.2, + numbers_to_show=np.arange(0.05, 0.25, 0.05), + decimal_number_config={ + "num_decimal_places": 2, + }, + tick_frequency=0.05, + ) + self.add(slider) + self.wait() + self.play(slider.get_change_anim(0.1)) + self.wait() + self.play(slider.get_change_anim(0.05)) + self.wait() + + +class R0Categories(Scene): + def construct(self): + # Titles + titles = VGroup( + TexMobject("R > 1", color=RED), + TexMobject("R = 1", color=YELLOW), + TexMobject("R < 1", color=GREEN), + ) + titles.scale(1.25) + titles.arrange(RIGHT, buff=2.7) + titles.to_edge(UP) + + v_lines = VGroup() + for t1, t2 in zip(titles, titles[1:]): + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.set_x(midpoint(t1.get_right(), t2.get_left())[0]) + v_lines.add(v_line) + + self.add(titles) + self.add(v_lines) + + # Names + names = VGroup( + TextMobject("Epidemic", color=RED), + TextMobject("Endemic", color=YELLOW), + TextMobject("...Hypodemic?", color=GREEN), + ) + for name, title in zip(names, titles): + name.next_to(title, DOWN, MED_LARGE_BUFF) + + # Doubling dots + dot = Dot(color=RED) + dot.next_to(names[0], DOWN, LARGE_BUFF) + rows = VGroup(VGroup(dot)) + lines = VGroup() + for x in range(4): + new_row = VGroup() + new_lines = VGroup() + for dot in rows[-1]: + dot.children = [dot.copy(), dot.copy()] + new_row.add(*dot.children) + new_row.arrange(RIGHT) + max_width = 4 + if new_row.get_width() > max_width: + new_row.set_width(max_width) + new_row.next_to(rows[-1], DOWN, LARGE_BUFF) + for dot in rows[-1]: + for child in dot.children: + new_lines.add(Line( + dot.get_center(), + child.get_center(), + buff=0.2, + )) + rows.add(new_row) + lines.add(new_lines) + + for row, line_row in zip(rows[:-1], lines): + self.add(row) + anims = [] + for dot in row: + for child in dot.children: + anims.append(TransformFromCopy(dot, child)) + for line in line_row: + anims.append(ShowCreation(line)) + self.play(*anims) + self.play(FadeInFrom(names[0], UP)) + self.wait() + + exp_tree = VGroup(rows, lines) + + # Singleton dots + mid_rows = VGroup() + mid_lines = VGroup() + + for row in rows: + dot = Dot(color=RED) + dot.match_y(row) + mid_rows.add(dot) + + for d1, d2 in zip(mid_rows[:-1], mid_rows[1:]): + d1.child = d2 + mid_lines.add(Line( + d1.get_center(), + d2.get_center(), + buff=0.2, + )) + + for dot, line in zip(mid_rows[:-1], mid_lines): + self.add(dot) + self.play( + TransformFromCopy(dot, dot.child), + ShowCreation(line) + ) + self.play(FadeInFrom(names[1], UP)) + self.wait() + + # Inverted tree + exp_tree_copy = exp_tree.copy() + exp_tree_copy.rotate(PI) + exp_tree_copy.match_x(titles[2]) + + self.play(TransformFromCopy(exp_tree, exp_tree_copy)) + self.play(FadeInFrom(names[2], UP)) + self.wait() + + +class RealR0Estimates(Scene): + def construct(self): + labels = VGroup( + TextMobject("COVID-19\\\\", "$R_0 \\approx 2$"), + TextMobject("1918 Spanish flu\\\\", "$R_0 \\approx 2$"), + TextMobject("Usual seasonal flu\\\\", "$R_0 \\approx 1.3$"), + ) + images = Group( + ImageMobject("COVID-19_Map"), + ImageMobject("spanish_flu"), + ImageMobject("Influenza"), + ) + for image in images: + image.set_height(3) + + images.arrange(RIGHT, buff=0.5) + images.to_edge(UP, buff=2) + + for label, image in zip(labels, images): + label.next_to(image, DOWN) + + for label, image in zip(labels, images): + self.play( + FadeInFrom(image, DOWN), + FadeInFrom(label, UP), + ) + self.wait() + + self.embed() + + +class WhyChooseJustOne(TeacherStudentsScene): + def construct(self): + self.student_says( + "Why choose\\\\just one?", + target_mode="sassy", + added_anims=[self.teacher.change, "thinking"], + run_time=1, + ) + self.play( + self.get_student_changes( + "confused", "confused", "sassy", + ), + ) + self.wait(2) + self.teacher_says( + "For science!", target_mode="hooray", + look_at_arg=self.students[2].eyes, + ) + self.change_student_modes("hesitant", "hesitant", "hesitant") + self.wait(3) + + self.embed() + + +class NickyCaseMention(Scene): + def construct(self): + words = TextMobject( + "Artwork by Nicky Case\\\\", + "...who is awesome." + ) + words.scale(1) + words.to_edge(LEFT) + arrow = Arrow( + words.get_top(), + words.get_top() + 2 * UR + RIGHT, + path_arc=-90 * DEGREES, + ) + self.play( + Write(words[0]), + ShowCreation(arrow), + run_time=1, + ) + self.wait(2) + self.play(Write(words[1]), run_time=1) + self.wait() + + +class PonderingRandy(Scene): + def construct(self): + randy = Randolph() + self.play(FadeIn(randy)) + self.play(randy.change, "pondering") + for x in range(3): + self.play(Blink(randy)) + self.wait(2) + + +class WideSpreadTesting(Scene): + def construct(self): + # Add dots + dots = VGroup(*[ + DotPerson( + height=0.2, + infection_radius=0.6, + max_speed=0, + wander_step_size=0, + p_symptomatic_on_infection=0.8, + ) + for x in range(600) + ]) + dots.arrange_in_grid(20, 30) + dots.set_height(FRAME_HEIGHT - 1) + + self.add(dots) + sick_dots = VGroup() + for x in range(36): + sicky = random.choice(dots) + sicky.set_status("I") + sick_dots.add(sicky) + self.wait(0.1) + self.wait(2) + + healthy_dots = VGroup() + for dot in dots: + if dot.status != "I": + healthy_dots.add(dot) + + # Show Flash + rings = self.get_rings(ORIGIN, FRAME_WIDTH + FRAME_HEIGHT, 0.1) + rings.shift(7 * LEFT) + for i, ring in enumerate(rings): + ring.shift(0.05 * i**1.2 * RIGHT) + + self.play(LaggedStartMap( + FadeIn, rings, + lag_ratio=3 / len(rings), + run_time=2.5, + rate_func=there_and_back, + )) + + # Quarantine + box = Square() + box.set_height(2) + box.to_corner(DL) + box.shift(LEFT) + + anims = [] + points = VGroup() + points_target = VGroup() + for dot in sick_dots: + point = VectorizedPoint(dot.get_center()) + point.generate_target() + points.add(point) + points_target.add(point.target) + + dot.push_anim(MaintainPositionRelativeTo(dot, point, run_time=3)) + anims.append(MoveToTarget(point)) + + points_target.arrange_in_grid() + points_target.set_width(box.get_width() - 1) + points_target.move_to(box) + + self.play( + ShowCreation(box), + LaggedStartMap(MoveToTarget, points, lag_ratio=0.05), + self.camera.frame.shift, LEFT, + run_time=3, + ) + self.wait(9) + + def get_rings(self, center, max_radius, delta_r): + radii = np.arange(0, max_radius, delta_r) + rings = VGroup(*[ + Circle( + radius=r, + stroke_opacity=0.75 * (1 - fdiv(r, max_radius)), + stroke_color=TEAL, + stroke_width=100 * delta_r, + ) + for r in radii + ]) + rings.move_to(center) + return rings + + +class VirusSpreading(Scene): + def construct(self): + virus = SVGMobject(file_name="virus") + virus.lock_triangulation() + virus.set_fill(RED_E, 1) + virus.set_stroke([RED, WHITE], width=0) + height = 3 + virus.set_height(height) + + self.play(DrawBorderThenFill(virus)) + + viruses = VGroup(virus) + + for x in range(8): + height *= 0.8 + anims = [] + new_viruses = VGroup() + for virus in viruses: + children = [virus.copy(), virus.copy()] + for child in children: + child.set_height(height) + child.set_color(interpolate_color( + RED_E, + GREY_D, + 0.7 * random.random(), + )) + child.shift([ + (random.random() - 0.5) * 3, + (random.random() - 0.5) * 3, + 0, + ]) + anims.append(TransformFromCopy(virus, child)) + new_viruses.add(child) + new_viruses.center() + self.remove(viruses) + self.play(*anims, run_time=0.5) + viruses.set_submobjects(list(new_viruses)) + self.wait() + + # Eliminate + for virus in viruses: + virus.generate_target() + virus.target.scale(3) + virus.target.set_color(WHITE) + virus.target.set_opacity(0) + + self.play(LaggedStartMap( + MoveToTarget, viruses, + run_time=8, + lag_ratio=3 / len(viruses) + )) + + +class GraciousPi(Scene): + def construct(self): + morty = Mortimer() + morty.flip() + self.play(FadeIn(morty)) + self.play(morty.change, "hesitant") + self.play(Blink(morty)) + self.wait() + self.play(morty.change, "gracious", morty.get_bottom()) + self.play(Blink(morty)) + self.wait(2) + self.play(Blink(morty)) + self.wait(2) + + +class SIREndScreen(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", + "Ashwin Siddarth", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Ben Delo", + "Bernd Sing", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "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", + "Dan Herbatschek", + "Daniel Herrera C", + "Dave B", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Eddie Landesberg", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Farzaneh Sarafraz", + "Federico Lebron", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Goodwine Carlos", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jake Vartuli - Schonberg", + "Jalex Stark", + "Jameel Syed", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Linse", + "Jeff Straathof", + "Jimmy Yang", + "John C. Vesey", + "John Camp", + "John Haley", + "John Le", + "John Rizzo", + "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", + "Lael S Costa", + "LAI Oscar", + "Lambda GPU Workstations", + "Lee Redden", + "Linh Tran", + "lol I totally forgot I had a patreon", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Magister Mugit", + "Magnus Dahlström", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark B Bahu", + "Mark Heising", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Maxim Nitsche", + "Mia Parent", + "Michael Hardel", + "Michael W White", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nate Heckmann", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Pavel Dubov", + "Petar Veličković", + "Peter Ehrnstrom", + "Peter Mcinerney", + "Pierre Lancien", + "Pradeep Gollakota", + "Quantopian", + "Rafael Bove Barrios", + "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.", + "soekul", + "Solara570", + "Steve Huynh", + "Steve Sperandeo", + "Steven Siddals", + "Stevie Metke", + "Still working on an upcoming skeptical humanist SciFi novels- Elux Luc", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tyler VanValkenburg", + "Vassili Philippov", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + ], + } + + +# For assembling script +# SCENES_IN_ORDER = [ +WILL_BE_SCENES_IN_ORDER = [ + # Quarantining + QuarantineInfectious, + QuarantineInfectiousLarger, + QuarantineInfectiousLarger80p, + QuarantineInfectiousTravel, + QuarantineInfectiousTravel80p, + QuarantineInfectiousTravel50p, + # Social distancing + SimpleSocialDistancing, + DelayedSocialDistancing, # Maybe remove? + DelayedSocialDistancingLargeCity, # Probably don't use + SimpleTravelSocialDistancePlusZeroTravel, + SimpleTravelDelayedSocialDistancing99p, + SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent, + SimpleTravelDelayedSocialDistancing50pThreshold100, + SimpleTravelDelayedTravelReductionThreshold100, # Maybe don't use + # Describing R0 + LargerCity2, + LargeCityHighInfectionRadius, + LargeCityLowerInfectionRate, + SimpleTravelDelayedSocialDistancing99p, # Need to re-render + # Market + CentralMarketLargePopulation, + CentralMarketLowerInfection, + CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing, + CentralMarketLessFrequent, + CentralMarketTransitionToLowerInfection, + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + CentralMarketQuarantine, + CentralMarketQuarantine80p, +] + + +to_run_later = [ + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + SimpleTravelDelayedTravelReduction, + CentralMarketLargePopulation, + CentralMarketLowerInfection, + CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing, + CentralMarketLessFrequent, + CentralMarketTransitionToLowerInfection, + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + CentralMarketQuarantine, + CentralMarketQuarantine80p, + SecondWave, +]