3b1b-manim/manimlib/once_useful_constructs/graph_theory.py

414 lines
12 KiB
Python
Raw Normal View History

from functools import reduce
import itertools as it
2015-06-19 08:31:02 -07:00
import operator as op
import numpy as np
from manimlib.constants import *
from manimlib.scene.scene import Scene
from manimlib.utils.rate_functions import there_and_back
from manimlib.utils.space_ops import center_of_mass
2015-06-19 08:31:02 -07:00
2015-06-22 10:14:53 -07:00
class Graph():
def __init__(self):
# List of points in R^3
# vertices = []
# List of pairs of indices of vertices
# edges = []
# List of tuples of indices of vertices. The last should
# be a cycle whose interior is the entire graph, and when
# regions are computed its complement will be taken.
# region_cycles = []
2015-06-22 10:14:53 -07:00
self.construct()
def construct(self):
pass
def __str__(self):
return self.__class__.__name__
2015-06-22 10:14:53 -07:00
class CubeGraph(Graph):
"""
5 7
12
03
2015-06-22 10:14:53 -07:00
4 6
"""
2015-06-22 10:14:53 -07:00
def construct(self):
self.vertices = [
(x, y, 0)
for r in (1, 2)
for x, y in it.product([-r, r], [-r, r])
2015-06-22 10:14:53 -07:00
]
self.edges = [
(0, 1),
(0, 2),
(3, 1),
(3, 2),
(4, 5),
(4, 6),
(7, 5),
(7, 6),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
]
self.region_cycles = [
[0, 2, 3, 1],
[4, 0, 1, 5],
[4, 6, 2, 0],
[6, 7, 3, 2],
[7, 5, 1, 3],
[4, 6, 7, 5], # By convention, last region will be "outside"
2015-06-22 10:14:53 -07:00
]
2015-06-22 10:14:53 -07:00
class SampleGraph(Graph):
"""
4 2 3 8
0 1
7
5 6
"""
2015-06-22 10:14:53 -07:00
def construct(self):
self.vertices = [
(0, 0, 0),
(2, 0, 0),
(1, 2, 0),
(3, 2, 0),
2015-06-22 10:14:53 -07:00
(-1, 2, 0),
(-2, -2, 0),
(2, -2, 0),
(4, -1, 0),
(6, 2, 0),
2015-06-22 10:14:53 -07:00
]
self.edges = [
(0, 1),
(1, 2),
(1, 3),
(3, 2),
(2, 4),
(4, 0),
(2, 0),
(4, 5),
(0, 5),
(1, 5),
(5, 6),
(6, 7),
(7, 1),
(7, 8),
(8, 3),
]
self.region_cycles = [
(0, 1, 2),
(1, 3, 2),
(2, 4, 0),
(4, 5, 0),
(0, 5, 1),
(1, 5, 6, 7),
(1, 7, 8, 3),
(4, 5, 6, 7, 8, 3, 2),
]
2015-06-22 10:14:53 -07:00
class OctohedronGraph(Graph):
"""
3
2015-06-22 10:14:53 -07:00
1 0
2
4 5
"""
2015-06-22 10:14:53 -07:00
def construct(self):
self.vertices = [
(r * np.cos(angle), r * np.sin(angle) - 1, 0)
2015-06-22 10:14:53 -07:00
for r, s in [(1, 0), (3, 3)]
for angle in (np.pi / 6) * np.array([s, 4 + s, 8 + s])
2015-06-22 10:14:53 -07:00
]
self.edges = [
(0, 1),
(1, 2),
(2, 0),
(5, 0),
(0, 3),
(3, 5),
(3, 1),
(3, 4),
(1, 4),
(4, 2),
(4, 5),
(5, 2),
]
self.region_cycles = [
(0, 1, 2),
(0, 5, 3),
(3, 1, 0),
(3, 4, 1),
(1, 4, 2),
(2, 4, 5),
(5, 0, 2),
(3, 4, 5),
]
2015-06-22 10:14:53 -07:00
class CompleteGraph(Graph):
def __init__(self, num_vertices, radius=3):
2015-06-22 10:14:53 -07:00
self.num_vertices = num_vertices
self.radius = radius
Graph.__init__(self)
def construct(self):
self.vertices = [
(self.radius * np.cos(theta), self.radius * np.sin(theta), 0)
2015-06-22 10:14:53 -07:00
for x in range(self.num_vertices)
for theta in [2 * np.pi * x / self.num_vertices]
2015-06-22 10:14:53 -07:00
]
2018-08-09 17:56:05 -07:00
self.edges = it.combinations(list(range(self.num_vertices)), 2)
2015-06-22 10:14:53 -07:00
def __str__(self):
return Graph.__str__(self) + str(self.num_vertices)
2018-05-09 14:05:14 -07:00
class DiscreteGraphScene(Scene):
2015-06-19 08:31:02 -07:00
args_list = [
2015-06-22 10:14:53 -07:00
(CubeGraph(),),
(SampleGraph(),),
(OctohedronGraph(),),
2015-06-19 08:31:02 -07:00
]
2015-06-19 08:31:02 -07:00
@staticmethod
def args_to_string(*args):
2015-06-22 10:14:53 -07:00
return str(args[0])
2015-06-19 08:31:02 -07:00
def __init__(self, graph, *args, **kwargs):
# See CubeGraph() above for format of graph
2015-06-19 08:31:02 -07:00
self.graph = graph
Scene.__init__(self, *args, **kwargs)
def construct(self):
2021-01-12 12:25:12 -10:00
self._points = list(map(np.array, self.graph.vertices))
self.vertices = self.dots = [Dot(p) for p in self._points]
2015-06-19 08:31:02 -07:00
self.edges = self.lines = [
2021-01-12 12:25:12 -10:00
Line(self._points[i], self._points[j])
2015-06-22 10:14:53 -07:00
for i, j in self.graph.edges
2015-06-19 08:31:02 -07:00
]
self.add(*self.dots + self.edges)
def generate_regions(self):
regions = [
self.region_from_cycle(cycle)
2015-06-22 10:14:53 -07:00
for cycle in self.graph.region_cycles
2015-06-19 08:31:02 -07:00
]
regions[-1].complement() # Outer region painted outwardly...
2015-06-19 08:31:02 -07:00
self.regions = regions
def region_from_cycle(self, cycle):
point_pairs = [
[
2021-01-12 12:25:12 -10:00
self._points[cycle[i]],
self._points[cycle[(i + 1) % len(cycle)]]
2015-06-19 08:31:02 -07:00
]
for i in range(len(cycle))
]
return region_from_line_boundary(
*point_pairs, shape=self.shape
2015-06-19 08:31:02 -07:00
)
def draw_vertices(self, **kwargs):
self.clear()
self.play(ShowCreation(Mobject(*self.vertices), **kwargs))
2015-06-19 08:31:02 -07:00
def draw_edges(self):
self.play(*[
ShowCreation(edge, run_time=1.0)
2015-06-19 08:31:02 -07:00
for edge in self.edges
])
def accent_vertices(self, **kwargs):
self.remove(*self.vertices)
start = Mobject(*self.vertices)
end = Mobject(*[
Dot(point, radius=3 * Dot.DEFAULT_RADIUS, color="lightgreen")
2021-01-12 12:25:12 -10:00
for point in self._points
2015-06-19 08:31:02 -07:00
])
self.play(Transform(
start, end, rate_func=there_and_back,
2015-06-19 08:31:02 -07:00
**kwargs
))
self.remove(start)
self.add(*self.vertices)
def replace_vertices_with(self, mobject):
mobject.center()
diameter = max(mobject.get_height(), mobject.get_width())
self.play(*[
CounterclockwiseTransform(
2015-06-19 08:31:02 -07:00
vertex,
mobject.copy().shift(vertex.get_center())
2015-06-19 08:31:02 -07:00
)
for vertex in self.vertices
] + [
ApplyMethod(
edge.scale,
2015-06-19 08:31:02 -07:00
(edge.get_length() - diameter) / edge.get_length()
)
for edge in self.edges
])
def annotate_edges(self, mobject, fade_in=True, **kwargs):
2018-08-09 17:56:05 -07:00
angles = list(map(np.arctan, list(map(Line.get_slope, self.edges))))
2015-06-19 08:31:02 -07:00
self.edge_annotations = [
mobject.copy().rotate(angle).move_to(edge.get_center())
2015-06-19 08:31:02 -07:00
for angle, edge in zip(angles, self.edges)
]
if fade_in:
self.play(*[
2015-06-19 08:31:02 -07:00
FadeIn(ann, **kwargs)
for ann in self.edge_annotations
])
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
if cycle is None:
2015-06-22 10:14:53 -07:00
cycle = self.graph.region_cycles[0]
2015-06-19 08:31:02 -07:00
next_in_cycle = it.cycle(cycle)
next(next_in_cycle) # jump one ahead
self.traced_cycle = Mobject(*[
2021-01-12 12:25:12 -10:00
Line(self._points[i], self._points[j]).set_color(color)
2015-06-19 08:31:02 -07:00
for i, j in zip(cycle, next_in_cycle)
])
self.play(
ShowCreation(self.traced_cycle),
run_time=run_time
2015-06-19 08:31:02 -07:00
)
def generate_spanning_tree(self, root=0, color="yellow"):
2015-06-19 08:31:02 -07:00
self.spanning_tree_root = 0
2015-06-22 10:14:53 -07:00
pairs = deepcopy(self.graph.edges)
2015-06-19 08:31:02 -07:00
pairs += [tuple(reversed(pair)) for pair in pairs]
self.spanning_tree_index_pairs = []
curr = root
spanned_vertices = set([curr])
to_check = set([curr])
while len(to_check) > 0:
curr = to_check.pop()
for pair in pairs:
if pair[0] == curr and pair[1] not in spanned_vertices:
self.spanning_tree_index_pairs.append(pair)
spanned_vertices.add(pair[1])
to_check.add(pair[1])
self.spanning_tree = Mobject(*[
2015-06-19 08:31:02 -07:00
Line(
2021-01-12 12:25:12 -10:00
self._points[pair[0]],
self._points[pair[1]]
2018-03-30 11:51:31 -07:00
).set_color(color)
2015-06-19 08:31:02 -07:00
for pair in self.spanning_tree_index_pairs
])
def generate_treeified_spanning_tree(self):
bottom = -FRAME_Y_RADIUS + 1
2015-06-19 08:31:02 -07:00
x_sep = 1
y_sep = 2
if not hasattr(self, "spanning_tree"):
self.generate_spanning_tree()
root = self.spanning_tree_root
2015-06-19 08:31:02 -07:00
color = self.spanning_tree.get_color()
2021-01-12 12:25:12 -10:00
indices = list(range(len(self._points)))
# Build dicts
2015-06-19 08:31:02 -07:00
parent_of = dict([
tuple(reversed(pair))
for pair in self.spanning_tree_index_pairs
])
children_of = dict([(index, []) for index in indices])
for child in parent_of:
children_of[parent_of[child]].append(child)
x_coord_of = {root: 0}
y_coord_of = {root: bottom}
# width to allocate to a given node, computed as
2021-08-07 22:25:26 +07:00
# the maximum number of decendents in a single generation,
# minus 1, multiplied by x_sep
width_of = {}
2015-06-19 08:31:02 -07:00
for index in indices:
next_generation = children_of[index]
curr_max = max(1, len(next_generation))
while next_generation != []:
next_generation = reduce(op.add, [
children_of[node]
for node in next_generation
])
curr_max = max(curr_max, len(next_generation))
width_of[index] = x_sep * (curr_max - 1)
to_process = [root]
while to_process != []:
index = to_process.pop()
if index not in y_coord_of:
y_coord_of[index] = y_sep + y_coord_of[parent_of[index]]
children = children_of[index]
left_hand = x_coord_of[index] - width_of[index] / 2.0
2015-06-19 08:31:02 -07:00
for child in children:
x_coord_of[child] = left_hand + width_of[child] / 2.0
2015-06-19 08:31:02 -07:00
left_hand += width_of[child] + x_sep
to_process += children
new_points = [
np.array([
x_coord_of[index],
y_coord_of[index],
0
])
for index in indices
]
self.treeified_spanning_tree = Mobject(*[
2018-03-30 11:51:31 -07:00
Line(new_points[i], new_points[j]).set_color(color)
2015-06-19 08:31:02 -07:00
for i, j in self.spanning_tree_index_pairs
])
def generate_dual_graph(self):
point_at_infinity = np.array([np.inf] * 3)
2015-06-22 10:14:53 -07:00
cycles = self.graph.region_cycles
2015-06-19 08:31:02 -07:00
self.dual_points = [
center_of_mass([
2021-01-12 12:25:12 -10:00
self._points[index]
2015-06-19 08:31:02 -07:00
for index in cycle
])
for cycle in cycles
]
self.dual_vertices = [
2018-03-30 11:51:31 -07:00
Dot(point).set_color("green")
2015-06-19 08:31:02 -07:00
for point in self.dual_points
]
self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS + FRAME_Y_RADIUS)
2015-06-19 08:31:02 -07:00
self.dual_points[-1] = point_at_infinity
self.dual_edges = []
2015-06-22 10:14:53 -07:00
for pair in self.graph.edges:
2015-06-19 08:31:02 -07:00
dual_point_pair = []
for cycle in cycles:
if not (pair[0] in cycle and pair[1] in cycle):
continue
index1, index2 = cycle.index(pair[0]), cycle.index(pair[1])
if abs(index1 - index2) in [1, len(cycle) - 1]:
2015-06-19 08:31:02 -07:00
dual_point_pair.append(
self.dual_points[cycles.index(cycle)]
)
assert(len(dual_point_pair) == 2)
for i in 0, 1:
if all(dual_point_pair[i] == point_at_infinity):
new_point = np.array(dual_point_pair[1 - i])
2015-06-19 08:31:02 -07:00
vect = center_of_mass([
2021-01-12 12:25:12 -10:00
self._points[pair[0]],
self._points[pair[1]]
2015-06-19 08:31:02 -07:00
]) - new_point
2018-08-15 17:30:24 -07:00
new_point += FRAME_X_RADIUS * vect / get_norm(vect)
2015-06-19 08:31:02 -07:00
dual_point_pair[i] = new_point
self.dual_edges.append(
2018-03-30 11:51:31 -07:00
Line(*dual_point_pair).set_color()
2015-06-19 08:31:02 -07:00
)