capytaine 3.0.0a1__cp310-cp310-macosx_15_0_x86_64.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 +21 -0
- capytaine/__init__.py +32 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +321 -0
- capytaine/bem/problems_and_results.py +601 -0
- capytaine/bem/solver.py +718 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +630 -0
- capytaine/bodies/dofs.py +146 -0
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/Delhommeau_float32.cpython-310-darwin.so +0 -0
- capytaine/green_functions/Delhommeau_float64.cpython-310-darwin.so +0 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +522 -0
- capytaine/green_functions/hams.py +210 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +228 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +673 -0
- capytaine/meshes/__init__.py +2 -0
- 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 +259 -0
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +826 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +280 -0
- capytaine/meshes/predefined/rectangles.py +202 -0
- capytaine/meshes/predefined/spheres.py +55 -0
- capytaine/meshes/quality.py +159 -0
- capytaine/meshes/surface_integrals.py +82 -0
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +85 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +161 -0
- capytaine/tools/timer.py +90 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine-3.0.0a1.dist-info/LICENSE +674 -0
- capytaine-3.0.0a1.dist-info/METADATA +755 -0
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine-3.0.0a1.dist-info/WHEEL +6 -0
- capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
capytaine/meshes/io.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
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 Union, Any, Optional
|
|
16
|
+
from io import IOBase
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import tempfile
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import xarray as xr
|
|
23
|
+
|
|
24
|
+
from capytaine.tools.optional_imports import silently_import_optional_dependency
|
|
25
|
+
from capytaine.meshes import Mesh, ReflectionSymmetricMesh
|
|
26
|
+
|
|
27
|
+
meshio = silently_import_optional_dependency("meshio")
|
|
28
|
+
trimesh = silently_import_optional_dependency("trimesh")
|
|
29
|
+
|
|
30
|
+
LOG = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_MESHIO_EXTS = {"inp", "msh", "avs", "cgns", "xml", "e", "exo", "f3grid", "h5m", "mdpa", "mesh", "meshb", "med", "bdf", "fem", "nas", "vol", "vol.gz", "obj", "off", "post", "post.gz", "dato", "dato.gz", "ply", "stl", "dat", "node", "ele", "svg", "su2", "ugrid", "vtk", "vtu", "wkt", "xdmf", "xmf", "gmsh"}
|
|
33
|
+
|
|
34
|
+
_TRIMESH_EXTS = {"obj", "stl", "ply", "glb", "gltf", "off"}
|
|
35
|
+
|
|
36
|
+
_XARRAY_EXTS = {"nc", "netcdf"}
|
|
37
|
+
|
|
38
|
+
_BUILTIN_EXTS = {"pnl", "hst", "mar", "gdf", "nemoh", "wamit", "hydrostar", "hams"}
|
|
39
|
+
|
|
40
|
+
_ALL_EXTS = _MESHIO_EXTS | _TRIMESH_EXTS | _XARRAY_EXTS | _BUILTIN_EXTS
|
|
41
|
+
|
|
42
|
+
# Structure of the function calls:
|
|
43
|
+
#
|
|
44
|
+
# load_mesh
|
|
45
|
+
# / | \
|
|
46
|
+
# if path / | \
|
|
47
|
+
# / | \
|
|
48
|
+
# _load_from_path | --------------------\
|
|
49
|
+
# | \ | |
|
|
50
|
+
# | \ | if opened file | if object from external lib
|
|
51
|
+
# if implemented | \ | |
|
|
52
|
+
# by external lib | _read_mesh_from_file_like_object |
|
|
53
|
+
# | | \ |
|
|
54
|
+
# | if implemented | \ if built-in |
|
|
55
|
+
# | by external lib | \ |
|
|
56
|
+
# \ | _read_*** |
|
|
57
|
+
# \ | |
|
|
58
|
+
# \ | ----------------/
|
|
59
|
+
# \ | /
|
|
60
|
+
# _import_from_***_mesh_class
|
|
61
|
+
#
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_mesh(mesh_to_be_loaded, file_format=None, *, backend=None) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
65
|
+
"""Load a mesh from a file or file-like object.
|
|
66
|
+
|
|
67
|
+
This function can load mesh data from many file formats. It supports file
|
|
68
|
+
paths and file-like objects.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
mesh_to_be_loaded : str, pathlib.Path, or file-like object
|
|
73
|
+
Can be either:
|
|
74
|
+
- a path to a mesh file
|
|
75
|
+
- a file-like object with mesh file data
|
|
76
|
+
- a mesh object from one of the compatible external libraries (meshio, trimesh)
|
|
77
|
+
file_format : str, optional
|
|
78
|
+
Format hint used only when loading from file-like objects, since the
|
|
79
|
+
filename extension is unavailable.
|
|
80
|
+
Valid values include ``"stl"``, ``"obj"``, ``"hst"``, ``"pnl"``, etc.
|
|
81
|
+
Can be automatically inferred from file extension when a path is provided as first argument.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
Mesh or ReflectionSymmetricMesh
|
|
86
|
+
A Mesh object, or a ReflectionSymmetricMesh if symmetry information
|
|
87
|
+
is present in the file format.
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
------
|
|
91
|
+
ValueError
|
|
92
|
+
If ``file_format`` is not provided when loading from a file-like
|
|
93
|
+
object, or if an unsupported format is encountered.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
Load a mesh from a file path.
|
|
98
|
+
|
|
99
|
+
>>> load_mesh("model.stl")
|
|
100
|
+
|
|
101
|
+
Load from a gzip-compressed file.
|
|
102
|
+
|
|
103
|
+
>>> import gzip
|
|
104
|
+
>>> with gzip.open("model.stl.gz") as handler:
|
|
105
|
+
... load_mesh(handler, file_format="stl")
|
|
106
|
+
|
|
107
|
+
Load from any file-like object.
|
|
108
|
+
|
|
109
|
+
>>> with open("model.obj", "rb") as handler:
|
|
110
|
+
... load_mesh(handler, file_format="obj")
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(mesh_to_be_loaded, IOBase): # A file already opened
|
|
114
|
+
return _read_mesh_from_file_like_object(mesh_to_be_loaded, file_format, backend=backend)
|
|
115
|
+
|
|
116
|
+
elif backend in {None, "xarray"} and isinstance(mesh_to_be_loaded, xr.Dataset):
|
|
117
|
+
return _import_from_xarray_dataset(mesh_to_be_loaded)
|
|
118
|
+
|
|
119
|
+
elif trimesh is not None and backend in {None, "trimesh"} and isinstance(mesh_to_be_loaded, trimesh.base.Trimesh):
|
|
120
|
+
return _import_from_trimesh_mesh_class(mesh_to_be_loaded)
|
|
121
|
+
|
|
122
|
+
elif meshio is not None and backend in {None, "meshio"} and isinstance(mesh_to_be_loaded, meshio.Mesh):
|
|
123
|
+
return _import_from_meshio_mesh_class(mesh_to_be_loaded)
|
|
124
|
+
|
|
125
|
+
elif isinstance(mesh_to_be_loaded, (str, Path)):
|
|
126
|
+
return _load_from_path(Path(mesh_to_be_loaded), file_format, backend=backend)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
raise TypeError(f"load_mesh() can't interpret the type of input of {repr(mesh_to_be_loaded)}"
|
|
130
|
+
+ "" if backend is None else f" when using backend={backend}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _normalise_format_hint(value: Any) -> str:
|
|
135
|
+
text = str(value).strip().lower()
|
|
136
|
+
if text.startswith("."):
|
|
137
|
+
text = text[1:]
|
|
138
|
+
return text
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _load_from_path(path: Path, file_format: Optional[str] = None, *, backend: Optional[str] = None):
|
|
142
|
+
if not path.exists():
|
|
143
|
+
raise FileNotFoundError(f"Mesh file not found: {path}")
|
|
144
|
+
|
|
145
|
+
if file_format is None:
|
|
146
|
+
if len(path.suffixes) == 1:
|
|
147
|
+
file_format = path.suffixes[0]
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f"No file format has been provided, nor can it be inferred from path: {path}")
|
|
150
|
+
fmt = _normalise_format_hint(file_format)
|
|
151
|
+
|
|
152
|
+
if trimesh is not None and backend in {None, "trimesh"} and fmt in _TRIMESH_EXTS:
|
|
153
|
+
trimesh_mesh = trimesh.load(path, force="mesh", file_type=fmt)
|
|
154
|
+
return _import_from_trimesh_mesh_class(trimesh_mesh)
|
|
155
|
+
# We could skip this, as _read_mesh_from_file_like_object would also try to pass the opened file to trimesh.
|
|
156
|
+
# However, some file format might supported by trimesh require the file to be opened in byte mode, and we don't want to handle that.
|
|
157
|
+
|
|
158
|
+
if meshio is not None and backend in {None, "meshio"} and fmt in _MESHIO_EXTS:
|
|
159
|
+
meshio_mesh = meshio.read(path, file_format=fmt)
|
|
160
|
+
return _import_from_meshio_mesh_class(meshio_mesh)
|
|
161
|
+
# We could skip this, as _read_mesh_from_file_like_object would also try to pass the opened file to meshio.
|
|
162
|
+
# But meshio does not support reading from an opened file, so we avoid the need for a temporary file by loading from the path here.
|
|
163
|
+
|
|
164
|
+
if backend in {None, "xarray"} and fmt in _XARRAY_EXTS:
|
|
165
|
+
dataset = xr.load_dataset(path)
|
|
166
|
+
return _import_from_xarray_dataset(dataset)
|
|
167
|
+
|
|
168
|
+
with open(path, 'r') as f:
|
|
169
|
+
return _read_mesh_from_file_like_object(f, file_format)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _read_mesh_from_file_like_object(
|
|
173
|
+
file_obj: IOBase,
|
|
174
|
+
file_format: str,
|
|
175
|
+
*,
|
|
176
|
+
backend: Optional[str] = None
|
|
177
|
+
) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
178
|
+
fmt = _normalise_format_hint(file_format)
|
|
179
|
+
|
|
180
|
+
if fmt in _BUILTIN_EXTS and backend is None:
|
|
181
|
+
if fmt == "mar" or fmt == "nemoh":
|
|
182
|
+
return _read_mar(file_obj)
|
|
183
|
+
elif fmt == "gdf" or fmt == "wamit":
|
|
184
|
+
return _read_gdf(file_obj)
|
|
185
|
+
elif fmt == "hst" or fmt == "hydrostar":
|
|
186
|
+
return _read_hst(file_obj)
|
|
187
|
+
elif fmt == "pnl" or fmt == "hams":
|
|
188
|
+
return _read_pnl(file_obj)
|
|
189
|
+
elif trimesh is not None and backend in {None, "trimesh"} and fmt in _TRIMESH_EXTS:
|
|
190
|
+
trimesh_mesh = trimesh.load(file_obj, force="mesh", file_type=fmt)
|
|
191
|
+
return _import_from_trimesh_mesh_class(trimesh_mesh)
|
|
192
|
+
elif meshio is not None and backend in {None, "meshio"} and fmt in _MESHIO_EXTS:
|
|
193
|
+
# Meshio does not support reading from opened file, so it is written in a temporary file here
|
|
194
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
|
195
|
+
content = file_obj.read()
|
|
196
|
+
if isinstance(content, str):
|
|
197
|
+
temp_file.write(content.encode("utf-8"))
|
|
198
|
+
else:
|
|
199
|
+
temp_file.write(content)
|
|
200
|
+
meshio_mesh = meshio.read(temp_file.name, file_format=fmt)
|
|
201
|
+
return _import_from_meshio_mesh_class(meshio_mesh)
|
|
202
|
+
elif backend in {None, "xarray"} and fmt in _XARRAY_EXTS:
|
|
203
|
+
# Seems to be supported according to documentation of xarray,
|
|
204
|
+
# but could not make it work with any backend in practice...
|
|
205
|
+
dataset = xr.load_dataset(file_obj)
|
|
206
|
+
return _import_from_xarray_dataset(dataset)
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Unrecognized or unsupported mesh format: {file_format}. "
|
|
210
|
+
f"Supported mesh formats (some may require external libraries to be installed): {sorted(_ALL_EXTS)}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _import_from_meshio_mesh_class(meshio_mesh):
|
|
215
|
+
faces = []
|
|
216
|
+
if "quad" in meshio_mesh.cells_dict:
|
|
217
|
+
faces.extend(list(meshio_mesh.cells_dict["quad"]))
|
|
218
|
+
if "triangle" in meshio_mesh.cells_dict:
|
|
219
|
+
faces.extend(list(meshio_mesh.cells_dict["triangle"]))
|
|
220
|
+
|
|
221
|
+
if not faces:
|
|
222
|
+
raise ValueError("No triangle or quad cells found in meshio mesh.")
|
|
223
|
+
|
|
224
|
+
return Mesh(meshio_mesh.points, faces)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _import_from_trimesh_mesh_class(trimesh_mesh):
|
|
228
|
+
if not isinstance(trimesh_mesh, trimesh.base.Trimesh):
|
|
229
|
+
raise TypeError(f"Expected trimesh.base.Trimesh, received {type(trimesh_mesh)}")
|
|
230
|
+
return Mesh(trimesh_mesh.vertices, trimesh_mesh.faces)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _import_from_xarray_dataset(dataset):
|
|
234
|
+
return Mesh.from_list_of_faces(dataset["mesh_vertices"].values)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _read_hst(file_obj: IOBase) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
238
|
+
"""HST files have a 1-based indexing"""
|
|
239
|
+
|
|
240
|
+
lines = file_obj.readlines()
|
|
241
|
+
|
|
242
|
+
optional_keywords = ['PROJECT', 'SYMMETRY']
|
|
243
|
+
not_implemented_optional_keywords = ['USER', 'REFLENGTH', 'GRAVITY', 'RHO', 'NBBODY']
|
|
244
|
+
|
|
245
|
+
vertices = []
|
|
246
|
+
faces = []
|
|
247
|
+
optional_data = {kw: None for kw in optional_keywords}
|
|
248
|
+
current_context = None
|
|
249
|
+
ignored_lines = []
|
|
250
|
+
|
|
251
|
+
for i_line, line in enumerate(lines):
|
|
252
|
+
line = line.lstrip()
|
|
253
|
+
|
|
254
|
+
if line == '':
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
elif line.startswith("COORDINATES"):
|
|
258
|
+
current_context = 'vertices'
|
|
259
|
+
|
|
260
|
+
elif current_context == 'vertices' and line.startswith("ENDCOORDINATES"):
|
|
261
|
+
current_context = None
|
|
262
|
+
|
|
263
|
+
elif line.startswith("PANEL"):
|
|
264
|
+
panels_type = int(line[10:])
|
|
265
|
+
current_context = ('panels', panels_type)
|
|
266
|
+
|
|
267
|
+
elif (current_context == ('panels', 0) or current_context == ('panels', 1)) and line.startswith("ENDPANEL"):
|
|
268
|
+
current_context = None
|
|
269
|
+
|
|
270
|
+
elif current_context == 'vertices': # parse vertex coordinates
|
|
271
|
+
numbers = line.split()
|
|
272
|
+
if len(numbers) == 4:
|
|
273
|
+
i_vertex, x, y, z = numbers
|
|
274
|
+
if int(i_vertex) != len(vertices) + 1:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f"HST mesh reader expected the next vertex to be indexed as {len(vertices)+1}, "
|
|
277
|
+
f"but it was actually indexed as {i_vertex} (line {i_line+1}.")
|
|
278
|
+
elif len(numbers) == 3:
|
|
279
|
+
x, y, z = numbers
|
|
280
|
+
vertices.append([x, y, z])
|
|
281
|
+
|
|
282
|
+
elif current_context == ('panels', 0): # parse face definition (no index given)
|
|
283
|
+
numbers = line.split()
|
|
284
|
+
if len(numbers) == 3:
|
|
285
|
+
v1, v2, v3 = numbers
|
|
286
|
+
v4 = v3
|
|
287
|
+
elif len(numbers) == 4:
|
|
288
|
+
v1, v2, v3, v4 = numbers
|
|
289
|
+
faces.append([v1, v2, v3, v4])
|
|
290
|
+
|
|
291
|
+
elif current_context == ('panels', 1): # parse face definition
|
|
292
|
+
numbers = line.split()
|
|
293
|
+
if len(numbers) == 4:
|
|
294
|
+
i_face, v1, v2, v3 = numbers
|
|
295
|
+
v4 = v3
|
|
296
|
+
elif len(numbers) == 5:
|
|
297
|
+
i_face, v1, v2, v3, v4 = numbers
|
|
298
|
+
|
|
299
|
+
if int(i_face) != len(faces) + 1:
|
|
300
|
+
ii = len(faces) + 1
|
|
301
|
+
raise ValueError(f"HST mesh reader expected the next face to be indexed {ii},\n"
|
|
302
|
+
f"but it was actually indexed with {i_face} (line {i_line+1}.")
|
|
303
|
+
faces.append([v1, v2, v3, v4])
|
|
304
|
+
|
|
305
|
+
elif line.startswith("ENDFILE"):
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
for keyword in optional_data:
|
|
310
|
+
if line.startswith(keyword):
|
|
311
|
+
optional_data[keyword] = line[len(keyword)+1:].lstrip(':').strip()
|
|
312
|
+
break
|
|
313
|
+
else:
|
|
314
|
+
ignored_lines.append((i_line+1, line))
|
|
315
|
+
|
|
316
|
+
if len(ignored_lines) > 0:
|
|
317
|
+
formatted_ignored_lines = ["{: 4} | {}".format(i, line.strip('\n')) for (i, line) in ignored_lines]
|
|
318
|
+
LOG.warning("HST mesh reader ignored the following lines:\n" + "\n".join(formatted_ignored_lines))
|
|
319
|
+
|
|
320
|
+
vertices = np.array(vertices, dtype=float)
|
|
321
|
+
faces = np.array(faces, dtype=int) - 1
|
|
322
|
+
|
|
323
|
+
if optional_data['SYMMETRY'] == '1':
|
|
324
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz")
|
|
325
|
+
elif optional_data['SYMMETRY'] == '2':
|
|
326
|
+
return ReflectionSymmetricMesh(
|
|
327
|
+
ReflectionSymmetricMesh(Mesh(vertices, faces), plane="yOz"),
|
|
328
|
+
plane="xOz",
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
return Mesh(vertices, faces)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _read_gdf(file_obj: IOBase) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
335
|
+
title = file_obj.readline()
|
|
336
|
+
ulen, grav = map(float, file_obj.readline().split()[:2])
|
|
337
|
+
isx, isy = map(int, file_obj.readline().split()[:2])
|
|
338
|
+
npan = int(file_obj.readline().split()[0])
|
|
339
|
+
faces_vertices = np.genfromtxt(file_obj)
|
|
340
|
+
|
|
341
|
+
faces_vertices = faces_vertices.reshape(-1, 3)
|
|
342
|
+
vertices, indices = np.unique(faces_vertices, axis=0, return_inverse=True)
|
|
343
|
+
faces = indices.reshape(-1, 4)
|
|
344
|
+
|
|
345
|
+
if faces.shape[0] != npan:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"In GDF file, npan value: {npan} is not equal to face count: \
|
|
348
|
+
{faces.shape[0]}."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if isx == 1 and isy == 1:
|
|
352
|
+
return ReflectionSymmetricMesh(
|
|
353
|
+
ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz"),
|
|
354
|
+
plane="yOz",
|
|
355
|
+
)
|
|
356
|
+
elif isx == 1:
|
|
357
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="yOz")
|
|
358
|
+
elif isy == 1:
|
|
359
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz")
|
|
360
|
+
else:
|
|
361
|
+
return Mesh(vertices, faces)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _read_mar(file_obj) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
365
|
+
vertices = []
|
|
366
|
+
faces = []
|
|
367
|
+
|
|
368
|
+
# Read header: "n_faces symmetry_flag"
|
|
369
|
+
header = file_obj.readline().split()
|
|
370
|
+
symmetry_flag = int(header[1]) if len(header) > 1 else 0
|
|
371
|
+
|
|
372
|
+
# Read vertices until "0" marker
|
|
373
|
+
for line in file_obj:
|
|
374
|
+
tokens = line.split()
|
|
375
|
+
if tokens[0] == "0":
|
|
376
|
+
break
|
|
377
|
+
vertices.append(list(map(float, tokens[1:])))
|
|
378
|
+
|
|
379
|
+
# Read faces until "0" marker
|
|
380
|
+
for line in file_obj:
|
|
381
|
+
tokens = line.split()
|
|
382
|
+
if tokens[0] == "0":
|
|
383
|
+
break
|
|
384
|
+
faces.append(list(map(int, tokens)))
|
|
385
|
+
|
|
386
|
+
# Convert to numpy arrays and adjust indices (Fortran 1-based to Python 0-based)
|
|
387
|
+
vertices = np.array(vertices, dtype=float)
|
|
388
|
+
faces = np.array(faces, dtype=int) - 1
|
|
389
|
+
|
|
390
|
+
if symmetry_flag == 1:
|
|
391
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz")
|
|
392
|
+
else:
|
|
393
|
+
return Mesh(vertices, faces)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _read_pnl(file_obj) -> Union[Mesh, ReflectionSymmetricMesh]:
|
|
397
|
+
# Skip 3 title lines
|
|
398
|
+
file_obj.readline()
|
|
399
|
+
file_obj.readline()
|
|
400
|
+
file_obj.readline()
|
|
401
|
+
# Read header data
|
|
402
|
+
nb_faces, nb_vertices, x_sym, y_sym = map(int, file_obj.readline().split())
|
|
403
|
+
# Skip 2 more lines
|
|
404
|
+
file_obj.readline()
|
|
405
|
+
file_obj.readline()
|
|
406
|
+
vertices = np.genfromtxt((file_obj.readline() for _ in range(nb_vertices)), usecols=(1, 2, 3))
|
|
407
|
+
# Skip 3 more lines
|
|
408
|
+
file_obj.readline()
|
|
409
|
+
file_obj.readline()
|
|
410
|
+
file_obj.readline()
|
|
411
|
+
|
|
412
|
+
faces = np.zeros((nb_faces, 4), dtype=int)
|
|
413
|
+
for i in range(nb_faces):
|
|
414
|
+
index, nb_corners, *data = map(int, file_obj.readline().split())
|
|
415
|
+
assert i+1 == index
|
|
416
|
+
if nb_corners == 3: # Triangle
|
|
417
|
+
assert len(data) == 3
|
|
418
|
+
faces[i, 0:3] = data
|
|
419
|
+
faces[i, 3] = faces[i, 2] # Convention for triangles in Capytaine: repeat last vertex
|
|
420
|
+
elif int(nb_corners) == 4: # Quadrangle
|
|
421
|
+
assert len(data) == 4
|
|
422
|
+
faces[i, :] = data
|
|
423
|
+
faces = faces - 1 # Going from Fortran 1-based indices to Numpy 0-based indices
|
|
424
|
+
|
|
425
|
+
if x_sym == 1 and y_sym == 0:
|
|
426
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="yOz")
|
|
427
|
+
elif x_sym == 0 and y_sym == 1:
|
|
428
|
+
return ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz")
|
|
429
|
+
elif x_sym == 1 and y_sym == 1:
|
|
430
|
+
half_mesh = ReflectionSymmetricMesh(Mesh(vertices, faces), plane="xOz")
|
|
431
|
+
return ReflectionSymmetricMesh(half_mesh, plane="yOz")
|
|
432
|
+
else:
|
|
433
|
+
return Mesh(vertices, faces)
|