Skip to content

Commit a964921

Browse files
committed
domain/tetrahedralize: run TetGen via worker subprocess and return UnstructuredGrid
- Save surface and args to temp, invoke dedicated worker (tetgen_worker.py) in a separate Python process. - Show a live spinner with right-aligned elapsed time during tetrahedralization. - Load nodes/elements, fix 1-based indexing if present, and build a PyVista UnstructuredGrid. - Keep triangulate() helper unchanged; move direct TetGen calls out of the main path.
1 parent 93154e7 commit a964921

File tree

1 file changed

+140
-7
lines changed

1 file changed

+140
-7
lines changed

svv/domain/routines/tetrahedralize.py

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
import tetgen
22
import pymeshfix
3+
import subprocess
4+
import tempfile
5+
import os
6+
import sys
7+
from tqdm.auto import tqdm
8+
from itertools import cycle
9+
from time import sleep
10+
import time
11+
import numpy as np
12+
import pyvista as pv
313
from svv.utils.remeshing import remesh
14+
import shutil
15+
import json
416

17+
filepath = os.path.abspath(__file__)
18+
dirpath = os.path.dirname(filepath)
19+
20+
def format_elapsed(seconds: float) -> str:
21+
seconds = int(seconds)
22+
m, s = divmod(seconds, 60)
23+
h, m = divmod(m, 60)
24+
if h > 0:
25+
return f"{h:02d}:{m:02d}:{s:02d}"
26+
else:
27+
return f"{m:02d}:{s:02d}"
528

629
def triangulate(curve, verbose=False, **kwargs):
730
"""
@@ -33,8 +56,16 @@ def triangulate(curve, verbose=False, **kwargs):
3356
vertices = mesh.faces.reshape(-1, 4)[:, 1:]
3457
return mesh, nodes, vertices
3558

59+
def _run_tetgen(surface_mesh):
60+
tgen = tetgen.TetGen(surface_mesh)
61+
nodes, elems = tgen.tetrahedralize(verbose=0)
62+
return nodes, elems
3663

37-
def tetrahedralize(surface_mesh, verbose=False, **kwargs):
64+
def tetrahedralize(surface: pv.PolyData,
65+
*tet_args,
66+
worker_script: str = dirpath+os.sep+"tetgen_worker.py",
67+
python_exe: str = sys.executable,
68+
**tet_kwargs):
3869
"""
3970
Tetrahedralize a surface mesh using TetGen.
4071
@@ -53,9 +84,111 @@ def tetrahedralize(surface_mesh, verbose=False, **kwargs):
5384
An unstructured grid mesh representing the tetrahedralized
5485
volume enclosed by the surface mesh manifold.
5586
"""
56-
mesh = pymeshfix.MeshFix(surface_mesh)
57-
mesh.repair(verbose=verbose)
58-
tet = tetgen.TetGen(mesh.mesh)
59-
nodes, vertices = tet.tetrahedralize(**kwargs)
60-
mesh = tet.grid
61-
return mesh, nodes, vertices
87+
tet_kwargs.setdefault("verbose", 0)
88+
89+
with tempfile.TemporaryDirectory() as tmpdir:
90+
surface_path = os.path.join(tmpdir, "surface.vtp")
91+
out_path = os.path.join(tmpdir, "tet.npz")
92+
config_path = os.path.join(tmpdir, "config.json")
93+
94+
cfg = {
95+
"args": list(tet_args),
96+
"kwargs": tet_kwargs,
97+
}
98+
with open(config_path, "w") as f:
99+
json.dump(cfg, f)
100+
101+
# Save the surface mesh so the worker can read it
102+
surface.save(surface_path)
103+
104+
# Command: call the worker script as a separate Python process
105+
cmd = [python_exe, worker_script, surface_path, out_path, config_path]
106+
107+
# Start the worker process
108+
proc = subprocess.Popen(
109+
cmd,
110+
stdout=subprocess.PIPE,
111+
stderr=subprocess.PIPE,
112+
text=True, # decode to strings
113+
)
114+
115+
spinner = cycle(["⠋", "⠙", "⠹", "⠸", "⠼",
116+
"⠴", "⠦", "⠧", "⠇", "⠏"])
117+
start_time = time.time()
118+
119+
# Print label once
120+
sys.stdout.write("TetGen meshing| ")
121+
sys.stdout.flush()
122+
123+
# Live spinner loop
124+
while proc.poll() is None:
125+
# Compute elapsed time
126+
elapsed = time.time() - start_time
127+
elapsed_str = format_elapsed(elapsed)
128+
129+
# Build left side message
130+
spin_char = next(spinner)
131+
left = f"TetGen meshing| {spin_char}"
132+
133+
# Get terminal width (fallback if IDE doesn't report it)
134+
try:
135+
width = shutil.get_terminal_size(fallback=(80, 20)).columns
136+
except Exception:
137+
width = 80
138+
139+
# Compute spacing so elapsed time is right-aligned
140+
# We'll always keep at least one space between left and right
141+
min_gap = 1
142+
total_len = len(left) + min_gap + len(elapsed_str)
143+
if total_len <= width:
144+
spaces = width - len(left) - len(elapsed_str)
145+
else:
146+
# If line is longer than terminal, don't try to be clever; just put a single space
147+
spaces = min_gap
148+
149+
line = f"{left}{' ' * spaces}{elapsed_str}"
150+
151+
# '\r' to return to the start of the same line and overwrite
152+
sys.stdout.write("\r" + line)
153+
sys.stdout.flush()
154+
155+
time.sleep(0.1)
156+
157+
# Finish line
158+
sys.stdout.write("\n")
159+
sys.stdout.flush()
160+
161+
# Collect output (so the pipes don't hang)
162+
stdout, stderr = proc.communicate()
163+
164+
if proc.returncode != 0:
165+
raise RuntimeError(
166+
f"TetGen worker failed with code {proc.returncode}\n"
167+
f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"
168+
)
169+
170+
# Load results
171+
data = np.load(out_path)
172+
nodes = data["nodes"]
173+
elems = data["elems"]
174+
175+
if elems.min() == 1:
176+
elems = elems - 1
177+
178+
n_cells, n_vertices_per_cell = elems.shape
179+
cells = np.hstack(
180+
[
181+
np.full((n_cells, 1), n_vertices_per_cell, dtype=np.int64),
182+
elems.astype(np.int64),
183+
]
184+
).ravel()
185+
if n_vertices_per_cell == 4:
186+
celltypes = np.full(n_cells, pv.CellType.TETRA, dtype=np.uint8)
187+
elif n_vertices_per_cell == 10:
188+
celltypes = np.full(n_cells, pv.CellType.QUADRATIC_TETRA, dtype=np.uint8)
189+
else:
190+
raise ValueError(f"Unexpected number of vertices per cell: {n_vertices_per_cell}")
191+
192+
grid = pv.UnstructuredGrid(cells, celltypes, nodes)
193+
194+
return grid, nodes, elems

0 commit comments

Comments
 (0)