class Cell(object): """A cell is a uniform polyhedra (or a "room") in the 3d tiling. """ def __init__(self, cox_mat, v0, active, reflections): self.cox_mat = cox_mat self.v0 = v0 self.active = active self.reflections = reflections self.G = CoxeterGroup(cox_mat) self.vertices_coords = [] self.num_vertices = None self.edge_coords = [] self.num_edges = None def build_geometry(self, depth, maxcount): self.G.init() self.word_generator = partial(self.G.traverse, depth=depth, maxcount=maxcount) self.get_vertices() self.get_edges() return self def transform(self, word, v): for w in reversed(word): v = self.reflections[w](v) return vround(v) def project(self, v): return helpers.project_poincare(v) def get_vertices(self): for word in self.word_generator(): v = self.transform(word, self.v0) if v not in self.vertices_coords: self.vertices_coords.append(v) self.num_vertices = len(self.vertices_coords) def get_edges(self): """ An edge is uniquely determined by the coordinates of its middle point. Here I simply use a set to maintain the middle points of known edges and avoid duplicates. """ edgehash = set() for i, active in enumerate(self.active): if active: for word in self.word_generator(): p1 = self.transform(word, self.v0) p2 = self.transform(word + (i, ), self.v0) q = centroid((p1, p2)) if q not in edgehash: self.edge_coords.append((p1, p2)) edgehash.add(q) self.num_edges = len(self.edge_coords)
class Honeycomb(object): def __init__(self, coxeter_diagram, init_dist): if len(coxeter_diagram) != 6 or len(init_dist) != 4: raise ValueError("Invalid input dimension") # Coxeter matrix and its rank self.cox_mat = helpers.make_symmetry_matrix(coxeter_diagram) self.rank = len(self.cox_mat) # generators of the symmetry group self.gens = tuple(range(self.rank)) # symmetry group of this tiling self.G = CoxeterGroup(self.cox_mat) # a mirror is active iff the initial point is not on it self.active = tuple(bool(x) for x in init_dist) # reflection mirrors self.mirrors = self.get_mirrors(coxeter_diagram) # reflections (possibly affine) about the mirrors self.reflections = self.get_reflections() # coordinates of the initial point self.init_v = self.get_init_point(init_dist) self.edge_hash_set = set() self.num_vertices = 0 self.num_edges = 0 def get_reflections(self): def reflect(v, normal): return v - 2 * np.dot(v, normal) * normal return [partial(reflect, normal=n) for n in self.mirrors] def transform(self, word, v): for w in reversed(word): v = self.reflections[w](v) return vround(v) def project(self, v): return helpers.project_poincare(v) def get_init_point(self, init_dist): return helpers.get_point_from_distance(self.mirrors, init_dist) def get_mirrors(self, coxeter_diagram): return helpers.get_hyperbolic_honeycomb_mirrors(coxeter_diagram) def get_fundamental_cells(self, depth=None, maxcount=20000): """ Generate the fundamental cells of the tiling, these cells are centered at the vertices of the fundamental tetrahedron and are generated by reflecting the initial point about the three mirrors meeting at each vertex. """ result = {} for triple in combinations(self.gens, 3): cox_mat = self.cox_mat[np.ix_(triple, triple)] refs = [self.reflections[k] for k in triple] active = [self.active[k] for k in triple] if not helpers.is_degenerate(cox_mat, active): C = Cell(cox_mat, self.init_v, active, refs).build_geometry(depth, maxcount) result[triple] = C return result def is_new_edge(self, edge): mid = centroid(edge) if mid not in self.edge_hash_set: self.edge_hash_set.add(mid) return True return False def collect_fundamental_cell_edges(self): result = [] for C in self.fundamental_cells.values(): for edge in C.edge_coords: if self.is_new_edge(edge): result.append(edge) return result def export_edge(self, fobj, p1, p2): """Export the data of an edge to POV-Ray .inc file.""" fobj.write("HyperbolicEdge({}, {})\n".format(helpers.pov_vector(p1), helpers.pov_vector(p2))) def generate_povray_data( self, depth=100, maxcount=50000, cell_depth=None, cell_edges=10000, filename="./povray/honeycomb-data.inc", eye=(0, 0, 0.5), lookat=(0, 0, 0), ): self.G.init() self.word_generator = partial(self.G.traverse, depth=depth, maxcount=maxcount) self.fundamental_cells = self.get_fundamental_cells( cell_depth, cell_edges) init_edges = self.collect_fundamental_cell_edges() bar = tqdm.tqdm(desc="processing edges", total=maxcount) vertices = set() eye = np.array(eye) lookat = np.array(lookat) viewdir = helpers.normalize(lookat - eye) def add_new_edge(edge): p1 = self.project(edge[0]) p2 = self.project(edge[1]) if np.dot(p1 - eye, viewdir) > 0.5 or np.dot(p2 - eye, viewdir) > 0.5: self.export_edge(f, p1, p2) self.num_edges += 1 for v in [p1, p2]: v = vround(v) if v not in vertices: vertices.add(v) self.num_vertices += 1 with open(filename, "w") as f: f.write("#declare camera_loc = {};\n".format( helpers.pov_vector(eye))) f.write("#declare lookat = {};\n".format( helpers.pov_vector(lookat))) for edge in init_edges: add_new_edge(edge) for word in self.word_generator(): for edge in init_edges: edge = [self.transform(word, v) for v in edge] if self.is_new_edge(edge): add_new_edge(edge) bar.update(1) bar.close() verts = "#declare num_vertices = {};\n" verts_coords = "#declare vertices = array[{}]{{{}}};\n" print("{} vertices and {} edges generated".format( self.num_vertices, self.num_edges)) f.write(verts.format(self.num_vertices)) f.write( verts_coords.format(self.num_vertices, helpers.pov_vector_list(vertices)))
class Tiling2D(object): def __init__(self, coxeter_diagram, init_dist): if len(coxeter_diagram) != 3 or len(init_dist) != 3: raise ValueError("Invalid input dimension") self.diagram = coxeter_diagram # Coxeter matrix and its rank self.cox_mat = helpers.make_symmetry_matrix(coxeter_diagram) self.rank = len(self.cox_mat) # generators of the symmetry group self.gens = tuple(range(self.rank)) # symmetry group of this tiling self.G = CoxeterGroup(self.cox_mat) # a mirror is active iff the initial point is not on it self.active = tuple(bool(x) for x in init_dist) # reflection mirrors self.mirrors = self.get_mirrors(coxeter_diagram) # reflections (possibly affine) about the mirrors self.reflections = self.get_reflections() # coordinates of the initial point self.init_v = self.get_init_point(init_dist) # vertices of the fundamental triangle self.triangle_verts = self.get_fundamental_triangle_verts() # ---------------------- # to be calculated later # ---------------------- # holds the words in the symmetry group up to a given depth self.words = None # holds the coset representatives of the standard parabolic # subgroup of vertex-stabilizing subgroup self.vwords = None self.vertices_coords = [] self.num_vertices = None self.num_edges = None self.num_faces = None self.edge_indices = {} self.face_indices = {} def vertex_at_mirrors(self, i, j): return 2 * (i + j) % 3 def get_init_point(self, init_dist): raise NotImplementedError def get_mirrors(self, coxeter_diagram): raise NotImplementedError def get_fundamental_triangle_verts(self): raise NotImplementedError def build_geometry(self, depth=None, maxcount=20000): """Postpone the actual computations to this method. """ self.G.init() self.words = tuple(self.G.traverse(depth, maxcount)) self.get_vertices() self.get_edges() self.get_faces() return self def get_vertices(self): # generators of the vertex-stabilizing subgroup H = tuple(i for i, x in enumerate(self.active) if not x) # coset representatives of the vertex-stabilizing subgroup reps = set(self.G.get_coset_representative(w, H) for w in self.words) self.vwords = self.G.sort_words(reps) self.vtable = self.G.get_coset_table(self.vwords, H) self.num_vertices = len(self.vwords) self.vertices_coords = [ self.transform(w, self.init_v) for w in self.vwords ] def get_edges(self): for i in self.gens: if self.active[i]: elist = [] H = (i, ) # edge-stabilizing subgroup reps = set( self.G.get_coset_representative(w, H) for w in self.words) reps = self.G.sort_words(reps) for word in reps: v1 = self.G.move(self.vtable, 0, word) v2 = self.G.move(self.vtable, 0, word + (i, )) if v1 is not None and v2 is not None: if (v1, v2) not in elist and (v2, v1) not in elist: elist.append((v1, v2)) self.edge_indices[i] = elist self.num_edges = sum(len(L) for L in self.edge_indices.values()) def get_faces(self): for i, j in combinations(self.gens, 2): c0 = self.triangle_verts[self.vertex_at_mirrors(i, j)] f0 = [] m = self.cox_mat[i][j] H = (i, j) type = 0 if self.active[i] and self.active[j]: type = 1 for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) f0.append(self.G.move(self.vtable, 0, (i, j) * k + (i, ))) elif self.active[i] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (j, i) * k)) elif self.active[j] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) else: continue reps = set( self.G.get_coset_representative(w, H) for w in self.words) reps = self.G.sort_words(reps) flist = [] for word in reps: f = tuple(self.G.move(self.vtable, v, word) for v in f0) if None not in f and not helpers.check_duplicate_face( f, flist): center = self.transform(word, c0) coords = [self.vertices_coords[k] for k in f] face = DihedralFace(word, f, center, coords, type) flist.append(face) self.face_indices[(i, j)] = flist self.num_faces = sum(len(L) for L in self.face_indices.values()) def get_reflections(self): def reflect(v, normal): return v - 2 * np.dot(v, normal) * normal return [partial(reflect, normal=n) for n in self.mirrors] def transform(self, word, v): for w in reversed(word): v = self.reflections[w](v) return v def get_info(self): """Return some statistics of the tiling. """ pattern = "{}-{}-{}".format(*self.diagram).replace("/", "|") info = "" info += "name: triangle group {}\n".format(pattern) info += "cox_mat: {}\n".format(self.cox_mat) info += "vertices: {}\n".format(self.num_vertices) info += "edges: {}\n".format(self.num_edges) info += "faces: {}\n".format(self.num_faces) info += "states in the automaton: {}\n".format(self.G.dfa.num_states) info += "reflection table:\n{}\n".format(self.G.reftable) info += "the automaton is saved as {}_dfa.png".format(pattern) self.G.dfa.draw(pattern + "_dfa.png") return info def render(self, *arg, **kwargs): raise NotImplementedError
class Tiling2D(object): """ Base class for all three types of tilings. """ def __init__(self, coxeter_diagram, init_dist): if len(coxeter_diagram) != 3 or len(init_dist) != 3: raise ValueError("Invalid input dimension") self.diagram = coxeter_diagram # Coxeter matrix and its rank self.cox_mat = helpers.get_coxeter_matrix(coxeter_diagram) self.rank = len(self.cox_mat) # generators of the symmetry group self.gens = tuple(range(self.rank)) # symmetry group of this tiling self.G = CoxeterGroup(self.cox_mat) # a mirror is active iff the initial point is not on it self.active = tuple(bool(x) for x in init_dist) # reflection mirrors self.mirrors = self.get_mirrors(coxeter_diagram) # reflections (possibly affine) about the mirrors self.reflections = self.get_reflections() # coordinates of the initial point self.init_v = self.get_init_point(init_dist) # vertices of the fundamental triangle self.triangle_verts = self.get_fundamental_triangle_verts() # ---------------------- # to be calculated later # ---------------------- # holds the words in the symmetry group up to a given depth self.words = None # holds the coset representatives of the standard parabolic # subgroup of vertex-stabilizing subgroup self.vwords = None self.vertices_coords = [] self.num_vertices = None self.num_edges = None self.num_faces = None self.edge_indices = {} self.face_indices = {} def vertex_at_mirrors(self, i, j): return 2 * (i + j) % 3 def get_init_point(self, init_dist): raise NotImplementedError def get_mirrors(self, coxeter_diagram): raise NotImplementedError def get_fundamental_triangle_verts(self): raise NotImplementedError def build_geometry(self, depth=None, maxcount=20000): """Postpone the actual computations to this method. """ self.G.init() self.word_generator = partial(self.G.traverse, depth=depth, maxcount=maxcount) self.get_vertices() self.get_edges() self.get_faces() return self def get_vertices(self): # generators of the vertex-stabilizing subgroup H = tuple(i for i, x in enumerate(self.active) if not x) # coset representatives of the vertex-stabilizing subgroup reps = set(self.word_generator(parabolic=H)) # sort the words in shortlex order self.vwords = self.G.sort_words(reps) # build the coset table for these cosets self.vtable = self.G.get_coset_table(self.vwords, H) self.num_vertices = len(self.vwords) # apply each coset representative to the initial vertex self.vertices_coords = [ self.transform(w, self.init_v) for w in self.vwords ] def get_edges(self): """ Compute the indices of the edges. Steps: 1. Use a generator to yield a list L of words in the group. 2. Compute the coset representatives of the edge stabilizing subgroup for words in L and remove duplicates. (So each remaining representative maps to different edges) 3. Apply each coset representative to the ends of an initial edge to get the transformed edge. 4. Find the indices of the resulting edge in L. """ for i in self.gens: if self.active[i]: elist = [] H = (i, ) + self.get_orthogonal_stabilizing_mirrors((i, )) reps = set(self.word_generator(parabolic=H)) reps = self.G.sort_words(reps) for word in reps: v1 = self.G.move(self.vtable, 0, word) v2 = self.G.move(self.vtable, 0, word + (i, )) if v1 is not None and v2 is not None: elist.append((v1, v2)) self.edge_indices[i] = elist self.num_edges = sum(len(L) for L in self.edge_indices.values()) def get_faces(self): """Compute the indices of the faces (and other information we will need). """ for i, j in combinations(self.gens, 2): # this is the center of the initial face, # it's a vertex of the fundamental triangle. c0 = self.triangle_verts[self.vertex_at_mirrors(i, j)] # a list holds the vertices of the initial face. f0 = [] m = self.cox_mat[i][j] # this is the stabilizing subgroup of the initial face f0. H = (i, j) + self.get_orthogonal_stabilizing_mirrors((i, j)) # type indicates if this face is regular (0) or truncated (1). # it's truncated if and only if both mirrors are active. type = 0 # compute the words (may not be in normal form) for the # vertices of the initial face if self.active[i] and self.active[j]: type = 1 for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) f0.append(self.G.move(self.vtable, 0, (i, j) * k + (i, ))) elif self.active[i] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (j, i) * k)) elif self.active[j] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) else: continue # compute coset representatives of the initial face, # each word in the result set maps f0 to a different face. reps = set(self.word_generator(parabolic=H)) # sort the faces in shortlex order. reps = self.G.sort_words(reps) # a set holds faces, we use a set here because though a word w # in H stabilizes f0, it may change cyclically rotate f0 to another # different ordered tuple. flist = [] for word in reps: # compute the indices of the vertices of the transformed face f = tuple(self.G.move(self.vtable, v, word) for v in f0) # check if `None` is in f (in this case f contains some # vertex that is not in the vertices list) or there already has # a rotated version of f in the set. if None not in f: center = self.transform(word, c0) coords = [self.vertices_coords[k] for k in f] face = DihedralFace(word, f, center, coords, type) flist.append(face) self.face_indices[(i, j)] = flist self.num_faces = sum(len(L) for L in self.face_indices.values()) def get_reflections(self): def reflect(v, normal): return v - 2 * np.dot(v, normal) * normal return [partial(reflect, normal=n) for n in self.mirrors] def transform(self, word, v): for w in reversed(word): v = self.reflections[w](v) return v def get_orthogonal_stabilizing_mirrors(self, subgens): """ :param subgens: a list of generators, e.g. [0, 1] Given a list of generators in `subgens`, return the generators that commute with all of those in `subgens` and fix the initial vertex. """ result = [] for s in self.gens: # check commutativity if all(self.cox_mat[x][s] == 2 for x in subgens): # check if it fixes v0 if not self.active[s]: result.append(s) return tuple(result) def get_info(self): """Return some statistics of the tiling. """ pattern = "{}-{}-{}".format(*self.diagram).replace("/", "|") info = "" info += "name: triangle group {}\n".format(pattern) info += "cox_mat: {}\n".format(self.cox_mat) info += "vertices: {}\n".format(self.num_vertices) info += "edges: {}\n".format(self.num_edges) info += "faces: {}\n".format(self.num_faces) info += "states in the automaton: {}\n".format(self.G.dfa.num_states) info += "reflection table:\n{}\n".format(self.G.reftable) info += "the automaton is saved as {}_dfa.png".format(pattern) self.G.dfa.draw(pattern + "_dfa.png") return info def render(self, *arg, **kwargs): raise NotImplementedError
class UniformTiling(object): def __init__(self, coxeter_diagram, init_dist): self.cox_mat = helpers.get_coxeter_matrix(coxeter_diagram) self.G = CoxeterGroup(self.cox_mat) self.active = tuple(bool(x) for x in init_dist) self.words = None self.mirrors = self.get_mirrors(coxeter_diagram) self.init_v = helpers.get_point_from_distance(self.mirrors, init_dist) self.reflections = self.get_reflections(init_dist) # to be calculated later self.vertices_coords = [] self.num_vertices = None self.num_edges = None self.num_faces = None self.edge_indices = {} self.face_indices = {} def build_geometry(self, depth=None, maxcount=20000): self.G.init() self.words = tuple(self.G.traverse(depth, maxcount)) self.get_vertices() self.get_edges() self.get_faces() def get_vertices(self): parabolic = tuple(i for i, x in enumerate(self.active) if not x) coset_reps = set([ self.G.get_coset_representative(w, parabolic, True) for w in self.words ]) self.vwords = self.G.sort_words(coset_reps) self.vtable = self.G.get_coset_table(self.vwords, parabolic) self.num_vertices = len(self.vwords) self.vertices_coords = [ self.transform(word, self.init_v) for word in self.vwords ] def get_edges(self): for i in range(len(self.active)): if self.active[i]: elist = [] coset_reps = set([ self.G.get_coset_representative(w, (i, ), True) for w in self.words ]) for word in self.G.sort_words(coset_reps): v1 = self.G.move(self.vtable, 0, word) v2 = self.G.move(self.vtable, 0, word + (i, )) if v1 is not None and v2 is not None: if (v1, v2) not in elist and (v2, v1) not in elist: elist.append((v1, v2)) self.edge_indices[i] = elist self.num_edges = sum(len(L) for L in self.edge_indices.values()) def get_faces(self): for i, j in combinations(range(len(self.active)), 2): f0 = [] m = self.cox_mat[i][j] parabolic = (i, j) if self.active[i] and self.active[j]: for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) f0.append(self.G.move(self.vtable, 0, (i, j) * k + (i, ))) elif self.active[i] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (j, i) * k)) elif self.active[j] and m > 2: for k in range(m): f0.append(self.G.move(self.vtable, 0, (i, j) * k)) else: continue coset_reps = set([ self.G.get_coset_representative(w, parabolic, True) for w in self.words ]) flist = [] for word in self.G.sort_words(coset_reps): f = tuple(self.G.move(self.vtable, v, word) for v in f0) if None not in f and not helpers.check_duplicate_face( f, flist): flist.append(f) self.face_indices[(i, j)] = flist self.num_faces = sum(len(L) for L in self.face_indices.values()) def transform(self, word, v): for w in reversed(word): v = self.reflections[w](v) return v def get_reflections(self, init_dist): raise NotImplementedError def get_fundamental_triangle_vertices(self): raise NotImplementedError def project(self, v): raise NotImplementedError def get_mirrors(self, coxeter_diagram): raise NotImplementedError