capytaine 2.3__cp310-cp310-macosx_14_0_arm64.whl → 3.0.0a1__cp310-cp310-macosx_14_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
- capytaine/.dylibs/libgfortran.5.dylib +0 -0
- capytaine/.dylibs/libquadmath.0.dylib +0 -0
- capytaine/__about__.py +7 -2
- capytaine/__init__.py +8 -12
- capytaine/bem/engines.py +234 -354
- capytaine/bem/problems_and_results.py +30 -21
- capytaine/bem/solver.py +205 -81
- capytaine/bodies/bodies.py +279 -862
- capytaine/bodies/dofs.py +136 -9
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/{libs/Delhommeau_float32.cpython-310-darwin.so → Delhommeau_float32.cpython-310-darwin.so} +0 -0
- capytaine/green_functions/{libs/Delhommeau_float64.cpython-310-darwin.so → Delhommeau_float64.cpython-310-darwin.so} +0 -0
- capytaine/green_functions/abstract_green_function.py +2 -2
- capytaine/green_functions/delhommeau.py +50 -31
- capytaine/green_functions/hams.py +19 -13
- capytaine/io/legacy.py +3 -103
- capytaine/io/xarray.py +15 -10
- capytaine/meshes/__init__.py +2 -6
- capytaine/meshes/abstract_meshes.py +375 -0
- capytaine/meshes/clean.py +302 -0
- capytaine/meshes/clip.py +347 -0
- capytaine/meshes/export.py +89 -0
- capytaine/meshes/geometry.py +244 -394
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +621 -676
- capytaine/meshes/predefined/cylinders.py +22 -56
- capytaine/meshes/predefined/rectangles.py +26 -85
- capytaine/meshes/predefined/spheres.py +4 -11
- capytaine/meshes/quality.py +118 -407
- capytaine/meshes/surface_integrals.py +48 -29
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/free_surfaces.py +1 -4
- capytaine/post_pro/kochin.py +10 -10
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/lists_of_points.py +2 -2
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/symbolic_multiplication.py +31 -5
- capytaine/tools/timer.py +68 -42
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine-3.0.0a1.dist-info/WHEEL +6 -0
- capytaine/bodies/predefined/__init__.py +0 -6
- capytaine/bodies/predefined/cylinders.py +0 -151
- capytaine/bodies/predefined/rectangles.py +0 -111
- capytaine/bodies/predefined/spheres.py +0 -70
- capytaine/green_functions/FinGreen3D/.gitignore +0 -1
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
- capytaine/green_functions/FinGreen3D/LICENSE +0 -165
- capytaine/green_functions/FinGreen3D/Makefile +0 -16
- capytaine/green_functions/FinGreen3D/README.md +0 -24
- capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
- capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
- capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
- capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
- capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/mesh_loaders.py +0 -1086
- capytaine/io/mesh_writers.py +0 -692
- capytaine/io/meshio.py +0 -38
- capytaine/matrices/__init__.py +0 -16
- capytaine/matrices/block.py +0 -592
- capytaine/matrices/block_toeplitz.py +0 -325
- capytaine/matrices/builders.py +0 -89
- capytaine/matrices/linear_solvers.py +0 -232
- capytaine/matrices/low_rank.py +0 -395
- capytaine/meshes/clipper.py +0 -465
- capytaine/meshes/collections.py +0 -334
- capytaine/meshes/mesh_like_protocol.py +0 -37
- capytaine/meshes/properties.py +0 -276
- capytaine/meshes/quadratures.py +0 -80
- capytaine/meshes/symmetric.py +0 -392
- capytaine/tools/lru_cache.py +0 -49
- capytaine/ui/vtk/__init__.py +0 -3
- capytaine/ui/vtk/animation.py +0 -329
- capytaine/ui/vtk/body_viewer.py +0 -28
- capytaine/ui/vtk/helpers.py +0 -82
- capytaine/ui/vtk/mesh_viewer.py +0 -461
- capytaine-2.3.dist-info/RECORD +0 -92
- capytaine-2.3.dist-info/WHEEL +0 -4
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
capytaine/meshes/geometry.py
CHANGED
|
@@ -1,409 +1,259 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
""
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# Copyright 2025 Mews Labs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import List
|
|
16
|
+
from functools import reduce
|
|
17
|
+
from itertools import chain
|
|
9
18
|
|
|
10
19
|
import numpy as np
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
from numpy.typing import NDArray
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_vertices_face(face, vertices):
|
|
24
|
+
if len(face) == 4 and face[2] != face[3]:
|
|
25
|
+
return (
|
|
26
|
+
vertices[face[0]],
|
|
27
|
+
vertices[face[1]],
|
|
28
|
+
vertices[face[2]],
|
|
29
|
+
vertices[face[3]],
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
return vertices[face[0]], vertices[face[1]], vertices[face[2]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compute_faces_normals(vertices, faces):
|
|
36
|
+
normals = []
|
|
37
|
+
for face in faces:
|
|
38
|
+
if len(face) == 4 and face[2] != face[3]:
|
|
39
|
+
normal = _quad_normal(vertices, face[0], face[1], face[2], face[3])
|
|
29
40
|
else:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
The child classes have to define `mirror`, `rotate` and `translate`,
|
|
45
|
-
then more routines such as `translate_x` and `translated` are automatically available."""
|
|
46
|
-
|
|
47
|
-
@abstractmethod
|
|
48
|
-
def translate(self, vector):
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
@abstractmethod
|
|
52
|
-
def rotate(self, axis, angle):
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
@abstractmethod
|
|
56
|
-
def mirror(self, plane):
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
@inplace_transformation
|
|
60
|
-
def translate_x(self, tx):
|
|
61
|
-
return self.translate((tx, 0., 0.))
|
|
62
|
-
|
|
63
|
-
@inplace_transformation
|
|
64
|
-
def translate_y(self, ty):
|
|
65
|
-
return self.translate((0., ty, 0.))
|
|
66
|
-
|
|
67
|
-
@inplace_transformation
|
|
68
|
-
def translate_z(self, tz):
|
|
69
|
-
return self.translate((0., 0., tz))
|
|
70
|
-
|
|
71
|
-
@inplace_transformation
|
|
72
|
-
def translate_point_to_point(self, point_a, point_b):
|
|
73
|
-
return self.translate(np.asarray(point_b) - np.asarray(point_a))
|
|
74
|
-
|
|
75
|
-
@inplace_transformation
|
|
76
|
-
def rotate_x(self, thetax):
|
|
77
|
-
return self.rotate(Ox_axis, thetax)
|
|
78
|
-
|
|
79
|
-
@inplace_transformation
|
|
80
|
-
def rotate_y(self, thetay):
|
|
81
|
-
return self.rotate(Oy_axis, thetay)
|
|
82
|
-
|
|
83
|
-
@inplace_transformation
|
|
84
|
-
def rotate_z(self, thetaz):
|
|
85
|
-
return self.rotate(Oz_axis, thetaz)
|
|
86
|
-
|
|
87
|
-
@inplace_transformation
|
|
88
|
-
def rotate_around_center_to_align_vectors(self, center, vec1, vec2):
|
|
89
|
-
"""Rotate self such that if vec1 is in self, then it will point in the same direction as vec2."""
|
|
90
|
-
vec1 = np.asarray(vec1)
|
|
91
|
-
vec2 = np.asarray(vec2)
|
|
92
|
-
if parallel_vectors_with_same_direction(vec1, vec2):
|
|
93
|
-
return self
|
|
41
|
+
normal = _triangle_normal(vertices, face[0], face[1], face[2])
|
|
42
|
+
normals.append(normal)
|
|
43
|
+
return np.array(normals)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_faces_areas(vertices, faces):
|
|
47
|
+
areas = []
|
|
48
|
+
for face in faces:
|
|
49
|
+
verts = get_vertices_face(face, vertices)
|
|
50
|
+
if len(verts) == 4:
|
|
51
|
+
a, b, c, d = verts
|
|
52
|
+
area1 = 0.5 * np.linalg.norm(np.cross(b - a, c - a))
|
|
53
|
+
area2 = 0.5 * np.linalg.norm(np.cross(c - a, d - a))
|
|
54
|
+
areas.append(area1 + area2)
|
|
94
55
|
else:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
56
|
+
a, b, c = verts
|
|
57
|
+
areas.append(0.5 * np.linalg.norm(np.cross(b - a, c - a)))
|
|
58
|
+
return np.array(areas)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compute_faces_centers(vertices, faces):
|
|
62
|
+
centers = []
|
|
63
|
+
for face in faces:
|
|
64
|
+
verts = get_vertices_face(face, vertices)
|
|
65
|
+
if len(verts) == 4:
|
|
66
|
+
a, b, c, d = verts
|
|
67
|
+
area1 = 0.5 * np.linalg.norm(np.cross(b - a, c - a))
|
|
68
|
+
area2 = 0.5 * np.linalg.norm(np.cross(c - a, d - a))
|
|
69
|
+
c1 = (a + b + c) / 3
|
|
70
|
+
c2 = (a + c + d) / 3
|
|
71
|
+
center = (c1 * area1 + c2 * area2) / (area1 + area2)
|
|
72
|
+
else:
|
|
73
|
+
a, b, c = verts
|
|
74
|
+
center = (a + b + c) / 3
|
|
75
|
+
centers.append(center)
|
|
76
|
+
return np.array(centers)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def compute_faces_radii(vertices, faces):
|
|
80
|
+
centers = compute_faces_centers(vertices, faces)
|
|
81
|
+
distances = []
|
|
82
|
+
for face, center in zip(faces, centers):
|
|
83
|
+
d = compute_distance_between_points(vertices[face[0]], center)
|
|
84
|
+
distances.append(d)
|
|
85
|
+
return np.array(distances)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def compute_gauss_legendre_2_quadrature(vertices, faces):
|
|
89
|
+
# Parameters of Gauss-Legendre 2 quadrature scheme
|
|
90
|
+
local_points = np.array([(+1/np.sqrt(3), +1/np.sqrt(3)),
|
|
91
|
+
(+1/np.sqrt(3), -1/np.sqrt(3)),
|
|
92
|
+
(-1/np.sqrt(3), +1/np.sqrt(3)),
|
|
93
|
+
(-1/np.sqrt(3), -1/np.sqrt(3))])
|
|
94
|
+
local_weights = np.array([1/4, 1/4, 1/4, 1/4])
|
|
95
|
+
|
|
96
|
+
# Application to mesh
|
|
97
|
+
faces = vertices[faces[:, :], :]
|
|
98
|
+
nb_faces = faces.shape[0]
|
|
99
|
+
nb_quad_points = len(local_weights)
|
|
100
|
+
points = np.empty((nb_faces, nb_quad_points, 3))
|
|
101
|
+
weights = np.empty((nb_faces, nb_quad_points))
|
|
102
|
+
for i_face in range(nb_faces):
|
|
103
|
+
for k_quad in range(nb_quad_points):
|
|
104
|
+
xk, yk = local_points[k_quad, :]
|
|
105
|
+
points[i_face, k_quad, :] = (
|
|
106
|
+
(1+xk)*(1+yk) * faces[i_face, 0, :]
|
|
107
|
+
+ (1+xk)*(1-yk) * faces[i_face, 1, :]
|
|
108
|
+
+ (1-xk)*(1-yk) * faces[i_face, 2, :]
|
|
109
|
+
+ (1-xk)*(1+yk) * faces[i_face, 3, :]
|
|
110
|
+
)/4
|
|
111
|
+
dxidx = ((1+yk)*faces[i_face, 0, :] + (1-yk)*faces[i_face, 1, :]
|
|
112
|
+
- (1-yk)*faces[i_face, 2, :] - (1+yk)*faces[i_face, 3, :])/4
|
|
113
|
+
dxidy = ((1+xk)*faces[i_face, 0, :] - (1+xk)*faces[i_face, 1, :]
|
|
114
|
+
- (1-xk)*faces[i_face, 2, :] + (1-xk)*faces[i_face, 3, :])/4
|
|
115
|
+
detJ = np.linalg.norm(np.cross(dxidx, dxidy))
|
|
116
|
+
weights[i_face, k_quad] = local_weights[k_quad] * 4 * detJ
|
|
117
|
+
|
|
118
|
+
return points, weights
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _triangle_normal(vertices, v0_idx, v1_idx, v2_idx):
|
|
144
122
|
"""
|
|
123
|
+
Compute normal vector of a triangle face.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
vertices : ndarray
|
|
128
|
+
Vertex coordinate array.
|
|
129
|
+
v0_idx, v1_idx, v2_idx : int
|
|
130
|
+
Indices of triangle vertices.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
np.ndarray
|
|
135
|
+
Normalized normal vector (3,)
|
|
136
|
+
"""
|
|
137
|
+
v0, v1, v2 = vertices[v0_idx], vertices[v1_idx], vertices[v2_idx]
|
|
138
|
+
normal = np.cross(v1 - v0, v2 - v0)
|
|
139
|
+
return normal / np.linalg.norm(normal)
|
|
145
140
|
|
|
146
|
-
@abstractmethod
|
|
147
|
-
def clip(self, plane):
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
|
-
def clipped(self, plane, **kwargs):
|
|
151
|
-
# Same API as for the other transformations
|
|
152
|
-
return self.clip(plane, inplace=False, **kwargs)
|
|
153
|
-
|
|
154
|
-
@inplace_transformation
|
|
155
|
-
def keep_immersed_part(self, free_surface=0.0, *, sea_bottom=None, water_depth=None):
|
|
156
|
-
self.clip(Plane(normal=(0, 0, 1), point=(0, 0, free_surface)))
|
|
157
|
-
water_depth = _get_water_depth(free_surface, water_depth, sea_bottom,
|
|
158
|
-
default_water_depth=np.inf)
|
|
159
|
-
if water_depth < np.inf:
|
|
160
|
-
self.clip(Plane(normal=(0, 0, -1), point=(0, 0, free_surface-water_depth)))
|
|
161
|
-
return self
|
|
162
|
-
|
|
163
|
-
def immersed_part(self, free_surface=0.0, *, sea_bottom=None, water_depth=None):
|
|
164
|
-
return self.keep_immersed_part(free_surface, inplace=False, name=self.name,
|
|
165
|
-
sea_bottom=sea_bottom, water_depth=water_depth)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
######################
|
|
169
|
-
# HELPER FUNCTIONS #
|
|
170
|
-
######################
|
|
171
|
-
|
|
172
|
-
def orthogonal_vectors(vec1, vec2) -> bool:
|
|
173
|
-
return np.linalg.norm(vec1 @ vec2) < 1e-6
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def parallel_vectors(vec1, vec2) -> bool:
|
|
177
|
-
return np.linalg.norm(np.cross(vec1, vec2)) < 1e-6
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def parallel_vectors_with_same_direction(vec1, vec2) -> bool:
|
|
181
|
-
return parallel_vectors(vec1, vec2) and np.dot(vec1, vec2) > 0
|
|
182
141
|
|
|
142
|
+
def _quad_normal(vertices, v0_idx, v1_idx, v2_idx, v3_idx):
|
|
143
|
+
"""
|
|
144
|
+
Compute normal vector of a quadrilateral face via diagonals.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
vertices : ndarray
|
|
149
|
+
Vertex coordinate array.
|
|
150
|
+
v0_idx, v1_idx, v2_idx, v3_idx : int
|
|
151
|
+
Indices of quad vertices.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
np.ndarray
|
|
156
|
+
Normalized normal vector (3,)
|
|
157
|
+
"""
|
|
158
|
+
v0, v1, v2, v3 = (
|
|
159
|
+
vertices[v0_idx],
|
|
160
|
+
vertices[v1_idx],
|
|
161
|
+
vertices[v2_idx],
|
|
162
|
+
vertices[v3_idx],
|
|
163
|
+
)
|
|
164
|
+
ac = v2 - v0
|
|
165
|
+
bd = v3 - v1
|
|
166
|
+
normal = np.cross(ac, bd)
|
|
167
|
+
return normal / np.linalg.norm(normal)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def compute_distance_between_points(a, b):
|
|
171
|
+
"""
|
|
172
|
+
Compute Euclidean distance between two points in n-dimensional space.
|
|
183
173
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
a, b : array_like
|
|
177
|
+
Coordinate arrays (length 3 or more).
|
|
187
178
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
float
|
|
182
|
+
Euclidean distance.
|
|
183
|
+
"""
|
|
184
|
+
a = np.asarray(a)
|
|
185
|
+
b = np.asarray(b)
|
|
186
|
+
return np.linalg.norm(b - a)
|
|
187
|
+
|
|
188
|
+
def faces_in_group(faces: NDArray[np.integer], group: NDArray[np.integer]) -> NDArray[np.bool_]:
|
|
189
|
+
"""Identification of faces with vertices within group.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
faces : NDArray[np.integer]
|
|
194
|
+
Mesh faces. Expecting a numpy array of shape N_faces x N_vertices_per_face.
|
|
195
|
+
group : NDArray[np.integer]
|
|
196
|
+
Group of connected vertices
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
NDArray[np.bool]
|
|
201
|
+
Mask of faces containing vertices from the group
|
|
202
|
+
"""
|
|
203
|
+
return np.any(np.isin(faces, group), axis=1)
|
|
195
204
|
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
def clustering(faces: NDArray[np.integer]) -> List[NDArray[np.integer]]:
|
|
206
|
+
"""Clustering of vertices per connected faces.
|
|
198
207
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
else:
|
|
204
|
-
raise NotImplementedError
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
faces : NDArray[np.integer]
|
|
211
|
+
Mesh faces. Expecting a numpy array of shape N_faces x N_vertices_per_face.
|
|
205
212
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
list[NDArray[np.integer]]
|
|
216
|
+
Groups of connected vertices.
|
|
217
|
+
"""
|
|
218
|
+
vert_groups: list[NDArray[np.integer]] = []
|
|
219
|
+
mask = np.ones(faces.shape[0], dtype=bool)
|
|
220
|
+
while np.any(mask):
|
|
221
|
+
# Consider faces whose vertices are not already identified in a group.
|
|
222
|
+
# Start new group by considering first face
|
|
223
|
+
remaining_faces = faces[mask]
|
|
224
|
+
group = remaining_faces[0]
|
|
225
|
+
rem_mask = np.ones(remaining_faces.shape[0], dtype=bool)
|
|
226
|
+
# Iterative update of vertices group. Output final result to frozenset
|
|
227
|
+
while not np.allclose(new:=faces_in_group(remaining_faces, group), rem_mask):
|
|
228
|
+
group = np.unique(remaining_faces[new])
|
|
229
|
+
rem_mask = new
|
|
209
230
|
else:
|
|
210
|
-
|
|
231
|
+
group = np.unique(remaining_faces[new])
|
|
232
|
+
vert_groups.append(group)
|
|
233
|
+
# Identify faces that have no vertices in current groups
|
|
234
|
+
mask = ~reduce(np.logical_or, [faces_in_group(faces, group) for group in vert_groups])
|
|
235
|
+
return vert_groups
|
|
211
236
|
|
|
212
|
-
def is_orthogonal_to(self, other):
|
|
213
|
-
if isinstance(other, Plane):
|
|
214
|
-
return parallel_vectors(self.vector, other.normal)
|
|
215
|
-
elif len(other) == 3: # The other is supposed to be a vector given as a 3-ple
|
|
216
|
-
return orthogonal_vectors(self.vector, other)
|
|
217
|
-
else:
|
|
218
|
-
raise NotImplementedError
|
|
219
|
-
|
|
220
|
-
def is_parallel_to(self, other):
|
|
221
|
-
if isinstance(other, Plane):
|
|
222
|
-
return orthogonal_vectors(self.vector, other.normal)
|
|
223
|
-
elif isinstance(other, Axis):
|
|
224
|
-
return parallel_vectors(self.vector, other.vector)
|
|
225
|
-
elif len(other) == 3: # The other is supposed to be a vector given as a 3-ple
|
|
226
|
-
return parallel_vectors(self.vector, other)
|
|
227
|
-
else:
|
|
228
|
-
raise NotImplementedError
|
|
229
|
-
|
|
230
|
-
def angle_with_respect_to(self, other_axis: 'Axis') -> float:
|
|
231
|
-
"""Angle between two axes."""
|
|
232
|
-
return np.arccos(np.dot(self.vector, other_axis.vector))
|
|
233
|
-
|
|
234
|
-
################################
|
|
235
|
-
# Transformation of the axis #
|
|
236
|
-
################################
|
|
237
|
-
|
|
238
|
-
def copy(self, name=None):
|
|
239
|
-
return Axis(vector=self.vector.copy(), point=self.point.copy())
|
|
240
|
-
|
|
241
|
-
@inplace_transformation
|
|
242
|
-
def translate(self, vector):
|
|
243
|
-
self.point += vector
|
|
244
|
-
return self
|
|
245
|
-
|
|
246
|
-
@inplace_transformation
|
|
247
|
-
def rotate(self, axis, angle):
|
|
248
|
-
rot_matrix = axis.rotation_matrix(angle)
|
|
249
|
-
self.point = rot_matrix @ (self.point - axis.point) + axis.point
|
|
250
|
-
self.vector = rot_matrix @ self.vector
|
|
251
|
-
return self
|
|
252
|
-
|
|
253
|
-
@inplace_transformation
|
|
254
|
-
def mirror(self, plane):
|
|
255
|
-
self.point -= 2 * (self.point @ plane.normal - plane.c) * plane.normal
|
|
256
|
-
self.vector -= 2 * (self.vector @ plane.normal) * plane.normal
|
|
257
|
-
return self
|
|
258
|
-
|
|
259
|
-
###########
|
|
260
|
-
# Other #
|
|
261
|
-
###########
|
|
262
|
-
|
|
263
|
-
def rotation_matrix(self, theta):
|
|
264
|
-
"""Rotation matrix around the vector according to Rodrigues' formula."""
|
|
265
|
-
ux, uy, uz = self.vector
|
|
266
|
-
W = np.array([[0, -uz, uy],
|
|
267
|
-
[uz, 0, -ux],
|
|
268
|
-
[-uy, ux, 0]])
|
|
269
|
-
return np.identity(3) + np.sin(theta)*W + 2*np.sin(theta/2)**2 * (W @ W)
|
|
270
|
-
|
|
271
|
-
def rotate_vectors(self, vectors, angle):
|
|
272
|
-
vectors = np.asarray(vectors)
|
|
273
|
-
return (self.rotation_matrix(angle) @ vectors.T).T
|
|
274
|
-
|
|
275
|
-
def rotate_points(self, points, angle):
|
|
276
|
-
points = np.asarray(points)
|
|
277
|
-
return self.rotate_vectors(points - self.point, angle) + self.point
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Ox_axis = Axis(vector=e_x, point=(0, 0, 0))
|
|
281
|
-
Oy_axis = Axis(vector=e_y, point=(0, 0, 0))
|
|
282
|
-
Oz_axis = Axis(vector=e_z, point=(0, 0, 0))
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
#################
|
|
286
|
-
# PLANE CLASS #
|
|
287
|
-
#################
|
|
288
|
-
|
|
289
|
-
class Plane(Abstract3DObject):
|
|
290
|
-
"""3D plane, oriented by the direction of their normal."""
|
|
291
|
-
def __init__(self, normal=(0.0, 0.0, 1.0), point=(0.0, 0.0, 0.0)):
|
|
292
|
-
normal = np.asarray(normal, dtype=float)
|
|
293
|
-
self.normal = normal / np.linalg.norm(normal)
|
|
294
|
-
self.point = np.asarray(point, dtype=float)
|
|
295
|
-
|
|
296
|
-
def __repr__(self):
|
|
297
|
-
return f"Plane(normal={self.normal}, point={self.point})"
|
|
298
|
-
|
|
299
|
-
def __contains__(self, other):
|
|
300
|
-
if isinstance(other, Axis):
|
|
301
|
-
return other.point in self and orthogonal_vectors(self.normal, other.vector)
|
|
302
|
-
elif len(other) == 3:
|
|
303
|
-
return orthogonal_vectors(other - self.point, self.normal)
|
|
304
|
-
else:
|
|
305
|
-
raise NotImplementedError
|
|
306
237
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
@property
|
|
331
|
-
def s(self):
|
|
332
|
-
"""Distance from origin to plane along the normal"""
|
|
333
|
-
return np.dot(self.normal, self.point)
|
|
334
|
-
|
|
335
|
-
#################################
|
|
336
|
-
# Transformation of the plane #
|
|
337
|
-
#################################
|
|
338
|
-
|
|
339
|
-
def copy(self, name=None):
|
|
340
|
-
return Plane(normal=self.normal.copy(), point=self.point.copy())
|
|
341
|
-
|
|
342
|
-
@inplace_transformation
|
|
343
|
-
def translate(self, vector):
|
|
344
|
-
self.point = self.point + np.asarray(vector)
|
|
345
|
-
return self
|
|
346
|
-
|
|
347
|
-
@inplace_transformation
|
|
348
|
-
def rotate(self, axis, angle):
|
|
349
|
-
rot_matrix = axis.rotation_matrix(angle)
|
|
350
|
-
self.point = rot_matrix @ self.point
|
|
351
|
-
self.normal = rot_matrix @ self.normal
|
|
352
|
-
return self
|
|
353
|
-
|
|
354
|
-
@inplace_transformation
|
|
355
|
-
def mirror(self, plane):
|
|
356
|
-
self.point -= 2 * (self.point @ plane.normal - plane.c) * plane.normal
|
|
357
|
-
self.normal -= 2 * (self.normal @ plane.normal) * plane.normal
|
|
358
|
-
return self
|
|
359
|
-
|
|
360
|
-
###########
|
|
361
|
-
# Other #
|
|
362
|
-
###########
|
|
363
|
-
|
|
364
|
-
def distance_to_point(self, points):
|
|
365
|
-
"""
|
|
366
|
-
Return the orthogonal distance of points with respect to the plane.
|
|
367
|
-
The distance is counted positively on one side of the plane and negatively on the other.
|
|
368
|
-
|
|
369
|
-
Parameters
|
|
370
|
-
----------
|
|
371
|
-
points : ndarray
|
|
372
|
-
Array of points coordinates
|
|
373
|
-
|
|
374
|
-
Returns
|
|
375
|
-
-------
|
|
376
|
-
dist : ndarray
|
|
377
|
-
Array of distances of points with respect to the plane
|
|
378
|
-
"""
|
|
379
|
-
return np.dot(points, self.normal) - np.dot(self.point, self.normal)
|
|
380
|
-
|
|
381
|
-
def get_edge_intersection(self, p0, p1):
|
|
382
|
-
"""
|
|
383
|
-
Returns the coordinates of the intersection point between the plane and the edge P0P1.
|
|
384
|
-
|
|
385
|
-
Parameters
|
|
386
|
-
----------
|
|
387
|
-
p0 : ndarray
|
|
388
|
-
Coordinates of point p0
|
|
389
|
-
p1 : ndarray
|
|
390
|
-
Coordinates of point P1
|
|
391
|
-
|
|
392
|
-
Returns
|
|
393
|
-
-------
|
|
394
|
-
I : ndarray
|
|
395
|
-
Coordinates of intersection point
|
|
396
|
-
"""
|
|
397
|
-
assert len(p0) == 3 and len(p1) == 3
|
|
398
|
-
|
|
399
|
-
p0n = np.dot(p0, self.normal)
|
|
400
|
-
p1n = np.dot(p1, self.normal)
|
|
401
|
-
t = (p0n - self.s) / (p0n - p1n)
|
|
402
|
-
if t < 0. or t > 1.:
|
|
403
|
-
raise RuntimeError('Intersection is outside the edge')
|
|
404
|
-
return (1-t) * p0 + t * p1
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
yOz_Plane = Plane(normal=e_x, point=(0, 0, 0))
|
|
408
|
-
xOz_Plane = Plane(normal=e_y, point=(0, 0, 0))
|
|
409
|
-
xOy_Plane = Plane(normal=e_z, point=(0, 0, 0))
|
|
238
|
+
def connected_components(mesh):
|
|
239
|
+
"""Returns a list of meshes that each corresponds to the a connected component in the original mesh.
|
|
240
|
+
Assumes the mesh is mostly conformal without duplicate vertices.
|
|
241
|
+
"""
|
|
242
|
+
# Get connected vertices
|
|
243
|
+
vertices_components = clustering(mesh.faces)
|
|
244
|
+
# Verification
|
|
245
|
+
if sum(len(group) for group in vertices_components) != len(set(chain.from_iterable(vertices_components))):
|
|
246
|
+
raise ValueError("Error in connected components clustering. Some elements are duplicated")
|
|
247
|
+
# The components are found. The rest is just about retrieving the faces in each components.
|
|
248
|
+
faces_components = [np.argwhere(faces_in_group(mesh.faces, group)) for group in vertices_components]
|
|
249
|
+
components = [mesh.extract_faces(f) for f in faces_components]
|
|
250
|
+
return components
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def connected_components_of_waterline(mesh, z=0.0):
|
|
254
|
+
if np.any(mesh.vertices[:, 2] > z + 1e-8):
|
|
255
|
+
mesh = mesh.immersed_part(free_surface=z)
|
|
256
|
+
fs_vertices_indices = np.where(np.isclose(mesh.vertices[:, 2], z))[0]
|
|
257
|
+
fs_faces_indices = np.where(np.any(np.isin(mesh.faces, fs_vertices_indices), axis=1))[0]
|
|
258
|
+
crown_mesh = mesh.extract_faces(fs_faces_indices)
|
|
259
|
+
return connected_components(crown_mesh)
|