mmgpy 0.3.0__cp312-cp312-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 (132) hide show
  1. bin/Release/mmg2d_O3.exe +0 -0
  2. bin/Release/mmg3d_O3.exe +0 -0
  3. bin/Release/mmgs_O3.exe +0 -0
  4. bin/__init__.py +10 -0
  5. bin/concrt140.dll +0 -0
  6. bin/mmg.dll +0 -0
  7. bin/mmg2d.dll +0 -0
  8. bin/mmg2d_O3.exe +0 -0
  9. bin/mmg3d.dll +0 -0
  10. bin/mmg3d_O3.exe +0 -0
  11. bin/mmgs.dll +0 -0
  12. bin/mmgs_O3.exe +0 -0
  13. bin/msvcp140.dll +0 -0
  14. bin/msvcp140_1.dll +0 -0
  15. bin/msvcp140_2.dll +0 -0
  16. bin/msvcp140_atomic_wait.dll +0 -0
  17. bin/msvcp140_codecvt_ids.dll +0 -0
  18. bin/vcruntime140.dll +0 -0
  19. bin/vcruntime140_1.dll +0 -0
  20. include/__init__.py +10 -0
  21. include/mmg/common/libmmgtypes.h +687 -0
  22. include/mmg/common/libmmgtypesf.h +762 -0
  23. include/mmg/common/mmg_export.h +47 -0
  24. include/mmg/common/mmgcmakedefines.h +46 -0
  25. include/mmg/common/mmgcmakedefinesf.h +29 -0
  26. include/mmg/common/mmgversion.h +54 -0
  27. include/mmg/libmmg.h +67 -0
  28. include/mmg/libmmgf.h +42 -0
  29. include/mmg/mmg2d/libmmg2d.h +2761 -0
  30. include/mmg/mmg2d/libmmg2df.h +3263 -0
  31. include/mmg/mmg2d/mmg2d_export.h +34 -0
  32. include/mmg/mmg3d/libmmg3d.h +3444 -0
  33. include/mmg/mmg3d/libmmg3df.h +4041 -0
  34. include/mmg/mmg3d/mmg3d_export.h +34 -0
  35. include/mmg/mmgs/libmmgs.h +2560 -0
  36. include/mmg/mmgs/libmmgsf.h +3028 -0
  37. include/mmg/mmgs/mmgs_export.h +34 -0
  38. lib/__init__.py +10 -0
  39. lib/cmake/mmg/FindElas.cmake +57 -0
  40. lib/cmake/mmg/FindSCOTCH.cmake +373 -0
  41. lib/cmake/mmg/MmgTargets-release.cmake +53 -0
  42. lib/cmake/mmg/MmgTargets.cmake +127 -0
  43. lib/cmake/mmg/mmgConfig.cmake +43 -0
  44. lib/mmg.lib +0 -0
  45. lib/mmg2d.lib +0 -0
  46. lib/mmg3d.lib +0 -0
  47. lib/mmgs.lib +0 -0
  48. mmgpy/__init__.py +888 -0
  49. mmgpy/_logging.py +86 -0
  50. mmgpy/_mmgpy.cp312-win_amd64.pyd +0 -0
  51. mmgpy/_mmgpy.pyi +650 -0
  52. mmgpy/_options.py +304 -0
  53. mmgpy/_progress.py +539 -0
  54. mmgpy/_pyvista.py +423 -0
  55. mmgpy/_version.py +3 -0
  56. mmgpy/_version.py.in +3 -0
  57. mmgpy/lagrangian.py +394 -0
  58. mmgpy/metrics.py +595 -0
  59. mmgpy/mmg2d.dll +0 -0
  60. mmgpy/mmg2d.lib +0 -0
  61. mmgpy/mmg3d.dll +0 -0
  62. mmgpy/mmg3d.lib +0 -0
  63. mmgpy/mmgs.dll +0 -0
  64. mmgpy/mmgs.lib +0 -0
  65. mmgpy/progress.py +57 -0
  66. mmgpy/py.typed +0 -0
  67. mmgpy/sizing.py +370 -0
  68. mmgpy-0.3.0.dist-info/DELVEWHEEL +2 -0
  69. mmgpy-0.3.0.dist-info/METADATA +75 -0
  70. mmgpy-0.3.0.dist-info/RECORD +132 -0
  71. mmgpy-0.3.0.dist-info/WHEEL +5 -0
  72. mmgpy-0.3.0.dist-info/entry_points.txt +6 -0
  73. mmgpy-0.3.0.dist-info/licenses/LICENSE +38 -0
  74. mmgpy.libs/vtkCommonColor-9.5-07cd19e9d77559cb8be83e8ac8833cd4.dll +0 -0
  75. mmgpy.libs/vtkCommonComputationalGeometry-9.5-4aaf997b087c330e171c14a4ba6be7b2.dll +0 -0
  76. mmgpy.libs/vtkCommonCore-9.5.dll +0 -0
  77. mmgpy.libs/vtkCommonDataModel-9.5.dll +0 -0
  78. mmgpy.libs/vtkCommonExecutionModel-9.5-2f7a1bae0a1d4d0e205eea43596a659c.dll +0 -0
  79. mmgpy.libs/vtkCommonMath-9.5-609b01246386fe29df2677fa5c7ca793.dll +0 -0
  80. mmgpy.libs/vtkCommonMisc-9.5-4173df33811eddea1529a40bf93266c8.dll +0 -0
  81. mmgpy.libs/vtkCommonSystem-9.5-e5b15bd84934b99e3b2bbe5d3e064c97.dll +0 -0
  82. mmgpy.libs/vtkCommonTransforms-9.5-9b76a61640718d893271cc0b5db50d1d.dll +0 -0
  83. mmgpy.libs/vtkDICOMParser-9.5-203c95a77d21799a8049a576e1b28f2e.dll +0 -0
  84. mmgpy.libs/vtkFiltersCellGrid-9.5-fa6bda61d2d528369d8b2f3a66d2d6b4.dll +0 -0
  85. mmgpy.libs/vtkFiltersCore-9.5-935a5f5225a975e99626296b2f3ded70.dll +0 -0
  86. mmgpy.libs/vtkFiltersExtraction-9.5-dc0a7543ba584f7e8ce9f9184485a228.dll +0 -0
  87. mmgpy.libs/vtkFiltersGeneral-9.5-709f69dbcca8aba1750582106a97c605.dll +0 -0
  88. mmgpy.libs/vtkFiltersGeometry-9.5-7abfb655763a62f56d63b45038d6e811.dll +0 -0
  89. mmgpy.libs/vtkFiltersHybrid-9.5-0721ec98d8a8b7442d900747e1ec59fb.dll +0 -0
  90. mmgpy.libs/vtkFiltersHyperTree-9.5-f9ee6a4761fdad8956c08a51dae77636.dll +0 -0
  91. mmgpy.libs/vtkFiltersModeling-9.5-458d9d2c544bb3c37de28c26c05c07bc.dll +0 -0
  92. mmgpy.libs/vtkFiltersParallel-9.5-1f243ffe308277c3970d8be4172d856f.dll +0 -0
  93. mmgpy.libs/vtkFiltersReduction-9.5-bf8c4a248bd84fbd6bb1a2ab5f646e56.dll +0 -0
  94. mmgpy.libs/vtkFiltersSources-9.5-492fa5b1b8562f4b141a347a38ae1ce5.dll +0 -0
  95. mmgpy.libs/vtkFiltersStatistics-9.5-6e99ef76387303ec5ff8c0fe6101d446.dll +0 -0
  96. mmgpy.libs/vtkFiltersTexture-9.5-15c23120b41b9a1c4acb01f790aad01f.dll +0 -0
  97. mmgpy.libs/vtkFiltersVerdict-9.5-332c0402a58129ec5b6af7b7f56cbb62.dll +0 -0
  98. mmgpy.libs/vtkIOCellGrid-9.5-88e1ec9c5a3554a82aedc0027fe84c6b.dll +0 -0
  99. mmgpy.libs/vtkIOCore-9.5.dll +0 -0
  100. mmgpy.libs/vtkIOGeometry-9.5-47c69db15c63c5773efa6851b59ae0a7.dll +0 -0
  101. mmgpy.libs/vtkIOImage-9.5-74bb92e688da5595ff9ff7645f9a0a13.dll +0 -0
  102. mmgpy.libs/vtkIOLegacy-9.5.dll +0 -0
  103. mmgpy.libs/vtkIOParallel-9.5.dll +0 -0
  104. mmgpy.libs/vtkIOParallelXML-9.5.dll +0 -0
  105. mmgpy.libs/vtkIOXML-9.5.dll +0 -0
  106. mmgpy.libs/vtkIOXMLParser-9.5-1893156c41fd4cf7165904675cb5d15d.dll +0 -0
  107. mmgpy.libs/vtkImagingCore-9.5-145fc0249cffbd27c610d10812e1cbfc.dll +0 -0
  108. mmgpy.libs/vtkImagingSources-9.5-f0c087a4669caa045584ed61f52502b7.dll +0 -0
  109. mmgpy.libs/vtkParallelCore-9.5-e91757b6dbd2a5369ab2bd05ff95d79d.dll +0 -0
  110. mmgpy.libs/vtkParallelDIY-9.5-04dd6b6b5dd8a5eacd43d270999cf09a.dll +0 -0
  111. mmgpy.libs/vtkRenderingCore-9.5-24a9802d77a083def26449fa681b1af7.dll +0 -0
  112. mmgpy.libs/vtkdoubleconversion-9.5-5e39712b9f4e44ea8a26e9119e53a7d4.dll +0 -0
  113. mmgpy.libs/vtkexpat-9.5-3b1dd25e09a2cccbbac723de448cb894.dll +0 -0
  114. mmgpy.libs/vtkfmt-9.5-50239b66bf315d100ecd306114139e9b.dll +0 -0
  115. mmgpy.libs/vtkjpeg-9.5-9412ee79f685a9196398b988a59666cd.dll +0 -0
  116. mmgpy.libs/vtkjsoncpp-9.5-abfad956527e3a4885dbb39f99f9e4d4.dll +0 -0
  117. mmgpy.libs/vtkkissfft-9.5-464db9175ce63de19addc69be524c4b7.dll +0 -0
  118. mmgpy.libs/vtkloguru-9.5-ec016ed005b4a79062e329ad8f1c382d.dll +0 -0
  119. mmgpy.libs/vtklz4-9.5-798b58f4518733b0eee8027eeba022fb.dll +0 -0
  120. mmgpy.libs/vtklzma-9.5-8f489b5430eb47d578de52c769a4dd5c.dll +0 -0
  121. mmgpy.libs/vtkmetaio-9.5-8f0a559399d53e4c7fc06272620b2167.dll +0 -0
  122. mmgpy.libs/vtkpng-9.5-1dbed3116ba7e31f56512a93b942cdf5.dll +0 -0
  123. mmgpy.libs/vtkpugixml-9.5-23ef37d65494ab52babc6d45b24764b7.dll +0 -0
  124. mmgpy.libs/vtksys-9.5.dll +0 -0
  125. mmgpy.libs/vtktiff-9.5-767fd93c8402517d5b2d1befab98c41e.dll +0 -0
  126. mmgpy.libs/vtktoken-9.5-f4ff567202eeb9a613c0b242aa05dbc9.dll +0 -0
  127. mmgpy.libs/vtkverdict-9.5-6bb84649d1b0ca1cc5454307fd35083b.dll +0 -0
  128. mmgpy.libs/vtkzlib-9.5-779937c44671e188e9f96125eb5afb12.dll +0 -0
  129. share/__init__.py +10 -0
  130. share/man/man1/mmg2d.1.gz +0 -0
  131. share/man/man1/mmg3d.1.gz +0 -0
  132. share/man/man1/mmgs.1.gz +0 -0
mmgpy/__init__.py ADDED
@@ -0,0 +1,888 @@
1
+ """Python bindings for the MMG library."""
2
+
3
+
4
+ # start delvewheel patch
5
+ def _delvewheel_patch_1_11_2():
6
+ import os
7
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'mmgpy.libs'))):
8
+ os.add_dll_directory(libs_dir)
9
+
10
+
11
+ _delvewheel_patch_1_11_2()
12
+ del _delvewheel_patch_1_11_2
13
+ # end delvewheel patch
14
+
15
+ import os
16
+ import platform
17
+ import site
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from ._logging import (
23
+ disable_logging,
24
+ enable_debug,
25
+ get_logger,
26
+ set_log_level,
27
+ )
28
+
29
+ _logger = get_logger()
30
+
31
+ # Handle DLL loading on Windows
32
+ if sys.platform == "win32":
33
+ # Get the directory containing this file
34
+ _module_dir = Path(__file__).absolute().parent
35
+
36
+ # Add common DLL directories to search path
37
+ dll_search_dirs = [
38
+ _module_dir, # Module directory itself
39
+ _module_dir / "lib", # Common lib subdirectory
40
+ _module_dir / "Scripts", # Common bin subdirectory
41
+ _module_dir / ".libs", # delvewheel directory
42
+ Path("C:/vcpkg/installed/x64-windows/bin"), # VTK DLLs from vcpkg
43
+ ]
44
+
45
+ for dll_dir in dll_search_dirs:
46
+ if dll_dir.exists() and dll_dir.is_dir():
47
+ try:
48
+ os.add_dll_directory(str(dll_dir))
49
+ _logger.debug("Added DLL directory: %s", dll_dir)
50
+ except (OSError, AttributeError):
51
+ os.environ.setdefault("PATH", "")
52
+ if str(dll_dir) not in os.environ["PATH"]:
53
+ os.environ["PATH"] = str(dll_dir) + os.pathsep + os.environ["PATH"]
54
+ _logger.debug("Added to PATH: %s", dll_dir)
55
+
56
+ # Let delvewheel handle the rest of the imports
57
+ # Import after DLL setup is complete
58
+ try:
59
+ from . import _version # type: ignore[attr-defined]
60
+
61
+ __version__ = _version.__version__
62
+ except ImportError:
63
+ __version__ = "unknown"
64
+
65
+ # Main imports
66
+ try:
67
+ from ._mmgpy import ( # type: ignore[attr-defined]
68
+ MMG_VERSION,
69
+ MmgMesh2D,
70
+ MmgMesh3D,
71
+ MmgMeshS,
72
+ mmg2d,
73
+ mmg3d,
74
+ mmgs,
75
+ )
76
+
77
+ except ImportError:
78
+ if sys.platform == "win32":
79
+ _module_dir = Path(__file__).absolute().parent
80
+ available_files = list(_module_dir.glob("*"))
81
+ lib_files = list(_module_dir.glob("**/*.dll")) + list(
82
+ _module_dir.glob("**/*.pyd"),
83
+ )
84
+
85
+ _logger.exception(
86
+ "Failed to import _mmgpy module on Windows.\n"
87
+ "Module directory: %s\n"
88
+ "Available files: %s\n"
89
+ "Found DLLs/PYDs: %s\n"
90
+ "To debug, set MMGPY_DEBUG=1 or call mmgpy.enable_debug().",
91
+ _module_dir,
92
+ [f.name for f in available_files],
93
+ [str(f) for f in lib_files],
94
+ )
95
+ raise
96
+
97
+
98
+ def _run_mmg2d() -> None:
99
+ """Run the mmg2d_O3 executable."""
100
+ # Find the executable in site-packages for installed package
101
+ site_packages_list = site.getsitepackages()
102
+ # On Windows, prefer the actual site-packages over the venv root
103
+ if sys.platform == "win32" and len(site_packages_list) > 1:
104
+ site_packages = Path(site_packages_list[1])
105
+ else:
106
+ site_packages = Path(site_packages_list[0])
107
+
108
+ scripts_dir = "bin" # Always use bin for the actual MMG executables
109
+ exe_name = "mmg2d_O3.exe" if sys.platform == "win32" else "mmg2d_O3"
110
+ exe_path = site_packages / scripts_dir / exe_name
111
+
112
+ if exe_path.exists():
113
+ subprocess.run([str(exe_path)] + sys.argv[1:], check=False)
114
+ else:
115
+ _logger.error("mmg2d_O3 executable not found at %s", exe_path)
116
+ sys.exit(1)
117
+
118
+
119
+ def _run_mmg3d() -> None:
120
+ """Run the mmg3d_O3 executable."""
121
+ site_packages_list = site.getsitepackages()
122
+ if sys.platform == "win32" and len(site_packages_list) > 1:
123
+ site_packages = Path(site_packages_list[1])
124
+ else:
125
+ site_packages = Path(site_packages_list[0])
126
+
127
+ scripts_dir = "bin"
128
+ exe_name = "mmg3d_O3.exe" if sys.platform == "win32" else "mmg3d_O3"
129
+ exe_path = site_packages / scripts_dir / exe_name
130
+
131
+ if exe_path.exists():
132
+ subprocess.run([str(exe_path)] + sys.argv[1:], check=False)
133
+ else:
134
+ _logger.error("mmg3d_O3 executable not found at %s", exe_path)
135
+ sys.exit(1)
136
+
137
+
138
+ def _run_mmgs() -> None:
139
+ """Run the mmgs_O3 executable."""
140
+ site_packages_list = site.getsitepackages()
141
+ if sys.platform == "win32" and len(site_packages_list) > 1:
142
+ site_packages = Path(site_packages_list[1])
143
+ else:
144
+ site_packages = Path(site_packages_list[0])
145
+
146
+ scripts_dir = "bin"
147
+ exe_name = "mmgs_O3.exe" if sys.platform == "win32" else "mmgs_O3"
148
+ exe_path = site_packages / scripts_dir / exe_name
149
+
150
+ if exe_path.exists():
151
+ subprocess.run([str(exe_path)] + sys.argv[1:], check=False)
152
+ else:
153
+ _logger.error("mmgs_O3 executable not found at %s", exe_path)
154
+ sys.exit(1)
155
+
156
+
157
+ def _fix_rpath() -> None:
158
+ """Fix RPATH for MMG executables - post-install utility."""
159
+ system = platform.system()
160
+ if system == "Darwin":
161
+ try:
162
+ _fix_rpath_macos()
163
+ except (OSError, subprocess.SubprocessError):
164
+ _logger.exception("Error fixing RPATH")
165
+ raise
166
+ elif system == "Linux":
167
+ try:
168
+ _fix_rpath_linux()
169
+ except (OSError, subprocess.SubprocessError):
170
+ _logger.exception("Error fixing RPATH")
171
+ raise
172
+ else:
173
+ _logger.debug("RPATH fix not needed for %s", system)
174
+
175
+
176
+ def _fix_rpath_macos() -> None:
177
+ """Fix RPATH for MMG executables on macOS."""
178
+ site_packages = Path(site.getsitepackages()[0])
179
+ _logger.debug("Site packages: %s", site_packages)
180
+
181
+ bin_dir = site_packages / "bin"
182
+ if not bin_dir.exists():
183
+ _logger.warning("Bin directory does not exist: %s", bin_dir)
184
+ return
185
+
186
+ executables = list(bin_dir.glob("mmg*_O3"))
187
+ if not executables:
188
+ _logger.warning("No MMG executables found")
189
+ return
190
+
191
+ _logger.debug("Found %d executables to fix", len(executables))
192
+
193
+ for exe in executables:
194
+ _fix_single_executable_rpath(exe)
195
+
196
+
197
+ def _fix_single_executable_rpath(exe: "Path") -> None:
198
+ """Fix RPATH for a single executable."""
199
+ _logger.debug("Fixing RPATH for %s...", exe.name)
200
+
201
+ if not exe.exists() or not exe.is_file():
202
+ _logger.debug("Skipping %s - not a valid file", exe.name)
203
+ return
204
+
205
+ target_rpath = "@loader_path/../mmgpy/lib"
206
+
207
+ if _has_correct_rpath(exe, target_rpath):
208
+ _logger.debug("RPATH already correct for %s", exe.name)
209
+ return
210
+
211
+ _remove_old_rpath(exe)
212
+ if _add_new_rpath(exe, target_rpath):
213
+ _verify_rpath_fix(exe, target_rpath)
214
+
215
+
216
+ def _has_correct_rpath(exe: "Path", target_rpath: str) -> bool:
217
+ """Check if executable has the correct RPATH."""
218
+ result = subprocess.run(
219
+ ["/usr/bin/otool", "-l", str(exe)],
220
+ capture_output=True,
221
+ text=True,
222
+ check=False,
223
+ )
224
+ return result.returncode == 0 and target_rpath in result.stdout
225
+
226
+
227
+ def _remove_old_rpath(exe: "Path") -> None:
228
+ """Remove existing @rpath entries from executable."""
229
+ subprocess.run(
230
+ ["/usr/bin/install_name_tool", "-delete_rpath", "@rpath", str(exe)],
231
+ check=False,
232
+ capture_output=True,
233
+ )
234
+
235
+
236
+ def _add_new_rpath(exe: "Path", target_rpath: str) -> bool:
237
+ """Add new RPATH to executable. Returns True if successful."""
238
+ result = subprocess.run(
239
+ ["/usr/bin/install_name_tool", "-add_rpath", target_rpath, str(exe)],
240
+ capture_output=True,
241
+ text=True,
242
+ check=False,
243
+ )
244
+
245
+ if result.returncode == 0:
246
+ _logger.info("Successfully fixed RPATH for %s", exe.name)
247
+ return True
248
+ _logger.error("Failed to fix RPATH for %s: %s", exe.name, result.stderr)
249
+ return False
250
+
251
+
252
+ def _verify_rpath_fix(exe: "Path", target_rpath: str) -> None:
253
+ """Verify that RPATH fix was successful."""
254
+ verify_result = subprocess.run(
255
+ ["/usr/bin/otool", "-l", str(exe)],
256
+ capture_output=True,
257
+ text=True,
258
+ check=False,
259
+ )
260
+
261
+ if target_rpath in verify_result.stdout:
262
+ _logger.debug("RPATH verification successful for %s", exe.name)
263
+ else:
264
+ _logger.warning("RPATH verification failed for %s", exe.name)
265
+
266
+
267
+ def _fix_rpath_linux() -> None:
268
+ """Fix RPATH for MMG executables on Linux using patchelf."""
269
+ site_packages = Path(site.getsitepackages()[0])
270
+ _logger.debug("Site packages: %s", site_packages)
271
+
272
+ bin_dir = site_packages / "bin"
273
+ if not bin_dir.exists():
274
+ _logger.warning("Bin directory does not exist: %s", bin_dir)
275
+ return
276
+
277
+ executables = list(bin_dir.glob("mmg*_O3"))
278
+ if not executables:
279
+ _logger.warning("No MMG executables found")
280
+ return
281
+
282
+ _logger.debug("Found %d executables to fix", len(executables))
283
+
284
+ lib_dirs = [
285
+ str(site_packages / "lib"),
286
+ str(site_packages / "mmgpy" / "lib"),
287
+ ]
288
+
289
+ for exe in executables:
290
+ _fix_single_executable_rpath_linux(exe, lib_dirs)
291
+
292
+
293
+ def _fix_single_executable_rpath_linux(exe: "Path", lib_dirs: list[str]) -> None:
294
+ """Fix RPATH for a single executable on Linux."""
295
+ _logger.debug("Fixing RPATH for %s...", exe.name)
296
+
297
+ if not exe.exists() or not exe.is_file():
298
+ _logger.debug("Skipping %s - not a valid file", exe.name)
299
+ return
300
+
301
+ try:
302
+ rpath = ":".join(lib_dirs)
303
+ result = subprocess.run(
304
+ ["patchelf", "--set-rpath", rpath, str(exe)], # noqa: S607
305
+ capture_output=True,
306
+ text=True,
307
+ check=False,
308
+ )
309
+
310
+ if result.returncode == 0:
311
+ _logger.info("Successfully fixed RPATH for %s", exe.name)
312
+ _verify_rpath_fix_linux(exe, lib_dirs)
313
+ else:
314
+ _logger.error("Failed to fix RPATH for %s: %s", exe.name, result.stderr)
315
+
316
+ except FileNotFoundError:
317
+ _logger.debug("patchelf not found - trying venv patchelf...")
318
+ venv_patchelf = Path(sys.executable).parent / "patchelf"
319
+ if venv_patchelf.exists():
320
+ result = subprocess.run(
321
+ [str(venv_patchelf), "--set-rpath", ":".join(lib_dirs), str(exe)],
322
+ capture_output=True,
323
+ text=True,
324
+ check=False,
325
+ )
326
+ if result.returncode == 0:
327
+ _logger.info("Successfully fixed RPATH for %s", exe.name)
328
+ else:
329
+ _logger.error( # noqa: TRY400
330
+ "Failed to fix RPATH for %s: %s",
331
+ exe.name,
332
+ result.stderr,
333
+ )
334
+ else:
335
+ _logger.warning(
336
+ "patchelf not available - RPATH fix skipped for %s",
337
+ exe.name,
338
+ )
339
+
340
+
341
+ def _verify_rpath_fix_linux(exe: "Path", lib_dirs: list[str]) -> None:
342
+ """Verify that RPATH fix was successful on Linux."""
343
+ try:
344
+ verify_result = subprocess.run(
345
+ ["patchelf", "--print-rpath", str(exe)], # noqa: S607
346
+ capture_output=True,
347
+ text=True,
348
+ check=False,
349
+ )
350
+
351
+ if verify_result.returncode == 0:
352
+ current_rpath = verify_result.stdout.strip()
353
+ _logger.debug("Current RPATH: %s", current_rpath)
354
+
355
+ rpath_dirs = current_rpath.split(":")
356
+ missing_dirs = [d for d in lib_dirs if d not in rpath_dirs]
357
+
358
+ if not missing_dirs:
359
+ _logger.debug("RPATH verification successful for %s", exe.name)
360
+ else:
361
+ _logger.warning(
362
+ "RPATH verification failed for %s - missing: %s",
363
+ exe.name,
364
+ missing_dirs,
365
+ )
366
+ else:
367
+ _logger.warning(
368
+ "RPATH verification failed for %s: %s",
369
+ exe.name,
370
+ verify_result.stderr,
371
+ )
372
+ except FileNotFoundError:
373
+ _logger.debug(
374
+ "Could not verify RPATH for %s - patchelf not available",
375
+ exe.name,
376
+ )
377
+
378
+
379
+ from . import lagrangian, metrics, progress, sizing
380
+ from ._options import Mmg2DOptions, Mmg3DOptions, MmgSOptions
381
+ from ._progress import ProgressEvent, rich_progress
382
+ from ._pyvista import add_pyvista_methods, from_pyvista, to_pyvista
383
+ from .lagrangian import detect_boundary_vertices, move_mesh, propagate_displacement
384
+ from .sizing import (
385
+ BoxSize,
386
+ CylinderSize,
387
+ PointSize,
388
+ SizingConstraint,
389
+ SphereSize,
390
+ apply_sizing_constraints,
391
+ )
392
+
393
+ # Add from_pyvista/to_pyvista methods to mesh classes
394
+ add_pyvista_methods()
395
+
396
+
397
+ def _add_convenience_methods() -> None:
398
+ """Add convenience remeshing methods and wrap remesh() to accept options objects."""
399
+ from collections.abc import Callable # noqa: PLC0415
400
+ from typing import Any # noqa: PLC0415
401
+
402
+ # Wrap remesh() to accept options objects directly
403
+ _original_remesh_3d = MmgMesh3D.remesh
404
+ _original_remesh_2d = MmgMesh2D.remesh
405
+ _original_remesh_s = MmgMeshS.remesh
406
+
407
+ # Map mesh types to their expected options types
408
+ _options_type_map: dict[type, type] = {
409
+ MmgMesh3D: Mmg3DOptions,
410
+ MmgMesh2D: Mmg2DOptions,
411
+ MmgMeshS: MmgSOptions,
412
+ }
413
+
414
+ def _make_remesh_wrapper(
415
+ original_remesh: Callable[..., None],
416
+ ) -> Callable[..., None]:
417
+ def _wrapped_remesh(
418
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
419
+ options: Mmg3DOptions | Mmg2DOptions | MmgSOptions | None = None,
420
+ **kwargs: Any, # noqa: ANN401
421
+ ) -> None:
422
+ if options is not None:
423
+ if kwargs:
424
+ msg = (
425
+ "Cannot pass both options object and keyword arguments. "
426
+ "Use one or the other."
427
+ )
428
+ raise TypeError(msg)
429
+ # Validate options type matches mesh type
430
+ expected_type = _options_type_map[type(self)]
431
+ if not isinstance(options, expected_type):
432
+ msg = (
433
+ f"Expected {expected_type.__name__} for {type(self).__name__}, "
434
+ f"got {type(options).__name__}"
435
+ )
436
+ raise TypeError(msg)
437
+ # Options object passed - convert to kwargs
438
+ kwargs = options.to_dict()
439
+ original_remesh(self, **kwargs)
440
+
441
+ return _wrapped_remesh
442
+
443
+ MmgMesh3D.remesh = _make_remesh_wrapper(_original_remesh_3d) # type: ignore[method-assign]
444
+ MmgMesh2D.remesh = _make_remesh_wrapper(_original_remesh_2d) # type: ignore[method-assign]
445
+ MmgMeshS.remesh = _make_remesh_wrapper(_original_remesh_s) # type: ignore[method-assign]
446
+
447
+ def _remesh_optimize(
448
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
449
+ *,
450
+ verbose: int | None = None,
451
+ ) -> None:
452
+ """Optimize mesh quality without changing topology.
453
+
454
+ Only moves vertices to improve element quality.
455
+ No points are inserted or removed.
456
+
457
+ Parameters
458
+ ----------
459
+ verbose : int | None
460
+ Verbosity level (-1=silent, 0=errors, 1=info).
461
+
462
+ """
463
+ opts: dict[str, int] = {"optim": 1, "noinsert": 1}
464
+ if verbose is not None:
465
+ opts["verbose"] = verbose
466
+ self.remesh(**opts) # type: ignore[arg-type]
467
+
468
+ def _remesh_uniform(
469
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
470
+ size: float,
471
+ *,
472
+ verbose: int | None = None,
473
+ ) -> None:
474
+ """Remesh with uniform element size.
475
+
476
+ Parameters
477
+ ----------
478
+ size : float
479
+ Target edge size for all elements.
480
+ verbose : int | None
481
+ Verbosity level (-1=silent, 0=errors, 1=info).
482
+
483
+ """
484
+ opts: dict[str, float | int] = {"hsiz": size}
485
+ if verbose is not None:
486
+ opts["verbose"] = verbose
487
+ self.remesh(**opts) # type: ignore[arg-type]
488
+
489
+ MmgMesh3D.remesh_optimize = _remesh_optimize # type: ignore[attr-defined]
490
+ MmgMesh3D.remesh_uniform = _remesh_uniform # type: ignore[attr-defined]
491
+
492
+ MmgMesh2D.remesh_optimize = _remesh_optimize # type: ignore[attr-defined]
493
+ MmgMesh2D.remesh_uniform = _remesh_uniform # type: ignore[attr-defined]
494
+
495
+ MmgMeshS.remesh_optimize = _remesh_optimize # type: ignore[attr-defined]
496
+ MmgMeshS.remesh_uniform = _remesh_uniform # type: ignore[attr-defined]
497
+
498
+
499
+ _add_convenience_methods()
500
+
501
+
502
+ import weakref as _weakref
503
+
504
+ _sizing_constraints_store: dict[int, list[SizingConstraint]] = {}
505
+ _sizing_mesh_refs: dict[int, "_weakref.ref[MmgMesh3D | MmgMesh2D | MmgMeshS]"] = {}
506
+
507
+
508
+ def _add_sizing_methods() -> None: # noqa: PLR0915
509
+ """Add local sizing methods to mesh classes."""
510
+ from collections.abc import Sequence # noqa: PLC0415
511
+
512
+ import numpy as np # noqa: PLC0415
513
+ from numpy.typing import NDArray # noqa: PLC0415
514
+
515
+ def _get_sizing_constraints(
516
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
517
+ ) -> list[SizingConstraint]:
518
+ """Get or create the sizing constraints list."""
519
+ mesh_id = id(self)
520
+ if mesh_id not in _sizing_constraints_store:
521
+ _sizing_constraints_store[mesh_id] = []
522
+
523
+ # Register weakref callback to clean up when mesh is garbage collected
524
+ def _cleanup_sizing(
525
+ _ref: "_weakref.ref[MmgMesh3D | MmgMesh2D | MmgMeshS]",
526
+ mid: int = mesh_id,
527
+ ) -> None:
528
+ _sizing_constraints_store.pop(mid, None)
529
+ _sizing_mesh_refs.pop(mid, None)
530
+
531
+ _sizing_mesh_refs[mesh_id] = _weakref.ref(self, _cleanup_sizing)
532
+ return _sizing_constraints_store[mesh_id]
533
+
534
+ def _set_size_sphere(
535
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
536
+ center: Sequence[float] | NDArray[np.float64],
537
+ radius: float,
538
+ size: float,
539
+ ) -> None:
540
+ """Set uniform size within a spherical region.
541
+
542
+ Parameters
543
+ ----------
544
+ center : array_like
545
+ Center of the sphere.
546
+ radius : float
547
+ Radius of the sphere. Must be positive.
548
+ size : float
549
+ Target edge size within the sphere. Must be positive.
550
+
551
+ Examples
552
+ --------
553
+ >>> mesh.set_size_sphere(center=[0.5, 0.5, 0.5], radius=0.2, size=0.01)
554
+ >>> mesh.remesh(hmax=0.1, verbose=-1)
555
+
556
+ """
557
+ constraints = _get_sizing_constraints(self)
558
+ constraints.append(
559
+ SphereSize(
560
+ center=np.asarray(center, dtype=np.float64),
561
+ radius=radius,
562
+ size=size,
563
+ ),
564
+ )
565
+
566
+ def _set_size_box(
567
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
568
+ bounds: Sequence[Sequence[float]] | NDArray[np.float64],
569
+ size: float,
570
+ ) -> None:
571
+ """Set uniform size within a box region.
572
+
573
+ Parameters
574
+ ----------
575
+ bounds : array_like
576
+ Box bounds as [[xmin, ymin, zmin], [xmax, ymax, zmax]] for 3D
577
+ or [[xmin, ymin], [xmax, ymax]] for 2D.
578
+ size : float
579
+ Target edge size within the box. Must be positive.
580
+
581
+ Examples
582
+ --------
583
+ >>> mesh.set_size_box(
584
+ ... bounds=[[0, 0, 0], [0.5, 0.5, 0.5]],
585
+ ... size=0.01,
586
+ ... )
587
+ >>> mesh.remesh(hmax=0.1, verbose=-1)
588
+
589
+ """
590
+ constraints = _get_sizing_constraints(self)
591
+ constraints.append(
592
+ BoxSize(
593
+ bounds=np.asarray(bounds, dtype=np.float64),
594
+ size=size,
595
+ ),
596
+ )
597
+
598
+ def _set_size_cylinder(
599
+ self: MmgMesh3D | MmgMeshS,
600
+ point1: Sequence[float] | NDArray[np.float64],
601
+ point2: Sequence[float] | NDArray[np.float64],
602
+ radius: float,
603
+ size: float,
604
+ ) -> None:
605
+ """Set uniform size within a cylindrical region.
606
+
607
+ Parameters
608
+ ----------
609
+ point1 : array_like
610
+ First endpoint of cylinder axis.
611
+ point2 : array_like
612
+ Second endpoint of cylinder axis.
613
+ radius : float
614
+ Radius of the cylinder. Must be positive.
615
+ size : float
616
+ Target edge size within the cylinder. Must be positive.
617
+
618
+ Examples
619
+ --------
620
+ >>> mesh.set_size_cylinder(
621
+ ... point1=[0, 0, 0],
622
+ ... point2=[0, 0, 1],
623
+ ... radius=0.1,
624
+ ... size=0.02,
625
+ ... )
626
+ >>> mesh.remesh(hmax=0.1, verbose=-1)
627
+
628
+ """
629
+ constraints = _get_sizing_constraints(self)
630
+ constraints.append(
631
+ CylinderSize(
632
+ point1=np.asarray(point1, dtype=np.float64),
633
+ point2=np.asarray(point2, dtype=np.float64),
634
+ radius=radius,
635
+ size=size,
636
+ ),
637
+ )
638
+
639
+ def _set_size_from_point(
640
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
641
+ point: Sequence[float] | NDArray[np.float64],
642
+ near_size: float,
643
+ far_size: float,
644
+ influence_radius: float,
645
+ ) -> None:
646
+ """Set distance-based sizing from a point.
647
+
648
+ Size varies linearly from near_size at the point to far_size at
649
+ influence_radius distance.
650
+
651
+ Parameters
652
+ ----------
653
+ point : array_like
654
+ Reference point.
655
+ near_size : float
656
+ Target size at the reference point. Must be positive.
657
+ far_size : float
658
+ Target size at influence_radius distance and beyond. Must be positive.
659
+ influence_radius : float
660
+ Distance over which size transitions. Must be positive.
661
+
662
+ Examples
663
+ --------
664
+ >>> mesh.set_size_from_point(
665
+ ... point=[0.5, 0.5, 0.5],
666
+ ... near_size=0.01,
667
+ ... far_size=0.1,
668
+ ... influence_radius=0.5,
669
+ ... )
670
+ >>> mesh.remesh(verbose=-1)
671
+
672
+ """
673
+ constraints = _get_sizing_constraints(self)
674
+ constraints.append(
675
+ PointSize(
676
+ point=np.asarray(point, dtype=np.float64),
677
+ near_size=near_size,
678
+ far_size=far_size,
679
+ influence_radius=influence_radius,
680
+ ),
681
+ )
682
+
683
+ def _clear_local_sizing(
684
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
685
+ ) -> None:
686
+ """Clear all local sizing constraints.
687
+
688
+ After calling this method, remeshing will use only global
689
+ parameters (hmin, hmax, hsiz) without any local sizing.
690
+
691
+ Examples
692
+ --------
693
+ >>> mesh.set_size_sphere(center=[0.5, 0.5, 0.5], radius=0.2, size=0.01)
694
+ >>> mesh.clear_local_sizing() # Remove all sizing constraints
695
+ >>> mesh.remesh(hmax=0.1) # Uses only global hmax
696
+
697
+ """
698
+ mesh_id = id(self)
699
+ if mesh_id in _sizing_constraints_store:
700
+ _sizing_constraints_store[mesh_id].clear()
701
+
702
+ def _get_local_sizing_count(
703
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
704
+ ) -> int:
705
+ """Get the number of local sizing constraints.
706
+
707
+ Returns
708
+ -------
709
+ int
710
+ Number of sizing constraints currently set.
711
+
712
+ """
713
+ mesh_id = id(self)
714
+ if mesh_id in _sizing_constraints_store:
715
+ return len(_sizing_constraints_store[mesh_id])
716
+ return 0
717
+
718
+ def _apply_local_sizing(
719
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
720
+ ) -> None:
721
+ """Apply local sizing constraints to the mesh metric field.
722
+
723
+ This is called automatically before remeshing if sizing constraints
724
+ are set. You can also call it manually to inspect the resulting
725
+ metric field before remeshing.
726
+
727
+ Multiple sizing constraints are combined by taking the minimum size
728
+ at each vertex (finest mesh wins).
729
+
730
+ """
731
+ import contextlib # noqa: PLC0415
732
+
733
+ constraints = _get_sizing_constraints(self)
734
+ if constraints:
735
+ existing_metric = None
736
+ with contextlib.suppress(RuntimeError, KeyError):
737
+ existing_metric = self["metric"]
738
+ apply_sizing_constraints(self, constraints, existing_metric)
739
+
740
+ # Add methods to all mesh classes
741
+ MmgMesh3D.set_size_sphere = _set_size_sphere # type: ignore[attr-defined]
742
+ MmgMesh3D.set_size_box = _set_size_box # type: ignore[attr-defined]
743
+ MmgMesh3D.set_size_cylinder = _set_size_cylinder # type: ignore[attr-defined]
744
+ MmgMesh3D.set_size_from_point = _set_size_from_point # type: ignore[attr-defined]
745
+ MmgMesh3D.clear_local_sizing = _clear_local_sizing # type: ignore[attr-defined]
746
+ MmgMesh3D.get_local_sizing_count = _get_local_sizing_count # type: ignore[attr-defined]
747
+ MmgMesh3D.apply_local_sizing = _apply_local_sizing # type: ignore[attr-defined]
748
+
749
+ MmgMesh2D.set_size_sphere = _set_size_sphere # type: ignore[attr-defined]
750
+ MmgMesh2D.set_size_box = _set_size_box # type: ignore[attr-defined]
751
+ MmgMesh2D.set_size_from_point = _set_size_from_point # type: ignore[attr-defined]
752
+ MmgMesh2D.clear_local_sizing = _clear_local_sizing # type: ignore[attr-defined]
753
+ MmgMesh2D.get_local_sizing_count = _get_local_sizing_count # type: ignore[attr-defined]
754
+ MmgMesh2D.apply_local_sizing = _apply_local_sizing # type: ignore[attr-defined]
755
+
756
+ MmgMeshS.set_size_sphere = _set_size_sphere # type: ignore[attr-defined]
757
+ MmgMeshS.set_size_box = _set_size_box # type: ignore[attr-defined]
758
+ MmgMeshS.set_size_cylinder = _set_size_cylinder # type: ignore[attr-defined]
759
+ MmgMeshS.set_size_from_point = _set_size_from_point # type: ignore[attr-defined]
760
+ MmgMeshS.clear_local_sizing = _clear_local_sizing # type: ignore[attr-defined]
761
+ MmgMeshS.get_local_sizing_count = _get_local_sizing_count # type: ignore[attr-defined]
762
+ MmgMeshS.apply_local_sizing = _apply_local_sizing # type: ignore[attr-defined]
763
+
764
+
765
+ _add_sizing_methods()
766
+
767
+
768
+ def _wrap_remesh_with_sizing() -> None:
769
+ """Wrap remesh methods to auto-apply sizing constraints."""
770
+ from collections.abc import Callable # noqa: PLC0415
771
+ from typing import Any # noqa: PLC0415
772
+
773
+ _sizing_aware_remesh_3d = MmgMesh3D.remesh
774
+ _sizing_aware_remesh_2d = MmgMesh2D.remesh
775
+ _sizing_aware_remesh_s = MmgMeshS.remesh
776
+
777
+ def _make_sizing_wrapper(
778
+ wrapped_remesh: Callable[..., None],
779
+ ) -> Callable[..., None]:
780
+ def _sizing_remesh(
781
+ self: MmgMesh3D | MmgMesh2D | MmgMeshS,
782
+ *args: Any, # noqa: ANN401
783
+ **kwargs: Any, # noqa: ANN401
784
+ ) -> None:
785
+ # Apply sizing constraints before remeshing
786
+ constraints = _sizing_constraints_store.get(id(self))
787
+ if constraints:
788
+ self.apply_local_sizing() # type: ignore[attr-defined]
789
+ wrapped_remesh(self, *args, **kwargs)
790
+
791
+ return _sizing_remesh
792
+
793
+ MmgMesh3D.remesh = _make_sizing_wrapper(_sizing_aware_remesh_3d) # type: ignore[method-assign]
794
+ MmgMesh2D.remesh = _make_sizing_wrapper(_sizing_aware_remesh_2d) # type: ignore[method-assign]
795
+ MmgMeshS.remesh = _make_sizing_wrapper(_sizing_aware_remesh_s) # type: ignore[method-assign]
796
+
797
+
798
+ _wrap_remesh_with_sizing()
799
+
800
+ __all__ = [
801
+ "MMG_VERSION",
802
+ "BoxSize",
803
+ "CylinderSize",
804
+ "Mmg2DOptions",
805
+ "Mmg3DOptions",
806
+ "MmgMesh2D",
807
+ "MmgMesh3D",
808
+ "MmgMeshS",
809
+ "MmgSOptions",
810
+ "PointSize",
811
+ "ProgressEvent",
812
+ "SizingConstraint",
813
+ "SphereSize",
814
+ "__version__",
815
+ "detect_boundary_vertices",
816
+ "disable_logging",
817
+ "enable_debug",
818
+ "from_pyvista",
819
+ "lagrangian",
820
+ "metrics",
821
+ "mmg2d",
822
+ "mmg3d",
823
+ "mmgs",
824
+ "move_mesh",
825
+ "progress",
826
+ "propagate_displacement",
827
+ "rich_progress",
828
+ "set_log_level",
829
+ "sizing",
830
+ "to_pyvista",
831
+ ]
832
+
833
+
834
+ # Auto-fix RPATH on import if needed (macOS only)
835
+ def _auto_fix_rpath_on_import() -> None:
836
+ """Automatically fix RPATH on import if executables need it."""
837
+ # Skip RPATH fixing on Windows entirely
838
+ if sys.platform == "win32":
839
+ return
840
+
841
+ system = platform.system()
842
+ if system not in ("Darwin", "Linux"):
843
+ return
844
+
845
+ try:
846
+ # Quick check if RPATH fix is needed
847
+ site_packages = Path(site.getsitepackages()[0])
848
+ bin_dir = site_packages / "bin"
849
+
850
+ if not bin_dir.exists():
851
+ return
852
+
853
+ executables = list(bin_dir.glob("mmg*_O3"))
854
+ if not executables:
855
+ return
856
+
857
+ # Check if any executable needs RPATH fix
858
+ needs_fix = False
859
+ if system == "Darwin":
860
+ for exe in executables:
861
+ if not _has_correct_rpath(exe, "@loader_path/../mmgpy/lib"):
862
+ needs_fix = True
863
+ break
864
+ elif system == "Linux":
865
+ # For Linux, check if libraries can be found
866
+
867
+ for exe in executables:
868
+ result = subprocess.run(
869
+ ["ldd", str(exe)], # noqa: S607
870
+ capture_output=True,
871
+ text=True,
872
+ check=False,
873
+ )
874
+ if "not found" in result.stdout:
875
+ needs_fix = True
876
+ break
877
+
878
+ if needs_fix:
879
+ _logger.info("Auto-fixing RPATH for MMG executables...")
880
+ _fix_rpath()
881
+
882
+ except Exception:
883
+ # Don't let RPATH fixing break package import
884
+ pass
885
+
886
+
887
+ # Run RPATH auto-fix on import
888
+ _auto_fix_rpath_on_import()