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.
- bin/Release/mmg2d_O3.exe +0 -0
- bin/Release/mmg3d_O3.exe +0 -0
- bin/Release/mmgs_O3.exe +0 -0
- bin/__init__.py +10 -0
- bin/concrt140.dll +0 -0
- bin/mmg.dll +0 -0
- bin/mmg2d.dll +0 -0
- bin/mmg2d_O3.exe +0 -0
- bin/mmg3d.dll +0 -0
- bin/mmg3d_O3.exe +0 -0
- bin/mmgs.dll +0 -0
- bin/mmgs_O3.exe +0 -0
- bin/msvcp140.dll +0 -0
- bin/msvcp140_1.dll +0 -0
- bin/msvcp140_2.dll +0 -0
- bin/msvcp140_atomic_wait.dll +0 -0
- bin/msvcp140_codecvt_ids.dll +0 -0
- bin/vcruntime140.dll +0 -0
- bin/vcruntime140_1.dll +0 -0
- include/__init__.py +10 -0
- include/mmg/common/libmmgtypes.h +687 -0
- include/mmg/common/libmmgtypesf.h +762 -0
- include/mmg/common/mmg_export.h +47 -0
- include/mmg/common/mmgcmakedefines.h +46 -0
- include/mmg/common/mmgcmakedefinesf.h +29 -0
- include/mmg/common/mmgversion.h +54 -0
- include/mmg/libmmg.h +67 -0
- include/mmg/libmmgf.h +42 -0
- include/mmg/mmg2d/libmmg2d.h +2761 -0
- include/mmg/mmg2d/libmmg2df.h +3263 -0
- include/mmg/mmg2d/mmg2d_export.h +34 -0
- include/mmg/mmg3d/libmmg3d.h +3444 -0
- include/mmg/mmg3d/libmmg3df.h +4041 -0
- include/mmg/mmg3d/mmg3d_export.h +34 -0
- include/mmg/mmgs/libmmgs.h +2560 -0
- include/mmg/mmgs/libmmgsf.h +3028 -0
- include/mmg/mmgs/mmgs_export.h +34 -0
- lib/__init__.py +10 -0
- lib/cmake/mmg/FindElas.cmake +57 -0
- lib/cmake/mmg/FindSCOTCH.cmake +373 -0
- lib/cmake/mmg/MmgTargets-release.cmake +53 -0
- lib/cmake/mmg/MmgTargets.cmake +127 -0
- lib/cmake/mmg/mmgConfig.cmake +43 -0
- lib/mmg.lib +0 -0
- lib/mmg2d.lib +0 -0
- lib/mmg3d.lib +0 -0
- lib/mmgs.lib +0 -0
- mmgpy/__init__.py +888 -0
- mmgpy/_logging.py +86 -0
- mmgpy/_mmgpy.cp312-win_amd64.pyd +0 -0
- mmgpy/_mmgpy.pyi +650 -0
- mmgpy/_options.py +304 -0
- mmgpy/_progress.py +539 -0
- mmgpy/_pyvista.py +423 -0
- mmgpy/_version.py +3 -0
- mmgpy/_version.py.in +3 -0
- mmgpy/lagrangian.py +394 -0
- mmgpy/metrics.py +595 -0
- mmgpy/mmg2d.dll +0 -0
- mmgpy/mmg2d.lib +0 -0
- mmgpy/mmg3d.dll +0 -0
- mmgpy/mmg3d.lib +0 -0
- mmgpy/mmgs.dll +0 -0
- mmgpy/mmgs.lib +0 -0
- mmgpy/progress.py +57 -0
- mmgpy/py.typed +0 -0
- mmgpy/sizing.py +370 -0
- mmgpy-0.3.0.dist-info/DELVEWHEEL +2 -0
- mmgpy-0.3.0.dist-info/METADATA +75 -0
- mmgpy-0.3.0.dist-info/RECORD +132 -0
- mmgpy-0.3.0.dist-info/WHEEL +5 -0
- mmgpy-0.3.0.dist-info/entry_points.txt +6 -0
- mmgpy-0.3.0.dist-info/licenses/LICENSE +38 -0
- mmgpy.libs/vtkCommonColor-9.5-07cd19e9d77559cb8be83e8ac8833cd4.dll +0 -0
- mmgpy.libs/vtkCommonComputationalGeometry-9.5-4aaf997b087c330e171c14a4ba6be7b2.dll +0 -0
- mmgpy.libs/vtkCommonCore-9.5.dll +0 -0
- mmgpy.libs/vtkCommonDataModel-9.5.dll +0 -0
- mmgpy.libs/vtkCommonExecutionModel-9.5-2f7a1bae0a1d4d0e205eea43596a659c.dll +0 -0
- mmgpy.libs/vtkCommonMath-9.5-609b01246386fe29df2677fa5c7ca793.dll +0 -0
- mmgpy.libs/vtkCommonMisc-9.5-4173df33811eddea1529a40bf93266c8.dll +0 -0
- mmgpy.libs/vtkCommonSystem-9.5-e5b15bd84934b99e3b2bbe5d3e064c97.dll +0 -0
- mmgpy.libs/vtkCommonTransforms-9.5-9b76a61640718d893271cc0b5db50d1d.dll +0 -0
- mmgpy.libs/vtkDICOMParser-9.5-203c95a77d21799a8049a576e1b28f2e.dll +0 -0
- mmgpy.libs/vtkFiltersCellGrid-9.5-fa6bda61d2d528369d8b2f3a66d2d6b4.dll +0 -0
- mmgpy.libs/vtkFiltersCore-9.5-935a5f5225a975e99626296b2f3ded70.dll +0 -0
- mmgpy.libs/vtkFiltersExtraction-9.5-dc0a7543ba584f7e8ce9f9184485a228.dll +0 -0
- mmgpy.libs/vtkFiltersGeneral-9.5-709f69dbcca8aba1750582106a97c605.dll +0 -0
- mmgpy.libs/vtkFiltersGeometry-9.5-7abfb655763a62f56d63b45038d6e811.dll +0 -0
- mmgpy.libs/vtkFiltersHybrid-9.5-0721ec98d8a8b7442d900747e1ec59fb.dll +0 -0
- mmgpy.libs/vtkFiltersHyperTree-9.5-f9ee6a4761fdad8956c08a51dae77636.dll +0 -0
- mmgpy.libs/vtkFiltersModeling-9.5-458d9d2c544bb3c37de28c26c05c07bc.dll +0 -0
- mmgpy.libs/vtkFiltersParallel-9.5-1f243ffe308277c3970d8be4172d856f.dll +0 -0
- mmgpy.libs/vtkFiltersReduction-9.5-bf8c4a248bd84fbd6bb1a2ab5f646e56.dll +0 -0
- mmgpy.libs/vtkFiltersSources-9.5-492fa5b1b8562f4b141a347a38ae1ce5.dll +0 -0
- mmgpy.libs/vtkFiltersStatistics-9.5-6e99ef76387303ec5ff8c0fe6101d446.dll +0 -0
- mmgpy.libs/vtkFiltersTexture-9.5-15c23120b41b9a1c4acb01f790aad01f.dll +0 -0
- mmgpy.libs/vtkFiltersVerdict-9.5-332c0402a58129ec5b6af7b7f56cbb62.dll +0 -0
- mmgpy.libs/vtkIOCellGrid-9.5-88e1ec9c5a3554a82aedc0027fe84c6b.dll +0 -0
- mmgpy.libs/vtkIOCore-9.5.dll +0 -0
- mmgpy.libs/vtkIOGeometry-9.5-47c69db15c63c5773efa6851b59ae0a7.dll +0 -0
- mmgpy.libs/vtkIOImage-9.5-74bb92e688da5595ff9ff7645f9a0a13.dll +0 -0
- mmgpy.libs/vtkIOLegacy-9.5.dll +0 -0
- mmgpy.libs/vtkIOParallel-9.5.dll +0 -0
- mmgpy.libs/vtkIOParallelXML-9.5.dll +0 -0
- mmgpy.libs/vtkIOXML-9.5.dll +0 -0
- mmgpy.libs/vtkIOXMLParser-9.5-1893156c41fd4cf7165904675cb5d15d.dll +0 -0
- mmgpy.libs/vtkImagingCore-9.5-145fc0249cffbd27c610d10812e1cbfc.dll +0 -0
- mmgpy.libs/vtkImagingSources-9.5-f0c087a4669caa045584ed61f52502b7.dll +0 -0
- mmgpy.libs/vtkParallelCore-9.5-e91757b6dbd2a5369ab2bd05ff95d79d.dll +0 -0
- mmgpy.libs/vtkParallelDIY-9.5-04dd6b6b5dd8a5eacd43d270999cf09a.dll +0 -0
- mmgpy.libs/vtkRenderingCore-9.5-24a9802d77a083def26449fa681b1af7.dll +0 -0
- mmgpy.libs/vtkdoubleconversion-9.5-5e39712b9f4e44ea8a26e9119e53a7d4.dll +0 -0
- mmgpy.libs/vtkexpat-9.5-3b1dd25e09a2cccbbac723de448cb894.dll +0 -0
- mmgpy.libs/vtkfmt-9.5-50239b66bf315d100ecd306114139e9b.dll +0 -0
- mmgpy.libs/vtkjpeg-9.5-9412ee79f685a9196398b988a59666cd.dll +0 -0
- mmgpy.libs/vtkjsoncpp-9.5-abfad956527e3a4885dbb39f99f9e4d4.dll +0 -0
- mmgpy.libs/vtkkissfft-9.5-464db9175ce63de19addc69be524c4b7.dll +0 -0
- mmgpy.libs/vtkloguru-9.5-ec016ed005b4a79062e329ad8f1c382d.dll +0 -0
- mmgpy.libs/vtklz4-9.5-798b58f4518733b0eee8027eeba022fb.dll +0 -0
- mmgpy.libs/vtklzma-9.5-8f489b5430eb47d578de52c769a4dd5c.dll +0 -0
- mmgpy.libs/vtkmetaio-9.5-8f0a559399d53e4c7fc06272620b2167.dll +0 -0
- mmgpy.libs/vtkpng-9.5-1dbed3116ba7e31f56512a93b942cdf5.dll +0 -0
- mmgpy.libs/vtkpugixml-9.5-23ef37d65494ab52babc6d45b24764b7.dll +0 -0
- mmgpy.libs/vtksys-9.5.dll +0 -0
- mmgpy.libs/vtktiff-9.5-767fd93c8402517d5b2d1befab98c41e.dll +0 -0
- mmgpy.libs/vtktoken-9.5-f4ff567202eeb9a613c0b242aa05dbc9.dll +0 -0
- mmgpy.libs/vtkverdict-9.5-6bb84649d1b0ca1cc5454307fd35083b.dll +0 -0
- mmgpy.libs/vtkzlib-9.5-779937c44671e188e9f96125eb5afb12.dll +0 -0
- share/__init__.py +10 -0
- share/man/man1/mmg2d.1.gz +0 -0
- share/man/man1/mmg3d.1.gz +0 -0
- 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()
|