capytaine 2.3__cp313-cp313-macosx_14_0_arm64.whl → 3.0.0a1__cp313-cp313-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.
Files changed (86) hide show
  1. capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. capytaine/.dylibs/libgfortran.5.dylib +0 -0
  3. capytaine/.dylibs/libquadmath.0.dylib +0 -0
  4. capytaine/__about__.py +7 -2
  5. capytaine/__init__.py +8 -12
  6. capytaine/bem/engines.py +234 -354
  7. capytaine/bem/problems_and_results.py +30 -21
  8. capytaine/bem/solver.py +205 -81
  9. capytaine/bodies/bodies.py +279 -862
  10. capytaine/bodies/dofs.py +136 -9
  11. capytaine/bodies/hydrostatics.py +540 -0
  12. capytaine/bodies/multibodies.py +216 -0
  13. capytaine/green_functions/{libs/Delhommeau_float32.cpython-313-darwin.so → Delhommeau_float32.cpython-313-darwin.so} +0 -0
  14. capytaine/green_functions/{libs/Delhommeau_float64.cpython-313-darwin.so → Delhommeau_float64.cpython-313-darwin.so} +0 -0
  15. capytaine/green_functions/abstract_green_function.py +2 -2
  16. capytaine/green_functions/delhommeau.py +50 -31
  17. capytaine/green_functions/hams.py +19 -13
  18. capytaine/io/legacy.py +3 -103
  19. capytaine/io/xarray.py +15 -10
  20. capytaine/meshes/__init__.py +2 -6
  21. capytaine/meshes/abstract_meshes.py +375 -0
  22. capytaine/meshes/clean.py +302 -0
  23. capytaine/meshes/clip.py +347 -0
  24. capytaine/meshes/export.py +89 -0
  25. capytaine/meshes/geometry.py +244 -394
  26. capytaine/meshes/io.py +433 -0
  27. capytaine/meshes/meshes.py +621 -676
  28. capytaine/meshes/predefined/cylinders.py +22 -56
  29. capytaine/meshes/predefined/rectangles.py +26 -85
  30. capytaine/meshes/predefined/spheres.py +4 -11
  31. capytaine/meshes/quality.py +118 -407
  32. capytaine/meshes/surface_integrals.py +48 -29
  33. capytaine/meshes/symmetric_meshes.py +641 -0
  34. capytaine/meshes/visualization.py +353 -0
  35. capytaine/post_pro/free_surfaces.py +1 -4
  36. capytaine/post_pro/kochin.py +10 -10
  37. capytaine/tools/block_circulant_matrices.py +275 -0
  38. capytaine/tools/lists_of_points.py +2 -2
  39. capytaine/tools/memory_monitor.py +45 -0
  40. capytaine/tools/symbolic_multiplication.py +31 -5
  41. capytaine/tools/timer.py +68 -42
  42. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
  43. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  44. capytaine-3.0.0a1.dist-info/WHEEL +6 -0
  45. capytaine/bodies/predefined/__init__.py +0 -6
  46. capytaine/bodies/predefined/cylinders.py +0 -151
  47. capytaine/bodies/predefined/rectangles.py +0 -111
  48. capytaine/bodies/predefined/spheres.py +0 -70
  49. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  50. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  51. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  52. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  53. capytaine/green_functions/FinGreen3D/README.md +0 -24
  54. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  55. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  56. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  57. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  58. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
  59. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  60. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  61. capytaine/green_functions/libs/__init__.py +0 -0
  62. capytaine/io/mesh_loaders.py +0 -1086
  63. capytaine/io/mesh_writers.py +0 -692
  64. capytaine/io/meshio.py +0 -38
  65. capytaine/matrices/__init__.py +0 -16
  66. capytaine/matrices/block.py +0 -592
  67. capytaine/matrices/block_toeplitz.py +0 -325
  68. capytaine/matrices/builders.py +0 -89
  69. capytaine/matrices/linear_solvers.py +0 -232
  70. capytaine/matrices/low_rank.py +0 -395
  71. capytaine/meshes/clipper.py +0 -465
  72. capytaine/meshes/collections.py +0 -334
  73. capytaine/meshes/mesh_like_protocol.py +0 -37
  74. capytaine/meshes/properties.py +0 -276
  75. capytaine/meshes/quadratures.py +0 -80
  76. capytaine/meshes/symmetric.py +0 -392
  77. capytaine/tools/lru_cache.py +0 -49
  78. capytaine/ui/vtk/__init__.py +0 -3
  79. capytaine/ui/vtk/animation.py +0 -329
  80. capytaine/ui/vtk/body_viewer.py +0 -28
  81. capytaine/ui/vtk/helpers.py +0 -82
  82. capytaine/ui/vtk/mesh_viewer.py +0 -461
  83. capytaine-2.3.dist-info/RECORD +0 -92
  84. capytaine-2.3.dist-info/WHEEL +0 -4
  85. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  86. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
@@ -1,409 +1,259 @@
1
- """Tools to describe geometric objects in 3D.
2
- Based on meshmagick <https://github.com/LHEEA/meshmagick> by François Rongère.
3
- """
4
- # Copyright (C) 2017-2019 Matthieu Ancellin, based on the work of François Rongère
5
- # See LICENSE file at <https://github.com/mancellin/capytaine>
6
-
7
- from abc import ABC, abstractmethod
8
- from capytaine.tools.deprecation_handling import _get_water_depth
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
- e_x = np.array((1, 0, 0))
13
- e_y = np.array((0, 1, 0))
14
- e_z = np.array((0, 0, 1))
15
-
16
-
17
- ###########################################
18
- # DECORATOR FOR INPLACE TRANSFORMATIONS #
19
- ###########################################
20
-
21
- def inplace_transformation(inplace_function):
22
- """Decorator for methods transforming 3D objects:
23
- * Add the optional argument `inplace` to return a new object instead of doing the transformation in place.
24
- * If the object has properties cached in an "__internals__" dict, they are deleted.
25
- """
26
- def enhanced_inplace_function(self, *args, inplace=True, name=None, **kwargs):
27
- if not inplace:
28
- object3d = self.copy(name=name)
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
- object3d = self
31
- inplace_function(object3d, *args, **kwargs)
32
- if hasattr(object3d, '__internals__'):
33
- object3d.__internals__.clear()
34
- return object3d
35
- return enhanced_inplace_function
36
-
37
-
38
- ##############################
39
- # ABSTRACT 3D OBJECT CLASS #
40
- ##############################
41
-
42
- class Abstract3DObject(ABC):
43
- """Abstract class for 3d objects that can be transformed in 3d.
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
- if parallel_vectors(vec1, vec2):
96
- if parallel_vectors(vec1, e_x):
97
- axis = Axis(vector=np.cross(vec1, e_y), point=center)
98
- else:
99
- axis = Axis(vector=np.cross(vec1, e_x), point=center)
100
- return self.rotate(axis, np.pi)
101
- else:
102
- axis = Axis(vector=np.cross(vec1, vec2), point=center)
103
- return self.rotate(axis, np.arccos(np.dot(vec1, vec2)))
104
-
105
- def translated(self, *args, **kwargs):
106
- return self.translate(*args, inplace=False, **kwargs)
107
-
108
- def rotated(self, *args, **kwargs):
109
- return self.rotate(*args, inplace=False, **kwargs)
110
-
111
- def mirrored(self, *args, **kwargs):
112
- return self.mirror(*args, inplace=False, **kwargs)
113
-
114
- def translated_x(self, *args, **kwargs):
115
- return self.translate_x(*args, inplace=False, **kwargs)
116
-
117
- def translated_y(self, *args, **kwargs):
118
- return self.translate_y(*args, inplace=False, **kwargs)
119
-
120
- def translated_z(self, *args, **kwargs):
121
- return self.translate_z(*args, inplace=False, **kwargs)
122
-
123
- def translated_point_to_point(self, *args, **kwargs):
124
- return self.translate_point_to_point(*args, inplace=False, **kwargs)
125
-
126
- def rotated_x(self, *args, **kwargs):
127
- return self.rotate_x(*args, inplace=False, **kwargs)
128
-
129
- def rotated_y(self, *args, **kwargs):
130
- return self.rotate_y(*args, inplace=False, **kwargs)
131
-
132
- def rotated_z(self, *args, **kwargs):
133
- return self.rotate_z(*args, inplace=False, **kwargs)
134
-
135
- def rotated_around_center_to_align_vectors(self, *args, **kwargs):
136
- return self.rotate_around_center_to_align_vectors(*args, inplace=False, **kwargs)
137
-
138
-
139
- class ClippableMixin(ABC):
140
- """Abstract base class for object that can be clipped.
141
- The child classes should implement a `clip` method, then this abstract
142
- class will append the new methods `clipped`, `keep_immersed_part` and
143
- `immersed_part`, all based on `clip`.
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
- # AXIS CLASS #
186
- ################
174
+ Parameters
175
+ ----------
176
+ a, b : array_like
177
+ Coordinate arrays (length 3 or more).
187
178
 
188
- class Axis(Abstract3DObject):
189
- def __init__(self, vector=(1, 0, 0), point=(0, 0, 0)):
190
- assert len(vector) == 3, "Vector of an axis should be given as a 3-ple of values."
191
- assert len(point) == 3, "Point of an axis should be given as a 3-ple of values."
192
- vector = np.array(vector, float)
193
- self.vector = vector / np.linalg.norm(vector)
194
- self.point = np.array(point, float)
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
- def __repr__(self):
197
- return f"Axis(vector={self.vector}, point={self.point})"
205
+ def clustering(faces: NDArray[np.integer]) -> List[NDArray[np.integer]]:
206
+ """Clustering of vertices per connected faces.
198
207
 
199
- def __contains__(self, other_point):
200
- if len(other_point) == 3:
201
- other_point = np.asarray(other_point, dtype=float)
202
- return parallel_vectors(other_point - self.point, self.vector)
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
- def __eq__(self, other):
207
- if isinstance(self, Axis):
208
- return (self is other) or (self.point in other and parallel_vectors(self.vector, other.vector))
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
- return NotImplemented
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
- def __eq__(self, other):
308
- """Plane are considered equal only when their normal are pointing in the same direction."""
309
- if isinstance(other, Plane):
310
- return ((self is other) or
311
- (other.point in self and parallel_vectors_with_same_direction(self.normal, other.normal)))
312
- else:
313
- return NotImplemented
314
-
315
- def is_orthogonal_to(self, other):
316
- if isinstance(other, Axis):
317
- return parallel_vectors(self.normal, other.vector)
318
- elif isinstance(other, Plane):
319
- return orthogonal_vectors(self.normal, other.normal)
320
- elif len(other) == 3: # The other is supposed to be a vector given as a 3-ple
321
- return parallel_vectors(self.normal, other)
322
- else:
323
- raise NotImplementedError
324
-
325
- @property
326
- def c(self):
327
- """Distance from plane to origin."""
328
- return np.linalg.norm(self.normal @ self.point)
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)