123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646 |
- """
- Polytope class and subclasses for n-polytopes. Likewise, generalized NFace class and subclasses for n-faces, which are stored inside Polytopes.
- """
- # Standard library imports
- from __future__ import annotations
- from functools import cached_property, reduce
- from itertools import combinations, cycle, islice
- from math import isclose
- from typing import List, Set, Tuple
- import weakref
- # Third-party imports
- import numpy as np
- import sympy
- from sympy import Rational
- # Local imports
- #from exceptions import SubfaceError
- class Polytope:
- """
- Superclass for n-polytopes. Such as polyhedra, polygons, line segments, and points.
- Each positional argument is a list. Vertices are a list of 3-tuples.
- And the rest are lists of integers to represent the indicies of the subfaces that form that n-face.
- Example for constructing a polyhedron:
- Polytope([vertices, edges, faces])
- """
- def __init__(self, subfaces: List[Polytope] = None, superfaces: List[Polytope] = None):
- if subfaces is None:
- self.subfaces = []
- else:
- self.subfaces = subfaces
- if superfaces is None:
- self.superfaces = []
- else:
- self.superfaces = weakref.proxy(superfaces)
- # def __eq__(self, other) -> bool:
- # if isinstance(other, Polytope):
- # return self.subfaces == self.subfaces
-
- # def __hash__(self):
- # return hash(self.subfaces)
-
- def __getitem__(self, idx) -> Polytope:
- """Index by subfaces."""
- return self.subfaces[idx]
- def __len__(self):
- """Return count of subfaces."""
- return len(self.subfaces)
- @property
- def rank(self) -> int:
- """The dimension of self."""
- if self.subfaces == []:
- return 0
- else:
- return next(iter(self.subfaces)).rank + 1
- @property
- def parents(self) -> List[Polytope]:
- """Alias for superfaces; that is, (n+1)-faces that contain self."""
- return self.superfaces
- @cached_property
- def siblings(self) -> List[Polytope]:
- """Return n-faces that share an (n+1)-face with self."""
- neighbours = set()
- for superface in self.superfaces:
- neighbours = neighbours.union(superface.subfaces)
- neighbours.remove(self)
- return list(neighbours)
- @cached_property
- def neighbours(self) -> List[Polytope]:
- """Return n-faces that share an (n-1)-face with self."""
- neighbours = set()
- for subface in self.subfaces:
- neighbours = neighbours.union(subface.superfaces)
- neighbours.remove(self)
- return list(neighbours)
- @property
- def children(self) -> List[Polytope]:
- """Alias for subfaces; that is, (n-1)-faces that self contains."""
- return self.subfaces
- @property
- def facets(self) -> List[Polytope]:
- """(n-1)-faces."""
- return self.subfaces
- @property
- def ridges(self) -> List[Polytope]:
- """(n-2)-faces."""
- return self.nfaces(self.rank - 2)
- @property
- def peaks(self) -> List[Polytope]:
- """(n-3)-faces."""
- return self.nfaces(self.rank - 3)
- def nfaces(self, rank) -> List[Polytope]:
- """Return n-faces, where n is the "rank" argument, that are within self or that self is within."""
- if rank > self.rank:
- faces = self.superfaces
- superfaces = set()
- while True:
- if next(iter(faces)).rank == rank:
- return list(faces)
- for superface in faces:
- superfaces = superfaces.union(superface.superfaces)
- faces = superfaces
- superfaces = set()
- elif rank == self.rank:
- return self
- else:
- faces = self.subfaces
- subfaces = set()
- while True:
- if next(iter(faces)).rank == rank:
- return list(faces)
- for subface in faces:
- subfaces = subfaces.union(subface.subfaces)
- faces = subfaces
- subfaces = set()
- @property
- def cells(self) -> List[Polytope]:
- """3-faces."""
- return self.nfaces(3)
- @property
- def faces(self) -> List[Polygon]:
- """2-faces."""
- return self.nfaces(2)
- @property
- def edges(self) -> List[LineSegment]:
- """1-faces."""
- return self.nfaces(1)
- @property
- def vertices(self) -> List[Point]:
- """0-faces."""
- if next(iter(self.subfaces)) == []:
- return self.subfaces
- return self.nfaces(0)
- @property
- def ambient_dimension(self) -> int:
- """The number of dimensions in which the polytope resides in.
- Note that this is not always the dimensionality of the shape itself,
- such as a square in a 3D space."""
- return self.vertices[0].ambient_dimension
- @property
- def centroid(self) -> Point:
- """Average of positions in element."""
- positions = [vertex.position for vertex in self.vertices]
- centroid = []
- for axis in range(len(positions[0])):
- centroid.append(reduce(lambda a, b: a + b[axis], positions, 0)*Rational(1, len(positions)))
- return Point(centroid)
- def stats(self) -> None:
- """Print number of vertices, edges, faces, etc."""
- nfaces = ["Vertices", "Edges", "Faces", "Cells"]
- for rank in range(0, self.rank):
- if rank <= 3:
- nface = nfaces[rank]
- else:
- nface = f"{rank}-face"
- count = len(self.nfaces(rank))
- print(f"{nface}: {count}")
-
- class Polyhedron(Polytope):
- """The 3-polytope."""
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- def face_types(self) -> None:
- """Print counts of each n-gon."""
- polygon_names = {3:"triangles", 4:"quadrilaterals", 5:"pentagons", 6:"hexagons", 7:"heptagons",
- 8:"octagons", 9:"nonagons", 10:"decagons", 11:"undecagons", 12:"dodecagons"}
- polygon_counts = {}
- for face in self.faces:
- if len(face.vertices) in polygon_counts:
- polygon_counts[len(face.vertices)] += 1
- else:
- polygon_counts[len(face.vertices)] = 1
- for key, value in polygon_counts.items():
- if key in polygon_names:
- print(f"{polygon_names[key]}: {value}")
- else:
- print(f"{key}-gon: {value}")
- def is_canonical(self) -> bool:
- """Return whether Polyhedron has midsphere. That is to say whether all edges form lines tangent to the same sphere."""
- midradius = None
- for edge in self.edges:
- vertex1 = list(edge.vertices)[0]
- vertex2 = list(edge.vertices)[1]
- line = sympy.Line3D(sympy.Point3D(vertex1.coordinates), sympy.Point3D(vertex2.coordinates))
- distance = line.distance(sympy.Point3D(0, 0, 0))
- if midradius is None:
- midradius = distance
- elif not isclose(float(midradius), float(distance)):
- return False
- return True
- ### Operations and closely associated functions ###
- @staticmethod
- def __add_to_list(point, new_vertices, new_face):
- """Test if point already in new_vertices, and if not, add it."""
- try:
- index = new_vertices.index(point)
- new_face.append(index)
- except ValueError:
- new_vertices.append(point)
- new_face.append(len(new_vertices) - 1)
- @staticmethod
- def diminish(func, vertex, new_vertices):
- """Remove pyramid off polyhedron where apex is a vertex on the polyhedron.
- Takes a func to determine how base of pyramid is formed.
- Used in truncate(), rectify(), and facet().
- """
- # Create new faces (new faces derived from previous vertices)
- unordered_face = []
- for neighbour in vertex.neighbours:
- midpoint = func(LineSegment([Point(vertex.coordinates), Point(neighbour.coordinates)]))
- unordered_face.append((midpoint, neighbour))
- # Find edges by comparing endpoint faces
- edges = []
- for midpoint1, endpoint1 in unordered_face:
- for midpoint2, endpoint2 in unordered_face:
- if midpoint1 != midpoint2:
- for end_face in endpoint1.faces:
- if end_face in endpoint2.faces:
- edges.append((midpoint1, midpoint2))
- # Order vertices in face
- face_vertices = [edges[0][0]]
- for _ in edges:
- for edge in edges:
- if edge[0] == face_vertices[-1] and edge[1] not in face_vertices:
- face_vertices.append(edge[1])
- break
-
- # Create new faces and vertices
- new_face = []
- for vertex in face_vertices:
- index = new_vertices.index(vertex)
- new_face.append(index)
- return new_face
-
- def truncate(self):
- """Perform truncation operation. Cleave vertices by marking 1/3rd and 2/3rds of each edge as new vertices."""
- print("Truncating")
- new_vertices = []
- new_faces = []
- # Create truncated faces (new faces derived from previous faces)
- for face in self.faces:
- new_face = []
- # Create new vertices and faces
- for x in range(len(face.vertices)):
- coordinates1 = face.vertices[x - 1].coordinates
- coordinates2 = face.vertices[x].coordinates
- third_forward = LineSegment([Point(coordinates1), Point(coordinates2)]).find_third()
- third_backward = LineSegment([Point(coordinates2), Point(coordinates1)]).find_third()
- self.__add_to_list(third_forward, new_vertices, new_face)
- self.__add_to_list(third_backward, new_vertices, new_face)
- new_faces.append(new_face)
- # Create new faces (new faces derived from previous vertices)
- for vertex in self.vertices:
- new_face = self.diminish(LineSegment.find_third, vertex, new_vertices)
- new_faces.append(new_face)
- return create_polytope(new_vertices, new_faces)
- def rectify(self, method="by_midsphere"):
- """Perform rectification operation. This diminishes the polyhedron at vertices such edges are converted into new vertices.
- The default implementation uses the intersections of the edges to the polyhedron's midsphere to place new vertices.
- And thus, this only works on polyhedra that have a midsphere.
- An alternate method can be used by setting the parameter method to "by_midpoint".
- This method doesn't require a midsphere and instead cleaves vertices by marking midpoints of edges as new vertices.
- For uniform polyhedra, this produces the same results as the midsphere method; however, for nonuniform polyhedra,
- this can result in nonplanar faces."""
- print("Rectifying")
- # Decide on method of rectification
- if method == "by_midsphere":
- if not self.is_canonical():
- print("Polyhedron is not canonical; must have a midsphere to rectify.")
- return self
- create_new_vertices = LineSegment.find_midsphere_intersection
- elif method == "by_midpoint":
- create_new_vertices = LineSegment.find_midpoint
- else:
- print("Not a valid option for parameter alt_method.")
-
- # Arrays for new polyhedron's vertices, edges, and faces
- new_vertices = []
- new_faces = []
- # Create rectified faces (new faces derived from previous faces)
- for face in self.faces:
- new_face = []
- # Find new vertices
- offset_cycle = islice(cycle(face.vertices), 1, None)
- for vertex, neighbour in zip(face.vertices, offset_cycle):
- midpoint = create_new_vertices(LineSegment([Point(vertex.coordinates), Point(neighbour.coordinates)]))
- # Test if midpoint is already in new_vertices, and if not, add it
- self.__add_to_list(midpoint, new_vertices, new_face)
- new_faces.append(new_face)
-
-
- # Create new faces (new faces derived from previous vertices)
- for vertex in self.vertices:
- new_face = self.diminish(create_new_vertices, vertex, new_vertices)
- new_faces.append(new_face)
- return create_polytope(new_vertices, new_faces)
- def facet(self):
- """Perform facet operation. Maintain all previous vertices, but connect them differently to form new faces on a nonconvex figure."""
- print("Faceting")
- def keep_only_neighbour(vertex1, vertex2):
- return vertex2
- new_faces = []
- new_vertices = [vertex.coordinates for vertex in self.vertices]
-
- # Create new faces (new faces derived from previous vertices)
- for vertex in self.vertices:
- new_face = self.diminish(keep_only_neighbour, vertex, new_vertices)
- new_faces.append(new_face)
- return create_polytope(new_vertices, new_faces)
- def reciprocate(self):
- """Perform reciprocation operation. Convert centroid of each face into a vertex and connect each adjacent centroid.
- This implementation creates skew faces on some polyhedra."""
- print("Reciprocating")
- def find_centroid(vertices_coordinates):
- return tuple(sum(np.array(vertices_coordinates))/len(vertices_coordinates))
- new_vertices = []
- new_faces = []
- # Create new vertices and faces
- for vertex in self.vertices:
- new_face = []
- for face in vertex.faces:
- centroid = find_centroid([vertex.coordinates for vertex in face.vertices])
- # Test if centroid is already in new_vertices, and if not, add it
- self.__add_to_list(centroid, new_vertices, new_face)
- new_faces.append(new_face)
- return create_polytope(new_vertices, new_faces)
- @staticmethod
- def augment(func, face, new_vertices):
- print("Under development")
- pass
- def cap(self):
- print("Under development")
- return self
- def bridge(self):
- print("Under development")
- return self
- def stellate(self, nth_stellation: int = 2):
- """Extends edges until meeting other edges, creating new vertices and changing shape of faces.
- The base polyhedron is designated as the first stellation, or nth_stellation=1."""
- print("Under development")
-
- # Edge needs to be of form [(1.0, 1.0, 1.0), (1.5, 2.0, 1.5)]
- # Create lines from edges
- lines = [sympy.Line(*edge) for edge in self.edges]
- midpoints = [Point(edge.midpoint) for edge in self.edges]
- new_edges = []
- for idx, line1 in enumerate(lines):
- intersections = []
- for line2 in lines:
- if line1 != line2:
- intersection = line1.intersection(line2)
- if intersection:
- intersections.append(intersection[0])
- distances = [intersection.distance(midpoints[idx]) for intersection in intersections]
- #print("Distances", distances)
- try:
- mins = sorted(list(set(distances)))
- endpoints = []
- for idx, distance in enumerate(distances):
- if distance == mins[nth_stellation]:
- endpoints.append(idx)
- new_edges.append(tuple(endpoints))
- except:
- print("Stellation of order", nth_stellation, "does not exist.")
- return self
- def greaten(self):
- """Extend faces to form new larger faces."""
- print("Under development")
- def intersection(face1):
- """The line that two face meet along."""
- pass
- # Find intersections of neighbours of each face.
- for face in self.faces:
- for face1, face2 in combinations([neighbour for neighbour in face.neighbours], 2):
- edge = intersection(face1, face2)
- return self
- def decompose(self):
- """Slice polyhedron into two or more parts. If not decomposable, return self."""
- print("Under development")
- return self
- def uncouple(self):
- """Separate compound polyhedra."""
- print("Under development")
- return self
- composititions = []
- connected_faces = []
- def neighbour_recursion(connected_faces, face):
- for neighbour in face.neighbours:
- if neighbour not in connected_faces:
- connected_faces.append(neighbour)
- neighbour_recursion(neighbour)
- neighbour_recursion(self.faces[0].neighbours)
- if len(connected_faces) == len(self.faces):
- return polyhedron
- else:
- return Polyhedron(self.vertices, connected_faces) # change new vertices
- class Polygon(Polytope):
- """The 2-polytope."""
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- @cached_property
- def vertices(self):
- """Return ordered vertices."""
- edges = {*self.edges}
- first_edge = edges.pop()
- vertices = [first_edge.vertices[0], first_edge.vertices[1]]
- while len(edges) > 1:
- for edge in edges:
- try:
- vertex_idx = edge.vertices.index(vertices[-1])
- if vertex_idx == 0:
- neighbour_idx = 1
- else:
- neighbour_idx = 0
- vertices.append(edge.vertices[neighbour_idx])
- edges.remove(edge)
- break
- except:
- pass
- return vertices
- class LineSegment(Polytope):
- """The 1-polytope."""
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- @property
- def midpoint(self):
- """Alias for centroid."""
- return self.centroid
- def find_third(self):
- return ((self[0][0]*Rational(2, 3) + self[1][0]*Rational(1, 3)), # x coordinate
- (self[0][1]*Rational(2, 3) + self[1][1]*Rational(1, 3)), # y coordinate
- (self[0][2]*Rational(2, 3) + self[1][2]*Rational(1, 3))) # z coordinate
- def find_midsphere_intersection(self):
- """"Use sympy to find point on line closest to origin."""
- line = sympy.Line3D(sympy.Point3D(self[0].coords), sympy.Point3D(self[1].coords))
- point = line.projection(sympy.Point3D(0, 0, 0))
- return point.coordinates
- class Point(Polytope):
- """The 0-polytope."""
- def __init__(self, coords: Tuple[float]):
- super().__init__()
- self.coordinates = coords
- # def __eq__(self, other) -> bool:
- # """Overide class Polytope implementation."""
- # return isinstance(other, Point) and self.coords == self.coords
- # def __hash__(self):
- # return hash(self.coords)
- def __str__(self):
- return "Point: " + str(self.coordinates)
- def __len__(self):
- return len(self.coordinates)
- def __getitem__(self, idx) -> Polytope:
- """Override class Polytope implementation. Index by coordinate."""
- return self.coordinates[idx]
- @property
- def children(self):
- # Overides the corresponding method for superclass NFace since vertices don't have subfaces.
- #raise SubfaceError("Vertices don't have subfaces.")
- pass
- @cached_property
- def neighbours(self) -> List[Point]:
- """Overide class Polytope implementation. Because vertices can't share a subface (for not having any),
- the closest idea of neighbours are those that share an edge.
- This usage of the term "neighbours" is used in graph theory and so applied here."""
- return self.siblings
- @property
- def coords(self):
- """Alias for coordinates."""
- return self.coordinates
- @property
- def position(self):
- """Alias for coordinates."""
- return self.coordinates
- @property
- def ambient_dimension(self):
- return len(self.position)
- def create_polytope(*elements, with_edges=False) -> Polytope:
- """Construct instance of class Polytope."""
- def create_with_edges(input_faces):
- """Take faces referencing vertices and convert to faces referencing edges and edges referencing vertices."""
- # Convert input element tuples into type Polytope or one of its subclasses
- new_edges = []
- new_faces = []
- # Find new edges and faces
- for face_by_indices in input_faces:
- offset_cycle = islice(cycle(face_by_indices), 1, None)
- new_face = []
- for vertex_idx, neighbour_idx in zip(face_by_indices, offset_cycle):
- directed_edge = (vertex_idx, neighbour_idx)
- if directed_edge in new_edges:
- idx = new_edges.index(directed_edge)
- elif tuple(reversed(directed_edge)) in new_edges:
- idx = new_edges.index(tuple(reversed(directed_edge)))
- else:
- idx = len(new_edges)
- new_edges.append(directed_edge)
- new_face.append(idx)
- new_faces.append(new_face)
- return new_edges, new_faces
- elements_by_indices = list(elements)
- if not with_edges:
- new_edges, new_faces = create_with_edges(elements[1])
- # Assign new edges and faces to elements
- elements_by_indices.insert(1, new_edges)
- elements_by_indices[2] = new_faces
- else:
- elements_by_indices = elements
- nfaces = [[] for rank in elements_by_indices]
- for rank, elements in enumerate(elements_by_indices):
- for nface in elements:
- # Set class to instantiate as
- if rank == 0:
- NFace = Point
- elif rank == 1:
- NFace = LineSegment
- elif rank == 2:
- NFace = Polygon
- elif rank == 3:
- NFace = Polyhedron
- else:
- NFace = Polytope
- # Build instance
- if rank == 0:
- nfaces[rank].append(NFace(nface))
- else:
- subfaces = []
- # Add subfaces for n-face
- for subface_idx in nface:
- subfaces.append(nfaces[rank - 1][subface_idx])
- cur_face = NFace(subfaces=subfaces)
- nfaces[rank].append(cur_face)
- # Add superface to (n-1)-faces (except for facet)
- for subface in subfaces:
- subface.superfaces.append(cur_face)
- # Create and return outermost container for all n-faces
- if len(elements_by_indices) == 2:
- polytope = Polygon(subfaces=nfaces[-1])
- elif len(elements_by_indices) == 3:
- polytope = Polyhedron(subfaces=nfaces[-1])
- elif len(elements_by_indices) > 3:
- polytope = Polytope(subfaces=nfaces[-1])
-
- # Add polytope as superface to facets
- for facet in nfaces[-1]:
- facet.superfaces.append(polytope)
- # Create and return outermost container for all n-faces
- return polytope
|