"""Helper functions to compute some properties of the mesh.
Based on meshmagick <https://github.com/LHEEA/meshmagick> by François Rongère.
"""
# Copyright (C) 2017-2019 Matthieu Ancellin, based on the work of François Rongère
# See LICENSE file at <https://github.com/mancellin/capytaine>
import numpy as np
[docs]
def compute_faces_properties(mesh):
"""Compute the faces properties of the mesh"""
# faces_areas, faces_normals, faces_centers = mm.get_all_faces_properties(mesh._vertices, mesh._faces)
nf = mesh.nb_faces
# triangle_mask = _faces[:, 0] == _faces[:, -1]
# nb_triangles = np.sum(triangle_mask)
# quads_mask = np.invert(triangle_mask)
# nb_quads = nf - nb_triangles
faces_areas = np.zeros(nf, dtype=float)
faces_normals = np.zeros((nf, 3), dtype=float)
faces_centers = np.zeros((nf, 3), dtype=float)
# Collectively dealing with triangles
# triangles = _faces[triangle_mask]
triangles_id = mesh.triangles_ids
triangles = mesh._faces[triangles_id]
triangles_normals = np.cross(mesh._vertices[triangles[:, 1]] - mesh._vertices[triangles[:, 0]],
mesh._vertices[triangles[:, 2]] - mesh._vertices[triangles[:, 0]])
triangles_normals_norm = np.linalg.norm(triangles_normals, axis=1)
degenerate_triangle = np.abs(triangles_normals_norm) < 1e-12
triangles_id = triangles_id[~degenerate_triangle]
triangles_normals = triangles_normals[~degenerate_triangle, :]
triangles_normals_norm = triangles_normals_norm[~degenerate_triangle]
triangles = triangles[~degenerate_triangle, :]
# Now, continue the computations without the degenerate triangles
faces_normals[triangles_id] = triangles_normals / triangles_normals_norm[:, np.newaxis]
faces_areas[triangles_id] = triangles_normals_norm / 2.
faces_centers[triangles_id] = np.sum(mesh._vertices[triangles[:, :3]], axis=1) / 3.
# Collectively dealing with quads
quads_id = mesh.quadrangles_ids
quads = mesh._faces[quads_id]
# quads = _faces[quads_mask]
quads_normals = np.cross(mesh._vertices[quads[:, 2]] - mesh._vertices[quads[:, 0]],
mesh._vertices[quads[:, 3]] - mesh._vertices[quads[:, 1]])
quads_normals_norm = np.linalg.norm(quads_normals, axis=1)
degenerate_quad = np.abs(quads_normals_norm) < 1e-12
quads_id = quads_id[~degenerate_quad]
quads_normals = quads_normals[~degenerate_quad]
quads_normals_norm = quads_normals_norm[~degenerate_quad]
quads = quads[~degenerate_quad, :]
# Now, continue the computations without the degenerate quads
faces_normals[quads_id] = quads_normals / quads_normals_norm[:, np.newaxis]
a1 = np.linalg.norm(np.cross(mesh._vertices[quads[:, 1]] - mesh._vertices[quads[:, 0]],
mesh._vertices[quads[:, 2]] - mesh._vertices[quads[:, 0]]), axis=1) * 0.5
a2 = np.linalg.norm(np.cross(mesh._vertices[quads[:, 3]] - mesh._vertices[quads[:, 0]],
mesh._vertices[quads[:, 2]] - mesh._vertices[quads[:, 0]]), axis=1) * 0.5
faces_areas[quads_id] = a1 + a2
c1 = np.sum(mesh._vertices[quads[:, :3]], axis=1) / 3.
c2 = (np.sum(mesh._vertices[quads[:, 2:4]], axis=1) + mesh._vertices[quads[:, 0]]) / 3.
faces_centers[quads_id] = (np.array(([a1, ] * 3)).T * c1 + np.array(([a2, ] * 3)).T * c2)
faces_centers[quads_id] /= np.array(([faces_areas[quads_id], ] * 3)).T
faces_radiuses = compute_radiuses(mesh, faces_centers)
return {'faces_areas': faces_areas,
'faces_normals': faces_normals,
'faces_centers': faces_centers,
'faces_radiuses': faces_radiuses,
}
[docs]
def compute_radiuses(mesh, faces_centers):
"""Compute the radiuses of the faces of the mesh.
The radius is defined here as the maximal distance between the center
of mass of a cell and one of its points."""
# Coordinates of all the vertices grouped by face
faces_vertices = mesh.vertices[mesh.faces, :]
# faces_vertices.shape == (nb_faces, 4, 3)
# Reorder the axes for array broadcasting below
faces_vertices = np.moveaxis(faces_vertices, 0, 1)
# faces_vertices.shape == (4, nb_faces, 3)
# Get all the vectors between the center of faces and their vertices.
radial_vector = faces_centers - faces_vertices
# radial_vector.shape == (4, nb_faces, 3)
# Keep the maximum length
faces_radiuses = np.max(np.linalg.norm(radial_vector, axis=2), axis=0)
# faces_radiuses.shape = (nb_faces)
return faces_radiuses
[docs]
def compute_connectivity(mesh):
"""Compute the connectivities of the mesh.
It concerns further connectivity than simple faces/vertices connectivities. It computes the vertices / vertices, vertices / faces and faces / faces connectivities.
Note
----
* Note that if the mesh is not conformal, the algorithm may not perform correctly
TODO: The computation of boundaries should be in the future computed with help of VTK
"""
nv = mesh.nb_vertices
nf = mesh.nb_faces
mesh_closed = True
# Building connectivities
# Establishing v_v and v_f connectivities
v_v = dict([(i, set()) for i in range(nv)])
v_f = dict([(i, set()) for i in range(nv)])
for (iface, face) in enumerate(mesh._faces):
if face[0] == face[-1]:
face_w = face[:3]
else:
face_w = face
for (index, iV) in enumerate(face_w):
v_f[iV].add(iface)
v_v[face_w[index - 1]].add(iV)
v_v[iV].add(face_w[index - 1])
# Connectivity f_f
boundary_edges = dict()
f_f = dict([(i, set()) for i in range(nf)])
for ivertex in range(nv):
set1 = v_f[ivertex]
for iadj_v in v_v[ivertex]:
set2 = v_f[iadj_v]
intersection = list(set1 & set2)
if len(intersection) == 2:
f_f[intersection[0]].add(intersection[1])
f_f[intersection[1]].add(intersection[0])
elif len(intersection) == 1:
boundary_face = mesh._faces[intersection[0]]
if boundary_face[0] == boundary_face[-1]:
boundary_face = boundary_face[:3]
ids = np.where((boundary_face == ivertex) + (boundary_face == iadj_v))[0]
if ids[1] != ids[0]+1:
i_v_orig, i_v_target = boundary_face[ids]
else:
i_v_target, i_v_orig = boundary_face[ids]
boundary_edges[i_v_orig] = i_v_target
else:
raise RuntimeError('Unexpected error while computing mesh connectivities')
# Computing boundaries
boundaries = list()
# TODO: calculer des boundaries fermees et ouvertes (closed_boundaries et open_boundaries) et mettre dans dict
while True:
try:
boundary = list()
i_v0_init, i_v1 = boundary_edges.popitem()
boundary.append(i_v0_init)
boundary.append(i_v1)
i_v0 = i_v1
while True:
try:
i_v1 = boundary_edges.pop(i_v0)
boundary.append(i_v1)
i_v0 = i_v1
except KeyError:
if boundary[0] != boundary[-1]:
print('Boundary is not closed !!!')
else:
boundaries.append(boundary)
break
except KeyError:
break
return {'v_v': v_v,
'v_f': v_f,
'f_f': f_f,
'boundaries': boundaries}
[docs]
def connected_components(mesh):
"""Returns a list of meshes that each corresponds to the a connected component in the original mesh.
Assumes the mesh is mostly conformal without duplicate vertices.
"""
from typing import Set, FrozenSet, List
vertices_components: Set[FrozenSet[int]] = set()
for set_of_v_in_face in map(frozenset, mesh.faces):
intersecting_components = [c for c in vertices_components if len(c.intersection(set_of_v_in_face)) > 0]
if len(intersecting_components) == 0:
vertices_components.add(set_of_v_in_face)
else:
for c in intersecting_components:
vertices_components.remove(c)
vertices_components.add(frozenset.union(set_of_v_in_face, *intersecting_components))
# Verification
for component in vertices_components:
assert all(len(component.intersection(c)) == 0 for c in vertices_components if c != component)
# The components are found. The rest is just about retrieving the faces in each components.
vertices_components: List[FrozenSet[int]] = list(vertices_components)
faces_components: List[List[int]] = [[] for _ in vertices_components]
for i_face, v_in_face in enumerate(mesh.faces):
for i_component, v_c in enumerate(vertices_components):
if any(v in v_c for v in v_in_face):
assert all(v in v_c for v in v_in_face)
faces_components[i_component].append(i_face)
break
components = [mesh.extract_faces(f) for f in faces_components]
return components
[docs]
def connected_components_of_waterline(mesh, z=0.0):
if np.any(mesh.vertices[:, 2] > z + 1e-8):
mesh = mesh.immersed_part(free_surface=z)
fs_vertices_indices = np.where(np.isclose(mesh.vertices[:, 2], z))[0]
fs_faces_indices = np.where(np.any(np.isin(mesh.faces, fs_vertices_indices), axis=1))[0]
crown_mesh = mesh.extract_faces(fs_faces_indices)
return connected_components(crown_mesh)