capytaine 2.3.1__cp310-cp310-win_amd64.whl → 3.0.0a1__cp310-cp310-win_amd64.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 (88) hide show
  1. capytaine/__about__.py +7 -2
  2. capytaine/__init__.py +11 -15
  3. capytaine/bem/engines.py +234 -354
  4. capytaine/bem/problems_and_results.py +14 -13
  5. capytaine/bem/solver.py +204 -80
  6. capytaine/bodies/bodies.py +278 -869
  7. capytaine/bodies/dofs.py +136 -9
  8. capytaine/bodies/hydrostatics.py +540 -0
  9. capytaine/bodies/multibodies.py +216 -0
  10. capytaine/green_functions/{libs/Delhommeau_float32.cp310-win_amd64.dll.a → Delhommeau_float32.cp310-win_amd64.dll.a} +0 -0
  11. capytaine/green_functions/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
  12. capytaine/green_functions/{libs/Delhommeau_float64.cp310-win_amd64.dll.a → Delhommeau_float64.cp310-win_amd64.dll.a} +0 -0
  13. capytaine/green_functions/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
  14. capytaine/green_functions/abstract_green_function.py +2 -2
  15. capytaine/green_functions/delhommeau.py +31 -16
  16. capytaine/green_functions/hams.py +19 -13
  17. capytaine/io/legacy.py +3 -103
  18. capytaine/io/xarray.py +11 -6
  19. capytaine/meshes/__init__.py +2 -6
  20. capytaine/meshes/abstract_meshes.py +375 -0
  21. capytaine/meshes/clean.py +302 -0
  22. capytaine/meshes/clip.py +347 -0
  23. capytaine/meshes/export.py +89 -0
  24. capytaine/meshes/geometry.py +244 -394
  25. capytaine/meshes/io.py +433 -0
  26. capytaine/meshes/meshes.py +617 -681
  27. capytaine/meshes/predefined/cylinders.py +22 -56
  28. capytaine/meshes/predefined/rectangles.py +26 -85
  29. capytaine/meshes/predefined/spheres.py +4 -11
  30. capytaine/meshes/quality.py +118 -407
  31. capytaine/meshes/surface_integrals.py +48 -29
  32. capytaine/meshes/symmetric_meshes.py +641 -0
  33. capytaine/meshes/visualization.py +353 -0
  34. capytaine/post_pro/free_surfaces.py +1 -4
  35. capytaine/post_pro/kochin.py +10 -10
  36. capytaine/tools/block_circulant_matrices.py +275 -0
  37. capytaine/tools/lists_of_points.py +2 -2
  38. capytaine/tools/memory_monitor.py +45 -0
  39. capytaine/tools/symbolic_multiplication.py +13 -1
  40. capytaine/tools/timer.py +58 -34
  41. capytaine-3.0.0a1.dist-info/DELVEWHEEL +2 -0
  42. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +7 -2
  43. capytaine-3.0.0a1.dist-info/RECORD +70 -0
  44. capytaine/bodies/predefined/__init__.py +0 -6
  45. capytaine/bodies/predefined/cylinders.py +0 -151
  46. capytaine/bodies/predefined/rectangles.py +0 -111
  47. capytaine/bodies/predefined/spheres.py +0 -70
  48. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  49. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  50. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  51. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  52. capytaine/green_functions/FinGreen3D/README.md +0 -24
  53. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  54. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  55. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  56. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  57. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -16
  58. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  59. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  60. capytaine/green_functions/libs/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
  61. capytaine/green_functions/libs/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
  62. capytaine/green_functions/libs/__init__.py +0 -0
  63. capytaine/io/mesh_loaders.py +0 -1086
  64. capytaine/io/mesh_writers.py +0 -692
  65. capytaine/io/meshio.py +0 -38
  66. capytaine/matrices/__init__.py +0 -16
  67. capytaine/matrices/block.py +0 -592
  68. capytaine/matrices/block_toeplitz.py +0 -325
  69. capytaine/matrices/builders.py +0 -89
  70. capytaine/matrices/linear_solvers.py +0 -232
  71. capytaine/matrices/low_rank.py +0 -395
  72. capytaine/meshes/clipper.py +0 -465
  73. capytaine/meshes/collections.py +0 -342
  74. capytaine/meshes/mesh_like_protocol.py +0 -37
  75. capytaine/meshes/properties.py +0 -276
  76. capytaine/meshes/quadratures.py +0 -80
  77. capytaine/meshes/symmetric.py +0 -462
  78. capytaine/tools/lru_cache.py +0 -49
  79. capytaine/ui/vtk/__init__.py +0 -3
  80. capytaine/ui/vtk/animation.py +0 -329
  81. capytaine/ui/vtk/body_viewer.py +0 -28
  82. capytaine/ui/vtk/helpers.py +0 -82
  83. capytaine/ui/vtk/mesh_viewer.py +0 -461
  84. capytaine-2.3.1.dist-info/DELVEWHEEL +0 -2
  85. capytaine-2.3.1.dist-info/RECORD +0 -97
  86. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  87. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
  88. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -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)