"""Module 'ifs' defines the indexed face set
"""
import copy
import numpy as np
from ddg.indexedfaceset import _utils
[docs]class GeneralizedIndexedFaceSet:
"""
Implementation of a combinatorial indexed face set data structure
with a list of faces and some utility functions.
Parameters
----------
faces: list
List of face tuples that will generate the indexed face set.
"""
def __init__(self, faces=[]):
"""
Construct an GeneralizedIndexedFaceSet with a list of faces
Parameters
----------
faces: list of tuples
List of face tuples that will generate the indexed face set.
"""
self._edge_list = set()
self._face_boundaries = dict()
self._edge_face_map = dict()
self._face_list = [tuple(f) for f in faces]
self._face_dict = {tuple(f): True for f in faces}
self._oriented = False
self._init_edge_face_map()
self.general_vertex_attributes = dict()
self.general_edge_attributes = dict()
self.general_face_attributes = dict()
[docs] def get_vertex_set(self):
"""
Returns the set of vertices of an indexed face set
"""
vertex = set()
for face in self.face_list():
for vert in face:
vertex.add(vert)
return vertex
def _return_cells(self, cell_type):
"""
Private utility method to return the corresponding cells and their
corresponding instance attribute name from a given cell type
Parameters
----------
cell_type : str
The cell type to be returned
Returns
-------
cells : set()
the cell of cells corresponding to the cell type
attribute_string : str
the name of the attribute corresponding to the cell type
"""
if cell_type == "verts":
cells = self.get_vertex_set()
attribute_string = "general_vertex_attributes"
elif cell_type == "edges":
cells = self.edge_set()
attribute_string = "general_edge_attributes"
elif cell_type == "faces":
cells = self.face_list()
attribute_string = "general_face_attributes"
else:
raise ValueError(
"Cell type must be one the following: 'verts', 'edges' or 'faces'"
)
return cells, attribute_string
[docs] def set_attribute(self, attr_name, cell_type, attribute, cell_subset=None):
"""
Method to add attributes to cells of an ifs. If the attribute already
exists, it changes it. If only a subset is specified, it only updates
the subset values.
Parameters
----------
cell_type: str
Type of cell to which the attribute must be added
attr_name : str
Name of the attribute
attribute : Subscriptable object
A subscriptable object of the attributes to be assigned to each vertex
cell_subset: set or list depending on the cell type
if the cells in question are not the whole cells of the ifs but a
subset of it, they must be given as input in this parameter
Returns
-------
dict
A nested dictionary of all attributes of vertices
"""
if not isinstance(attr_name, str):
raise TypeError("add_attribute: The attribute name must be of type 'str'")
all_cells, attribute_string = self._return_cells(cell_type)
if cell_subset is None:
cells = all_cells
else:
cells = cell_subset
if isinstance(cells, (list, np.ndarray)) and len(cells) != len(attribute):
raise ValueError(
f"The sizes of the attribute list {attribute} is {len(attribute)} but"
f" the size of cells list {cells} is {len(cells)}. Size mismatch"
)
general_dict = getattr(self, attribute_string)
specific_attr_dict = dict()
if attr_name in general_dict.keys(): # and cell_subset is not None:
specific_attr_dict = general_dict[attr_name]
# Case: Single cell
if cell_subset is not None and isinstance(cell_subset, (int, tuple, float)):
if cell_subset not in all_cells:
raise ValueError(
f"The cell '{cell_subset}' could not be found in the self.__name__"
)
specific_attr_dict[cells] = attribute
else:
for index, cell in enumerate(cells):
specific_attr_dict[cell] = attribute[index]
general_dict[attr_name] = specific_attr_dict
return
[docs] def delete_attribute(self, attr_name, cell_type):
"""
Deletes the attribute given by its name
Parameters
----------
attr_name : str
Name of the attribute
cell_type: str
Type of cell to which the attribute must be added
"""
_, attribute_string = self._return_cells(cell_type)
general_dict = getattr(self, attribute_string)
if attr_name not in general_dict.keys():
raise ValueError(f"The name '{attr_name}' does not exist as an attribute")
else:
general_dict.pop(attr_name)
[docs] def get_attribute(self, attr_name, cell_type, cell):
"""
get cell attribute
Parameters
----------
attr_name : str
Name of the attribute. Represents keys for the attribute dictionaries
cell_type : str
Type of cell from which the attribute is to be retrieved
cell : float or int for verts, tuples for edges and faces
The cell to which the attribute is assigned. Type depends on the
type of the inputs given to create the class.
Returns
-------
The attribute in the specified position. Type depends on the attribute type.
"""
cells, attribute_string = self._return_cells(cell_type)
attribute = getattr(self, attribute_string)
if cell not in cells:
raise ValueError(f"The cell ''{cell}'' could not be found in the surface")
return attribute[attr_name][cell]
def _valid_face(self, face):
if face not in self._face_dict:
raise ValueError(
f"Face {face} is not a face of the GeneralizedIndexedFaceSet"
)
def _generate_edge_set(self):
self._edge_list = set()
for face in self._face_list:
boundary = self.face_boundary(face)
for boundary_edge in boundary:
boundary_edge = tuple(sorted(boundary_edge))
self._edge_list.add(boundary_edge)
def _add_face_boundary_to_map(self, face):
self._face_boundaries[face] = _utils.face_boundary(face)
def _init_edge_face_map(self):
for face in self._face_list:
for boundary_edge in self.face_boundary(face):
self._add_to_edge_face_map(boundary_edge, face)
def _add_to_edge_face_map(self, edge, face):
if edge not in self._edge_face_map:
self._edge_face_map[edge] = [face]
else:
# oriented face sets can only be called by manifold structures
if self._oriented:
raise ValueError(
"The face "
+ str(face)
+ " uses an edge in the same orientation as a previous face of "
+ str(self._face_list)
)
# non oriented face sets can be anything for now unless you ask for
# is_manifold
self._edge_face_map[edge].append(face)
def _update_edge_face_map(self, face):
for boundary_edge in _utils.face_boundary(face):
self._add_to_edge_face_map(boundary_edge, face)
[docs] def adjacent_faces_with_orientation(self, edge):
"""
Get a list of oriented faces adjacent to an given edge.
The resulting faces are of the class OrientedFace with self.orientation = 1
if they contain the edge in the given orientation or self.orientation = -1
if they contain the edge in reversed orientation.
Parameters
----------
edge : tuple
Tuple of two integers.
Returns
-------
list
List of instances of the class OrientedFace that are adjacent to the
input edge.
"""
adjacent_faces = []
if edge in self._edge_face_map.keys():
adjacent_faces += [
OrientedFace(f, orientation=1) for f in self._edge_face_map[edge]
]
reversed_edge = tuple(reversed(edge))
if reversed_edge in self._edge_face_map.keys():
adjacent_faces += [
OrientedFace(f, orientation=-1)
for f in self._edge_face_map[reversed_edge]
]
return adjacent_faces
[docs] def adjacent_faces(self, edge):
"""
Get a list of faces, i.e. tuples adjacent to an given edge.
Parameters
----------
edge : tuple
Tuple of two integers.
Returns
-------
list
List of tuples that represent all adjacent faces despite of orientation.
"""
return [f.get_tuple() for f in self.adjacent_faces_with_orientation(edge)]
[docs] def opposite_face(self, face, edge):
"""
Get the other face adjacent to a given edge of a face.
If the edge is adjacent to only one or more than two
faces return None.
Parameters
----------
edge : tuple
Tuple of two integers.
face : tuple
The input face incuding the edge from which the other face
will be determined.
Returns
-------
tuple
The other face incident to the given edge.
None
If edge is adjacent to only one or more than two faces.
"""
return self.opposite_face_with_orientation(face, edge).get_tuple()
[docs] def opposite_face_with_orientation(self, face, edge):
"""
Get the other oriented face adjacent to a given edge of a face.
If the edge is adjacent to only one or more than two
faces return None.
Parameters
----------
edge : tuple
Tuple of two integers.
face : tuple
The input face incuding the edge from which the other face
will be determined.
Returns
-------
OrientedFace
The other face incident to the given edge.
NoneFace
If edge is adjacent to only one or more than two faces.
"""
self._valid_face(face)
if edge not in self.face_boundary(face):
raise ValueError("Edge is not an edge of the face")
adjacent_faces = [
f
for f in self.adjacent_faces_with_orientation(edge)
if f.get_tuple() != face
]
if len(adjacent_faces) != 1:
return NoneFace()
return adjacent_faces[0]
[docs] def neighboring_faces(self, face):
"""
Get all faces that are adjacent to a given a face.
If an edge has none or more than one other neighbouring faces,
None will be returned.
Parameters
----------
face : tuple
The input face whose neighbouring faces will be determined.
Returns
-------
list of tuple
List of neighbouring faces in cyclic order.
"""
return [f.get_tuple() for f in self.neighboring_faces_with_orientation(face)]
[docs] def neighboring_faces_with_orientation(self, face):
"""
Get all oriented faces that are adjacent to a given a face.
If an edge has none or more than one other neighbouring faces,
None will be returned.
Parameters
----------
face : tuple
The input face whose neighbouring faces will be determined.
Returns
-------
list of OrientedFace
List of neighbouring faces in cyclic order.
"""
if face not in self._face_dict:
raise ValueError("Face is not a face of the GeneralizedIndexedFaceSet")
neighboring_faces = [
self.opposite_face_with_orientation(face, e)
for e in self.face_boundary(face)
if not isinstance(self.opposite_face_with_orientation(face, e), NoneFace)
]
# remove double entries from neighboring faces with multiple common edges
neighboring_faces = list(dict.fromkeys(neighboring_faces))
return neighboring_faces
[docs] def face_list(self):
"""
Get all faces of the indexed face set.
Returns
-------
list
List of tuples representing the faces of the indexed face set.
"""
return self._face_list
[docs] def number_of_faces(self):
"""
Get the number of faces of the indexed face set.
Returns
-------
int
Number of faces of the indexed face set.
"""
return len(self._face_list)
[docs] def edge_set(self):
"""
Get all edges of the indexed face set.
Returns
-------
set
Unordered set of tuples of two integers representing all edges.
"""
if not self._edge_list:
self._generate_edge_set()
return copy.deepcopy(self._edge_list)
[docs] def face_boundary(self, face):
"""
Returns a set of edges bounding the face.
The edges are tuples orded by the orientation of the face.
Parameters
----------
face : tuple
The face to be investigated.
Returns
-------
tuple
tuple with ordered edge tuples
Examples
--------
>>> from ddg.indexedfaceset import GeneralizedIndexedFaceSet
>>> faceSet = GeneralizedIndexedFaceSet([(1, 2, 4), (2, 3, 4)])
>>> faceSet.face_boundary((1, 2, 4))
((1, 2), (2, 4), (4, 1))
"""
face_tuple = face
if face_tuple not in self._face_boundaries:
self._add_face_boundary_to_map(face)
return self._face_boundaries[face_tuple]
[docs] def add_face(self, face):
"""
Adds face to the indexed face set.
Simply adds the face without paying attention to the maifold property.
Parameters
----------
face : tuple
The face to be investigated.
"""
self._face_list.append(face)
self._face_dict[face] = True
self._update_edge_face_map(face)
[docs]class OrientedIndexedFaceSet(GeneralizedIndexedFaceSet):
def __init__(self, face_list=[]):
super(OrientedIndexedFaceSet, self).__init__(face_list)
self._oriented = True
[docs] def adjacent_faces(self, edge):
"""
Get a list of faces adjacent to an edge given as a tuple
of integers. The faces are ordered such that they contain
the given edge in the given orientation.
"""
adjacent_faces = []
if edge in self._edge_face_map:
adjacent_faces += self._edge_face_map[edge]
return adjacent_faces
[docs] def add_face(self, face):
if self.number_of_faces() == 0 or self._is_addable_face(face):
super(OrientedIndexedFaceSet, self).add_face(face)
else:
raise ValueError(
"Cannot add given face "
+ str(face)
+ " to oriented IFS with faces "
+ str(self._face_list)
)
def _is_addable_face(self, face):
for edge in self.face_boundary(face):
if edge in self._edge_face_map:
return False
return True
[docs]class OrientedFace:
def __init__(self, index_tuple, orientation=1):
self._tuple = index_tuple
self._orientation = orientation
self._oriented_tuple = (
tuple(self._tuple)
if (self._orientation == 1)
else tuple(reversed(self._tuple))
)
[docs] def get_orientation(self):
return self._orientation
[docs] def get_oriented_tuple(self):
return self._oriented_tuple
[docs] def get_tuple(self):
return self._tuple
[docs] def set_orientation(self, new_orientation):
if new_orientation != self._orientation:
self._orientation = new_orientation
if self._orientation == 1:
self._oriented_tuple = tuple(self._tuple)
else:
self._oriented_tuple = tuple(reversed(self._tuple))
def __hash__(self):
return hash(self.get_oriented_tuple())
def __eq__(self, f):
"""
Checks for equality of tuples up to cyclic permutation.
"""
a = self.get_oriented_tuple()
b = f.get_oriented_tuple()
return np.any([(np.roll(a, i) == b).all() for i in range(len(a))])
[docs]class IndexedFaceSet(GeneralizedIndexedFaceSet):
"""
An indexed face set whose vertices must be integers of indices 0 to n-1
only. The attributes of cells for this class are also restricted to numpy
arrays only.
"""
def __init__(self, faces=[]):
super(IndexedFaceSet, self).__init__(faces)
if any(
[
not isinstance(vertex, (int, np.integer))
for vertex in self.get_vertex_set()
]
):
raise ValueError(
"The vertices of an IndexedFaceSet must be given as integers from 0 to"
" n-1 only"
)
if not self._check_enumeration():
raise ValueError(
"The vertices of an IndexedFaceSet must be given as integers from 0 to"
" n-1 only"
)
self.verts = np.array(list(self.get_vertex_set()))
edge_list = [tuple(e) for e in self.edge_set()]
self.edges = np.array(edge_list)
self.faces = np.array(self.face_list(), dtype=object)
self.face_dict = self._create_face_dict()
self.vertex_attributes = dict()
self.edge_attributes = dict()
self.face_attributes = dict()
def _check_enumeration(self):
number_of_vertices = len(self.get_vertex_set())
return self.get_vertex_set() == set(range(number_of_vertices))
def _create_face_dict(self):
"""
Creates a dictionary of the faces with the number of edges of each face
as keys and the vertex list as value
"""
face_dict = dict()
for element in self.face_list():
value = np.array(element)
key = str(len(element))
if key in face_dict:
face_dict[key] = np.vstack([face_dict[key], value])
else:
face_dict[key] = value
return face_dict
def _return_cells(self, cell_type):
"""
Method to return the corresponding cells and their corresponding
instance attribute name from a given cell type
Parameters
----------
cell_type : str
The cell type to be returned
Returns
-------
cells : set()
the cell of cells corresponding to the cell type
attribute_string : str
the name of the attribute corresponding to the cell type
"""
if cell_type == "verts":
cells = self.verts
attribute_string = "vertex_attributes"
elif cell_type == "edges":
cells = self.edges
attribute_string = "edge_attributes"
elif cell_type == "faces":
cells = self.faces
attribute_string = "face_attributes"
else:
raise ValueError(
"Cell type must be one the following: 'verts', 'edges' or 'faces'"
)
return cells, attribute_string
[docs] def cell_index(self, cell_type, cell):
"""
Method to get the index of the given cell. Vertices are already
indices. Mostly used to get the cell of edges and faces
Parameters
----------
cell_type : str
type of cells to be indiced. "verts", "edges" or "faces" are the
proper values.
cell : int for verts, tuples for edges and faces
the cell to be indiced.
Returns
-------
The index of the cell
"""
cells = getattr(self, cell_type)
for index, cell_iterator in enumerate(cells):
if cell_iterator == cell:
return index
raise ValueError(f"The given cell '{cell}' was not found in the list of cells")
[docs] def get_attribute(self, attr_name, cell_type, cell):
"""
'Get' method corresponding the attribute structure of the subclass to
override the same method in the parent class
Parameters
----------
cell_type : str
Type of cell from which the attribute are to be recalled
cell_type : str
Type of cell from which the attribute is to be retrieved
cell : int for verts, int or tuples for edges and faces
The cell or its index. For vertices, these two are the same. For
edges and faces these are different. Both form are accepted.
Returns
-------
numpy.ndarray
The attribute in the specified position.
"""
if cell_type == "verts":
if not isinstance(cell, int):
raise ValueError(
"For vertices the input cell ('{cell}') must be integers"
)
index = cell
else:
if isinstance(cell, int):
index = cell
elif isinstance(cell, tuple):
index = self.cell_index(cell_type, cell)
else:
raise ValueError(
f"The given cell '{cell}' for edges and faces must be either"
" integer index or the cell as tuple"
)
cells, attribute_string = self._return_cells(cell_type)
attr_dict = getattr(self, attribute_string)
if attr_name not in attr_dict.keys():
raise ValueError(
f"The attribute name {attr_name} does not exist in the attribute"
" dictionary"
)
return attr_dict[attr_name][index]
[docs] def set_attribute(self, attr_name, cell_type, attribute, dtype=float):
"""
Method to create a dictionary of attributes for a given cell type.
Parameters
----------
attr_name : str
Name of the attribute to get in string
attribute : np.ndarray
An array of the attributes to be set
Returns
-------
Attribute dictionary
"""
if isinstance(attribute, (list, int)):
attribute = np.array(attribute, dtype=dtype)
elif attribute is None:
raise ValueError("No attribute is given")
_, attribute_string = self._return_cells(cell_type)
attr_dict = getattr(self, attribute_string)
if len(attribute) != len(getattr(self, cell_type)):
raise ValueError(
f"The given attribute {attr_name} must have length"
f" '{len(getattr(self, cell_type))}' but it has '{len(attribute)}'"
)
attr_dict[attr_name] = attribute
[docs] def edge_vertex_list(self, vertex_index):
"""
Method to find the edges which share a specific vertex.
Parameters
----------
vertex_index : int
The index of the vertex to be found (0,..., n-1)
Returns
-------
numpy.ndarray
2D array of edges as tuples
"""
idx = np.any(self.edges == vertex_index, axis=1)
return np.arange(self.edges.shape[0])[idx]
[docs] def face_vertex_list(self, vertex_index):
"""
Method to find the faces which share a specific vertex
Parameters
----------
vertex_index : int
The index of the vertex to be found (0,..., n-1)
Returns
-------
numpy.ndarray
returns indices of faces
"""
return np.array(
[i for i, face in enumerate(self.faces) if vertex_index in face]
)
[docs] def face_vertex_dict(self, vertex_index):
"""
Method to create a dictionary which has as values the arrays of faces
with which they share a specific vertex and as keys the number of edges
of the faces
Parameters
----------
vertex_index : int
The index of the vertex to be found (0,..., n-1)
Returns
-------
dict
Keys : the number of edges of faces
Values : thoses faces with the key number of edges which share the
input vertex
"""
face_vertex_dict = dict()
face_vertex_list = self.face_vertex_list(vertex_index)
for index, element in enumerate(self.faces[face_vertex_list]):
key = str(len(element))
if key in face_vertex_dict:
face_vertex_dict[key] = np.append(
face_vertex_dict[key], np.array(face_vertex_list[index])
)
else:
face_vertex_dict[key] = np.array(face_vertex_list[index])
return face_vertex_dict
[docs] def face_edge_list(self, edge_index):
"""
Method to find the faces which share a specific edge
Parameters
----------
edge_index : int
The index of the edge
Returns
-------
numpy.ndarray
For non-uniform meshes the return value is a 1D array of faces as tuples
For uniform meshes the return value is a 2D array
"""
return np.array(
[
i
for i, face in enumerate(self.faces)
if (
self.edges[edge_index, 0] in face
and self.edges[edge_index, 1] in face
)
]
)
[docs] def is_boundary_vertex(self, vertex_index):
"""
checks if a vertex is a boundary vertex or not
Parameters
----------
vertex_index : 1D list or array
the index of the vertex in question
Returns
-------
True if the the vertex is a boundary vertex, otherwise False
"""
edge_list = self.edge_vertex_list(vertex_index)
face_list = self.face_vertex_list(vertex_index)
if len(edge_list) > len(face_list):
return True
else:
return False
@property
def boundary_vertices(self):
"""
Method to create a set of boundary vertices of the ifs
"""
boundary_verts = set()
for v in self.verts:
if self.is_boundary_vertex(v):
boundary_verts.add(v)
return boundary_verts
[docs]class NgonalIndexedFaceSet(IndexedFaceSet):
"""
An indexed face set whose faces are of constant valency of k.
"""
def __init__(self, faces=np.array([])):
"""Creates an IFS with constant face valency
Parameters
----------
faces : np.array
the list of faces of the surface
In case only one face exists, it should also be passed as a 2D array
"""
if not isinstance(faces, np.ndarray):
raise ValueError(
"NgonalIndexedFaceSet only accepts numpy arrays as face array."
)
if faces.ndim != 2:
raise ValueError(
"The face array must be a 2D numpy array. Even only one face exists, it"
" must be passed as a 2D array"
)
super(NgonalIndexedFaceSet, self).__init__(faces)
self.faces = self.faces.astype(faces.dtype)
[docs] def face_vertex_array(self, vertex_index):
"""
Method to find the faces which share a specific vertex. Overrides the
method in parent class.
Parameters
----------
vertex_index : int
The index of the vertex to be found (0,..., n-1)
Returns
-------
numpy.ndarray
returns indices of faces
"""
idx = np.any(self.edges == vertex_index, axis=1)
return np.arange(self.edges.shape[0])[idx]
[docs] def face_vertex_incidence(self):
"""
Method to create the valencies.
Returns
-------
list
the list of valencies in the same order of vertex index
"""
return [self.face_vertex_array(i) for i in range(len(self.verts))]
[docs] def face_edge_incidence(self):
"""
Method to create the valency for edge-face.
Returns
-------
list
the list of face-edge-valency in the same order of edge index
"""
return [self.face_edge_list(i) for i in range(len(self.edges))]
[docs]class NoneFace(OrientedFace):
def __init__(self):
super().__init__(index_tuple=())
self._tuple = None
self._oriented_tuple = None
self._orientation = 0