mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
432 lines
12 KiB
Python
432 lines
12 KiB
Python
import itertools as it
|
|
import numpy as np
|
|
import operator as op
|
|
from random import random
|
|
|
|
from helpers import *
|
|
|
|
from scene import Scene
|
|
|
|
|
|
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 = []
|
|
|
|
self.construct()
|
|
|
|
def construct(self):
|
|
pass
|
|
|
|
def __str__(self):
|
|
return self.__class__.__name__
|
|
|
|
|
|
class CubeGraph(Graph):
|
|
"""
|
|
5 7
|
|
12
|
|
03
|
|
4 6
|
|
"""
|
|
def construct(self):
|
|
self.vertices = [
|
|
(x, y, 0)
|
|
for r in (1, 2)
|
|
for x, y in it.product([-r,r], [-r, r])
|
|
]
|
|
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"
|
|
]
|
|
|
|
|
|
class SampleGraph(Graph):
|
|
"""
|
|
4 2 3 8
|
|
0 1
|
|
7
|
|
5 6
|
|
"""
|
|
def construct(self):
|
|
self.vertices = [
|
|
( 0, 0, 0),
|
|
( 2, 0, 0),
|
|
( 1, 2, 0),
|
|
( 3, 2, 0),
|
|
(-1, 2, 0),
|
|
(-2,-2, 0),
|
|
( 2,-2, 0),
|
|
( 4,-1, 0),
|
|
( 6, 2, 0),
|
|
]
|
|
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),
|
|
]
|
|
|
|
|
|
class OctohedronGraph(Graph):
|
|
"""
|
|
3
|
|
|
|
1 0
|
|
2
|
|
4 5
|
|
"""
|
|
def construct(self):
|
|
self.vertices = [
|
|
(r*np.cos(angle), r*np.sin(angle)-1, 0)
|
|
for r, s in [(1, 0), (3, 3)]
|
|
for angle in (np.pi/6) * np.array([s, 4 + s, 8 + s])
|
|
]
|
|
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),
|
|
]
|
|
|
|
|
|
class CompleteGraph(Graph):
|
|
def __init__(self, num_vertices, radius = 3):
|
|
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)
|
|
for x in range(self.num_vertices)
|
|
for theta in [2*np.pi*x / self.num_vertices]
|
|
]
|
|
self.edges = it.combinations(range(self.num_vertices), 2)
|
|
|
|
def __str__(self):
|
|
return Graph.__str__(self) + str(self.num_vertices)
|
|
|
|
|
|
class GraphScene(Scene):
|
|
args_list = [
|
|
(CubeGraph(),),
|
|
(SampleGraph(),),
|
|
(OctohedronGraph(),),
|
|
]
|
|
@staticmethod
|
|
def args_to_string(*args):
|
|
return str(args[0])
|
|
|
|
def __init__(self, graph, *args, **kwargs):
|
|
#See CubeGraph() above for format of graph
|
|
self.graph = graph
|
|
Scene.__init__(self, *args, **kwargs)
|
|
|
|
def construct(self):
|
|
self.points = map(np.array, self.graph.vertices)
|
|
self.vertices = self.dots = [Dot(p) for p in self.points]
|
|
self.edges = self.lines = [
|
|
Line(self.points[i], self.points[j])
|
|
for i, j in self.graph.edges
|
|
]
|
|
self.add(*self.dots + self.edges)
|
|
|
|
def generate_regions(self):
|
|
regions = [
|
|
self.region_from_cycle(cycle)
|
|
for cycle in self.graph.region_cycles
|
|
]
|
|
regions[-1].complement()#Outer region painted outwardly...
|
|
self.regions = regions
|
|
|
|
def region_from_cycle(self, cycle):
|
|
point_pairs = [
|
|
[
|
|
self.points[cycle[i]],
|
|
self.points[cycle[(i+1)%len(cycle)]]
|
|
]
|
|
for i in range(len(cycle))
|
|
]
|
|
return region_from_line_boundary(
|
|
*point_pairs, shape = self.shape
|
|
)
|
|
|
|
def draw_vertices(self, **kwargs):
|
|
self.clear()
|
|
self.play(ShowCreation(Mobject(*self.vertices), **kwargs))
|
|
|
|
def draw_edges(self):
|
|
self.play(*[
|
|
ShowCreation(edge, run_time = 1.0)
|
|
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")
|
|
for point in self.points
|
|
])
|
|
self.play(Transform(
|
|
start, end, alpha_func = there_and_back,
|
|
**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(
|
|
vertex,
|
|
mobject.copy().shift(vertex.get_center())
|
|
)
|
|
for vertex in self.vertices
|
|
] + [
|
|
ApplyMethod(
|
|
edge.scale_in_place,
|
|
(edge.get_length() - diameter) / edge.get_length()
|
|
)
|
|
for edge in self.edges
|
|
])
|
|
|
|
def annotate_edges(self, mobject, fade_in = True, **kwargs):
|
|
angles = map(np.arctan, map(Line.get_slope, self.edges))
|
|
self.edge_annotations = [
|
|
mobject.copy().rotate(angle).shift(edge.get_center())
|
|
for angle, edge in zip(angles, self.edges)
|
|
]
|
|
if fade_in:
|
|
self.play(*[
|
|
FadeIn(ann, **kwargs)
|
|
for ann in self.edge_annotations
|
|
])
|
|
|
|
def trace_cycle(self, cycle = None, color = "yellow", run_time = 2.0):
|
|
if cycle == None:
|
|
cycle = self.graph.region_cycles[0]
|
|
time_per_edge = run_time / len(cycle)
|
|
next_in_cycle = it.cycle(cycle)
|
|
next_in_cycle.next()#jump one ahead
|
|
self.traced_cycle = Mobject(*[
|
|
Line(self.points[i], self.points[j]).highlight(color)
|
|
for i, j in zip(cycle, next_in_cycle)
|
|
])
|
|
self.play(
|
|
ShowCreation(self.traced_cycle),
|
|
run_time = run_time
|
|
)
|
|
|
|
def generate_spanning_tree(self, root = 0, color = "yellow"):
|
|
self.spanning_tree_root = 0
|
|
pairs = deepcopy(self.graph.edges)
|
|
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(*[
|
|
Line(
|
|
self.points[pair[0]],
|
|
self.points[pair[1]]
|
|
).highlight(color)
|
|
for pair in self.spanning_tree_index_pairs
|
|
])
|
|
|
|
def generate_treeified_spanning_tree(self):
|
|
bottom = -SPACE_HEIGHT + 1
|
|
x_sep = 1
|
|
y_sep = 2
|
|
if not hasattr(self, "spanning_tree"):
|
|
self.generate_spanning_tree()
|
|
root = self.spanning_tree_root
|
|
color = self.spanning_tree.get_color()
|
|
indices = range(len(self.points))
|
|
#Build dicts
|
|
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
|
|
#the maxium number of decendents in a single generation,
|
|
#minus 1, multiplied by x_sep
|
|
width_of = {}
|
|
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
|
|
for child in children:
|
|
x_coord_of[child] = left_hand + width_of[child]/2.0
|
|
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(*[
|
|
Line(new_points[i], new_points[j]).highlight(color)
|
|
for i, j in self.spanning_tree_index_pairs
|
|
])
|
|
|
|
def generate_dual_graph(self):
|
|
point_at_infinity = np.array([np.inf]*3)
|
|
cycles = self.graph.region_cycles
|
|
self.dual_points = [
|
|
center_of_mass([
|
|
self.points[index]
|
|
for index in cycle
|
|
])
|
|
for cycle in cycles
|
|
]
|
|
self.dual_vertices = [
|
|
Dot(point).highlight("green")
|
|
for point in self.dual_points
|
|
]
|
|
self.dual_vertices[-1] = Circle().scale(SPACE_WIDTH + SPACE_HEIGHT)
|
|
self.dual_points[-1] = point_at_infinity
|
|
|
|
self.dual_edges = []
|
|
for pair in self.graph.edges:
|
|
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]:
|
|
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])
|
|
vect = center_of_mass([
|
|
self.points[pair[0]],
|
|
self.points[pair[1]]
|
|
]) - new_point
|
|
new_point += SPACE_WIDTH*vect/np.linalg.norm(vect)
|
|
dual_point_pair[i] = new_point
|
|
self.dual_edges.append(
|
|
Line(*dual_point_pair).highlight()
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|