mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
3366 lines
95 KiB
Python
3366 lines
95 KiB
Python
from manimlib.imports import *
|
|
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"
|
|
COLOR_MAP = {
|
|
"S": BLUE,
|
|
"I": RED,
|
|
"R": GREY_D,
|
|
}
|
|
|
|
|
|
def update_time(mob, dt):
|
|
mob.time += dt
|
|
|
|
|
|
class Person(VGroup):
|
|
CONFIG = {
|
|
"status": "S", # S, I or R
|
|
"height": 0.2,
|
|
"color_map": COLOR_MAP,
|
|
"infection_ring_style": {
|
|
"stroke_color": RED,
|
|
"stroke_opacity": 0.8,
|
|
"stroke_width": 0,
|
|
},
|
|
"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):
|
|
super().__init__(**kwargs)
|
|
|
|
self.time = 0
|
|
self.last_step_change = -1
|
|
self.change_anims = []
|
|
self.velocity = np.zeros(3)
|
|
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)
|
|
self.add_body()
|
|
self.add_infection_ring()
|
|
self.set_status(self.status, run_time=0)
|
|
|
|
# Updaters
|
|
self.add_updater(update_time)
|
|
self.add_updater(lambda m, dt: m.update_position(dt))
|
|
self.add_updater(lambda m, dt: m.update_infection_ring(dt))
|
|
self.add_updater(lambda m: m.progress_through_change_anims())
|
|
|
|
def add_body(self):
|
|
body = self.get_body()
|
|
body.set_height(self.height)
|
|
body.move_to(self.get_center())
|
|
self.add(body)
|
|
self.body = body
|
|
|
|
def get_body(self, status):
|
|
person = SVGMobject(file_name="person")
|
|
person.set_stroke(width=0)
|
|
return person
|
|
|
|
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,
|
|
lambda m, a: m.set_color(interpolate_color(
|
|
start_color, end_color, a
|
|
)),
|
|
run_time=run_time,
|
|
)
|
|
]
|
|
for anim in anims:
|
|
self.push_anim(anim)
|
|
|
|
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)
|
|
return self
|
|
|
|
def pop_anim(self, anim):
|
|
anim.update(1)
|
|
anim.finish()
|
|
self.change_anims.remove(anim)
|
|
|
|
def add_infection_ring(self):
|
|
self.infection_ring = Circle(
|
|
radius=self.height / 2,
|
|
)
|
|
self.infection_ring.set_style(**self.infection_ring_style)
|
|
self.add(self.infection_ring)
|
|
self.infection_ring.time = 0
|
|
return self
|
|
|
|
def update_position(self, dt):
|
|
center = self.get_center()
|
|
total_force = np.zeros(3)
|
|
|
|
# Gravity
|
|
if self.wander_step_size != 0:
|
|
if (self.time - self.last_step_change) > self.wander_step_duration:
|
|
vect = rotate_vector(RIGHT, TAU * random.random())
|
|
self.gravity_well = center + self.wander_step_size * vect
|
|
self.last_step_change = self.time
|
|
|
|
if self.gravity_well is not None:
|
|
to_well = (self.gravity_well - center)
|
|
dist = get_norm(to_well)
|
|
if dist != 0:
|
|
total_force += self.gravity_strength * to_well / (dist**3)
|
|
|
|
# Potentially avoid neighbors
|
|
if self.social_distance_factor > 0:
|
|
repulsion_force = np.zeros(3)
|
|
min_dist = np.inf
|
|
for point in self.repulsion_points:
|
|
to_point = point - center
|
|
dist = get_norm(to_point)
|
|
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(
|
|
(sdct / min_dist) - sdct,
|
|
# 2 * (sdct / min_dist),
|
|
0,
|
|
self.max_social_distance_stroke_width
|
|
),
|
|
background=True,
|
|
)
|
|
total_force += repulsion_force
|
|
|
|
# Avoid walls
|
|
wall_force = np.zeros(3)
|
|
for i in range(2):
|
|
to_lower = center[i] - self.dl_bound[i]
|
|
to_upper = self.ur_bound[i] - center[i]
|
|
|
|
# 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 / 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
|
|
self.velocity += total_force * dt
|
|
|
|
# Limit speed
|
|
speed = get_norm(self.velocity)
|
|
if speed > self.max_speed:
|
|
self.velocity *= self.max_speed / speed
|
|
|
|
# Update position
|
|
self.shift(self.velocity * dt)
|
|
|
|
def update_infection_ring(self, dt):
|
|
ring = self.infection_ring
|
|
if not (self.infection_start_time <= self.time <= self.infection_end_time + 1):
|
|
return self
|
|
|
|
ring_time = self.time - self.infection_start_time
|
|
period = self.infection_animation_period
|
|
|
|
alpha = (ring_time % period) / period
|
|
ring.set_height(interpolate(
|
|
self.height,
|
|
self.infection_radius,
|
|
smooth(alpha),
|
|
))
|
|
ring.set_stroke(
|
|
width=interpolate(
|
|
0, 5,
|
|
there_and_back(alpha),
|
|
),
|
|
opacity=min([
|
|
min([ring_time, 1]),
|
|
min([self.infection_end_time + 1 - self.time, 1]),
|
|
]),
|
|
)
|
|
|
|
return self
|
|
|
|
def progress_through_change_anims(self):
|
|
for anim in self.change_anims:
|
|
if anim.run_time == 0:
|
|
alpha = 1
|
|
else:
|
|
alpha = (self.time - anim.start_time) / anim.run_time
|
|
anim.interpolate(alpha)
|
|
if alpha >= 1:
|
|
self.pop_anim(anim)
|
|
|
|
def get_center(self):
|
|
return self.center_point.points[0]
|
|
|
|
|
|
class DotPerson(Person):
|
|
def get_body(self):
|
|
return Dot()
|
|
|
|
|
|
class PiPerson(Person):
|
|
CONFIG = {
|
|
"mode_map": {
|
|
"S": "guilty",
|
|
"I": "sick",
|
|
"R": "tease",
|
|
}
|
|
}
|
|
|
|
def get_body(self):
|
|
return Randolph()
|
|
|
|
def set_status(self, status, run_time=1):
|
|
super().set_status(status)
|
|
|
|
target = self.body.copy()
|
|
target.change(self.mode_map[status])
|
|
target.set_color(self.color_map[status])
|
|
|
|
transform = Transform(self.body, target)
|
|
transform.begin()
|
|
|
|
def update(body, alpha):
|
|
transform.update(alpha)
|
|
body.move_to(self.center_point)
|
|
|
|
anims = [
|
|
UpdateFromAlphaFunc(self.body, update, run_time=run_time),
|
|
]
|
|
for anim in anims:
|
|
self.push_anim(anim)
|
|
|
|
return self
|
|
|
|
|
|
class SIRSimulation(VGroup):
|
|
CONFIG = {
|
|
"n_cities": 1,
|
|
"city_population": 100,
|
|
"box_size": 7,
|
|
"person_type": PiPerson,
|
|
"person_config": {
|
|
"height": 0.2,
|
|
"infection_radius": 0.6,
|
|
"gravity_strength": 1,
|
|
"wander_step_size": 1,
|
|
},
|
|
"p_infection_per_day": 0.2,
|
|
"infection_time": 5,
|
|
"travel_rate": 0,
|
|
"limit_social_distancing_to_infectious": False,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.time = 0
|
|
self.add_updater(update_time)
|
|
|
|
self.add_boxes()
|
|
self.add_people()
|
|
|
|
self.add_updater(lambda m, dt: m.update_statusses(dt))
|
|
|
|
def add_boxes(self):
|
|
boxes = VGroup()
|
|
for x in range(self.n_cities):
|
|
box = Square()
|
|
box.set_height(self.box_size)
|
|
box.set_stroke(WHITE, 3)
|
|
boxes.add(box)
|
|
boxes.arrange_in_grid(buff=LARGE_BUFF)
|
|
self.add(boxes)
|
|
self.boxes = boxes
|
|
|
|
def add_people(self):
|
|
people = 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,
|
|
ur_bound=ur_bound,
|
|
**self.person_config
|
|
)
|
|
person.move_to([
|
|
interpolate(lower, upper, random.random())
|
|
for lower, upper in zip(dl_bound, ur_bound)
|
|
])
|
|
person.box = box
|
|
box.people.add(person)
|
|
people.add(person)
|
|
|
|
# Choose a patient zero
|
|
random.choice(people).set_status("I")
|
|
self.add(people)
|
|
self.people = people
|
|
|
|
def update_statusses(self, dt):
|
|
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 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:
|
|
if (i_person.time - i_person.infection_start_time) > self.infection_time:
|
|
i_person.set_status("R")
|
|
|
|
# Travel
|
|
if self.travel_rate > 0:
|
|
path_func = path_along_arc(45 * DEGREES)
|
|
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(
|
|
person,
|
|
lambda m, a: m.move_to(path_func(
|
|
m.old_center, m.new_center, a,
|
|
)),
|
|
run_time=1,
|
|
)
|
|
person.push_anim(anim)
|
|
|
|
# Social distancing
|
|
centers = np.array([person.get_center() for person in self.people])
|
|
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([
|
|
len(list(filter(
|
|
lambda m: m.status == status,
|
|
self.people
|
|
)))
|
|
for status in "SIR"
|
|
])
|
|
|
|
def get_status_proportions(self):
|
|
counts = self.get_status_counts()
|
|
return counts / sum(counts)
|
|
|
|
|
|
class SIRGraph(VGroup):
|
|
CONFIG = {
|
|
"color_map": COLOR_MAP,
|
|
"height": 7,
|
|
"width": 5,
|
|
"update_frequency": 0.5,
|
|
"include_braces": False,
|
|
}
|
|
|
|
def __init__(self, simulation, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.simulation = simulation
|
|
self.data = [simulation.get_status_proportions()] * 2
|
|
self.add_axes()
|
|
self.add_graph()
|
|
self.add_x_labels()
|
|
|
|
self.time = 0
|
|
self.last_update_time = 0
|
|
self.add_updater(update_time)
|
|
self.add_updater(lambda m: m.update_graph())
|
|
self.add_updater(lambda m: m.update_x_labels())
|
|
|
|
def add_axes(self):
|
|
axes = Axes(
|
|
y_min=0,
|
|
y_max=1,
|
|
y_axis_config={
|
|
"tick_frequency": 0.1,
|
|
},
|
|
x_min=0,
|
|
x_max=1,
|
|
axis_config={
|
|
"include_tip": False,
|
|
},
|
|
)
|
|
origin = axes.c2p(0, 0)
|
|
axes.x_axis.set_width(self.width, about_point=origin, stretch=True)
|
|
axes.y_axis.set_height(self.height, about_point=origin, stretch=True)
|
|
|
|
self.add(axes)
|
|
self.axes = axes
|
|
|
|
def add_graph(self):
|
|
self.graph = self.get_graph(self.data)
|
|
self.add(self.graph)
|
|
|
|
def add_x_labels(self):
|
|
self.x_labels = VGroup()
|
|
self.x_ticks = VGroup()
|
|
self.add(self.x_ticks, self.x_labels)
|
|
|
|
def get_graph(self, data):
|
|
axes = self.axes
|
|
i_points = []
|
|
s_points = []
|
|
for x, props in zip(np.linspace(0, 1, len(data)), data):
|
|
i_point = axes.c2p(x, props[1])
|
|
s_point = axes.c2p(x, sum(props[:2]))
|
|
i_points.append(i_point)
|
|
s_points.append(s_point)
|
|
|
|
r_points = [
|
|
axes.c2p(0, 1),
|
|
axes.c2p(1, 1),
|
|
*s_points[::-1],
|
|
axes.c2p(0, 1),
|
|
]
|
|
s_points.extend([
|
|
*i_points[::-1],
|
|
s_points[0],
|
|
])
|
|
i_points.extend([
|
|
axes.c2p(1, 0),
|
|
axes.c2p(0, 0),
|
|
i_points[0],
|
|
])
|
|
|
|
points_lists = [s_points, i_points, r_points]
|
|
regions = VGroup(VMobject(), VMobject(), VMobject())
|
|
|
|
for region, status, points in zip(regions, "SIR", points_lists):
|
|
region.set_points_as_corners(points)
|
|
region.set_stroke(width=0)
|
|
region.set_fill(self.color_map[status], 1)
|
|
regions[0].set_fill(opacity=0.5)
|
|
|
|
return regions
|
|
|
|
def update_graph(self):
|
|
if (self.time - self.last_update_time) > self.update_frequency:
|
|
self.data.append(self.simulation.get_status_proportions())
|
|
self.graph.become(self.get_graph(self.data))
|
|
self.last_update_time = self.time
|
|
|
|
def update_x_labels(self):
|
|
tick_height = 0.03 * self.graph.get_height()
|
|
tick_template = Line(DOWN, UP)
|
|
tick_template.set_height(tick_height)
|
|
|
|
def get_tick(x):
|
|
tick = tick_template.copy()
|
|
tick.move_to(self.axes.c2p(x / self.time, 0))
|
|
return tick
|
|
|
|
def get_label(x, tick):
|
|
label = Integer(x)
|
|
label.set_height(tick_height)
|
|
label.next_to(tick, DOWN, buff=0.5 * tick_height)
|
|
return label
|
|
|
|
self.x_labels.set_submobjects([])
|
|
self.x_ticks.set_submobjects([])
|
|
|
|
if self.time < 15:
|
|
tick_range = range(1, int(self.time) + 1)
|
|
elif self.time < 50:
|
|
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)
|
|
label = get_label(x, tick)
|
|
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):
|
|
CONFIG = {
|
|
"update_frequency": 0.5,
|
|
}
|
|
|
|
def __init__(self, graph, simulation, **kwargs):
|
|
super().__init__(**kwargs)
|
|
axes = self.axes = graph.axes
|
|
self.simulation = simulation
|
|
|
|
ys = np.linspace(0, 1, 4)
|
|
self.lines = VGroup(*[
|
|
Line(axes.c2p(1, y1), axes.c2p(1, y2))
|
|
for y1, y2 in zip(ys, ys[1:])
|
|
])
|
|
self.braces = VGroup(*[Brace(line, RIGHT) for line in self.lines])
|
|
self.labels = VGroup(
|
|
TextMobject("Susceptible", color=COLOR_MAP["S"]),
|
|
TextMobject("Infectious", color=COLOR_MAP["I"]),
|
|
TextMobject("Removed", color=COLOR_MAP["R"]),
|
|
)
|
|
|
|
self.max_label_height = graph.get_height() * 0.05
|
|
|
|
self.add(self.braces, self.labels)
|
|
|
|
self.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:
|
|
return
|
|
|
|
self.last_update_time = self.time
|
|
lines = self.lines
|
|
braces = self.braces
|
|
labels = self.labels
|
|
axes = self.axes
|
|
|
|
props = self.simulation.get_status_proportions()
|
|
ys = np.cumsum([0, props[1], props[0], props[2]])
|
|
|
|
epsilon = 1e-6
|
|
for i, y1, y2 in zip([1, 0, 2], ys, ys[1:]):
|
|
lines[i].set_points_as_corners([
|
|
axes.c2p(1, y1),
|
|
axes.c2p(1, y2),
|
|
])
|
|
height = lines[i].get_height()
|
|
|
|
braces[i].set_height(
|
|
max(height, epsilon),
|
|
stretch=True
|
|
)
|
|
braces[i].next_to(lines[i], RIGHT)
|
|
label_height = clip(height, epsilon, self.max_label_height)
|
|
labels[i].scale(label_height / labels[i][0][0].get_height())
|
|
labels[i].next_to(braces[i], RIGHT)
|
|
return self
|
|
|
|
|
|
class ValueSlider(NumberLine):
|
|
CONFIG = {
|
|
"x_min": 0,
|
|
"x_max": 1,
|
|
"tick_frequency": 0.1,
|
|
"numbers_with_elongated_ticks": [],
|
|
"numbers_to_show": np.linspace(0, 1, 6),
|
|
"decimal_number_config": {
|
|
"num_decimal_places": 1,
|
|
},
|
|
"stroke_width": 5,
|
|
"width": 8,
|
|
"marker_color": BLUE,
|
|
}
|
|
|
|
def __init__(self, name, value, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.set_width(self.width, stretch=True)
|
|
self.add_numbers()
|
|
|
|
self.marker = ArrowTip(start_angle=-90 * DEGREES)
|
|
self.marker.move_to(self.n2p(value), DOWN)
|
|
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.name = TextMobject(name)
|
|
self.name.scale(1.25)
|
|
self.name.next_to(self, DOWN)
|
|
self.name.match_color(self.marker)
|
|
self.add(self.name)
|
|
|
|
def get_change_anim(self, new_value, **kwargs):
|
|
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)
|
|
|
|
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": {
|
|
"person_type": PiPerson,
|
|
"n_cities": 1,
|
|
"city_population": 100,
|
|
"person_config": {
|
|
"infection_radius": 0.75,
|
|
"social_distance_factor": 0,
|
|
"gravity_strength": 0.2,
|
|
"max_speed": 0.5,
|
|
},
|
|
"travel_rate": 0,
|
|
"infection_time": 5,
|
|
},
|
|
"graph_config": {
|
|
"update_frequency": 1 / 15,
|
|
},
|
|
"graph_height_to_frame_height": 0.5,
|
|
"graph_width_to_frame_height": 0.75,
|
|
"include_graph_braces": True,
|
|
}
|
|
|
|
def setup(self):
|
|
self.add_simulation()
|
|
self.position_camera()
|
|
self.add_graph()
|
|
self.add_sliders()
|
|
self.add_R_label()
|
|
self.add_total_cases_label()
|
|
|
|
def construct(self):
|
|
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)
|
|
self.add(self.simulation)
|
|
|
|
def position_camera(self):
|
|
frame = self.camera.frame
|
|
boxes = self.simulation.boxes
|
|
min_height = boxes.get_height() + 1
|
|
min_width = 3 * boxes.get_width()
|
|
if frame.get_height() < min_height:
|
|
frame.set_height(min_height)
|
|
if frame.get_width() < min_width:
|
|
frame.set_width(min_width)
|
|
|
|
frame.next_to(boxes.get_right(), LEFT, buff=-0.1 * boxes.get_width())
|
|
|
|
def add_graph(self):
|
|
frame = self.camera.frame
|
|
frame_height = frame.get_height()
|
|
graph = SIRGraph(
|
|
self.simulation,
|
|
height=self.graph_height_to_frame_height * frame_height,
|
|
width=self.graph_width_to_frame_height * frame_height,
|
|
**self.graph_config,
|
|
)
|
|
graph.move_to(frame, UL)
|
|
graph.shift(0.05 * DR * frame_height)
|
|
self.add(graph)
|
|
self.graph = graph
|
|
|
|
if self.include_graph_braces:
|
|
self.graph_braces = GraphBraces(
|
|
graph,
|
|
self.simulation,
|
|
update_frequency=graph.update_frequency
|
|
)
|
|
self.add(self.graph_braces)
|
|
|
|
def add_sliders(self):
|
|
pass
|
|
|
|
|
|
class RunSimpleSimulationWithDots(RunSimpleSimulation):
|
|
CONFIG = {
|
|
"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,
|
|
}
|
|
|
|
|
|
class SimpleSocialDistancing(RunSimpleSimulation):
|
|
CONFIG = {
|
|
"simulation_config": {
|
|
"person_type": PiPerson,
|
|
"n_cities": 1,
|
|
"city_population": 100,
|
|
"person_config": {
|
|
"infection_radius": 0.75,
|
|
"social_distance_factor": 2,
|
|
"gravity_strength": 0.1,
|
|
},
|
|
"travel_rate": 0,
|
|
"infection_time": 5,
|
|
},
|
|
}
|
|
|
|
|
|
class DelayedSocialDistancing(RunSimpleSimulation):
|
|
CONFIG = {
|
|
"delay_time": 8,
|
|
"target_sd_factor": 2,
|
|
"sd_probability": 1,
|
|
"random_seed": 1,
|
|
}
|
|
|
|
def construct(self):
|
|
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):
|
|
CONFIG = {
|
|
"simulation_config": {
|
|
"person_type": DotPerson,
|
|
"n_cities": 12,
|
|
"city_population": 100,
|
|
"person_config": {
|
|
"infection_radius": 0.75,
|
|
"social_distance_factor": 0,
|
|
"gravity_strength": 0.5,
|
|
},
|
|
"travel_rate": 0.02,
|
|
"infection_time": 5,
|
|
},
|
|
}
|
|
|
|
def add_sliders(self):
|
|
slider = ValueSlider(
|
|
"Travel rate",
|
|
self.simulation.travel_rate,
|
|
x_min=0,
|
|
x_max=0.02,
|
|
tick_frequency=0.005,
|
|
numbers_with_elongated_ticks=[],
|
|
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=0.2 * self.graph.get_height())
|
|
self.add(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(
|
|
FadeOut(words[2], UP),
|
|
FadeIn(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),
|
|
FadeOut(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),
|
|
),
|
|
FadeIn(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",
|
|
FadeIn(image, DOWN, run_time=2),
|
|
self.get_student_changes(
|
|
"pondering", "thinking", "pondering",
|
|
look_at_arg=image,
|
|
)
|
|
)
|
|
self.play(
|
|
FadeIn(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),
|
|
FadeIn(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(
|
|
FadeOut(R_label[1], UP),
|
|
FadeOut(name, UP),
|
|
FadeIn(R0, DOWN),
|
|
FadeIn(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(
|
|
FadeOut(words[0][0], UP),
|
|
FadeIn(estimate, DOWN),
|
|
)
|
|
self.wait()
|
|
|
|
self.play(
|
|
Write(parens),
|
|
FadeIn(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(FadeIn(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(FadeIn(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(FadeIn(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(FadeIn(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(
|
|
FadeIn(image, DOWN),
|
|
FadeIn(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,
|
|
]
|