Skip to content

Commit e112a2c

Browse files
committed
adding sphere refinement
1 parent 3c2befb commit e112a2c

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed

svv/utils/remeshing/remesh.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import pymeshfix
99
import meshio
1010
import numpy
11+
import numpy as np
1112
from copy import deepcopy
1213
import svv
14+
from typing import Sequence, Optional
1315

1416
filepath = os.path.abspath(__file__)
1517
dirpath = os.path.dirname(filepath)
@@ -778,3 +780,257 @@ def clean_medit(filename):
778780
new_lines.extend(lines[o:keywords_index[i+1]])
779781
new_file.writelines(new_lines)
780782
new_file.close()
783+
784+
def write_medit_sol(mesh: pv.PolyData, path: str, array_name="MeshSizingFunction",
785+
scale=1, default_size=None):
786+
npts = mesh.n_points
787+
vals = None
788+
if array_name in mesh.point_data:
789+
vals = np.asarray(mesh.point_data[array_name], dtype=float).reshape(-1)
790+
if vals.size != npts:
791+
raise RuntimeError(f"Array '{array_name}' length ({vals.size}) "
792+
f"!= number of points ({npts})")
793+
# Replace non-positive entries if default provided
794+
if default_size is not None:
795+
vals = np.where(vals > 0.0, vals, float(default_size))
796+
else:
797+
if default_size is None:
798+
raise RuntimeError(f"Point-data array '{array_name}' not found and no default_size provided.")
799+
vals = np.full(npts, float(default_size), dtype=float)
800+
801+
vals = scale * vals # SV typically scales by ~0.8 before MMG
802+
803+
with open(path, "w") as f:
804+
f.write("MeshVersionFormatted 2\n")
805+
f.write("Dimension 3\n\n")
806+
f.write("SolAtVertices\n")
807+
f.write(f"{npts}\n")
808+
f.write("1 1\n") # one scalar per vertex
809+
for v in vals:
810+
f.write(f"{v:.15g}\n")
811+
f.write("\nEnd\n")
812+
813+
def sphere_refinement(
814+
mesh: pv.PolyData,
815+
radius: float,
816+
center: Sequence[float],
817+
local_edge_size: float,
818+
global_edge_size: float,
819+
array_name: str = "MeshSizingFunction",
820+
refine_id_name: Optional[str] = None,
821+
refine_id_value: int = 1,
822+
inplace: bool = True,
823+
ar=None,
824+
hausd=None,
825+
hgrad=None,
826+
verbosity=1,
827+
hmax=None,
828+
hmin=None,
829+
hsiz=None,
830+
noinsert=None,
831+
nomove=None,
832+
nosurf=None,
833+
noswap=None,
834+
nr=None,
835+
optim=False,
836+
rn=None,
837+
required_triangles=None
838+
) -> pv.PolyData:
839+
"""
840+
Set local mesh edge size for points inside a sphere.
841+
842+
Args:
843+
844+
mesh: pyvista.PolyData surface mesh (triangulated or not).
845+
846+
radius: Sphere radius (> 0).
847+
848+
center: Sphere center [cx, cy, cz].
849+
850+
local_edge_size: Target edge size to assign inside the sphere (> 0).
851+
852+
array_name: Point-data array name to write (default: 'MeshSizingFunction').
853+
854+
global_edge_size: If provided and the array is missing, initialize all points
855+
to this value. If not provided and the array is missing, initialize with zeros.
856+
Points outside the sphere are left unchanged.
857+
858+
refine_id_name: Optional point-data int array to tag refined points
859+
(e.g., 'RefineID'). If provided, sets tag = refine_id_value inside the sphere,
860+
leaves others as-is (initializes to 0 if array missing).
861+
862+
refine_id_value: Tag value to set in refine_id_name for points in the sphere.
863+
864+
inplace: If False, process a deep copy and return it.
865+
866+
ar : float, optional
867+
Anisotropy ratio. See MMG3D documentation for details.
868+
869+
hausd : float, optional
870+
Control on Hausdorff distance. See MMG3D documentation.
871+
872+
hgrad : float, optional
873+
Gradation parameter. See MMG3D documentation.
874+
875+
verbosity : int, optional
876+
Verbosity level for MMG output. Default is 1.
877+
878+
hmax : float, optional
879+
Maximum edge size. See MMG3D documentation.
880+
881+
hmin : float, optional
882+
Minimum edge size. See MMG3D documentation.
883+
884+
hsiz : float, optional
885+
Size parameter for remeshing. See MMG3D documentation.
886+
887+
noinsert : bool, optional
888+
If True, prohibits node insertion. See MMG3D documentation.
889+
890+
nomove : bool, optional
891+
If True, prohibits node movement. See MMG3D documentation.
892+
893+
nosurf : bool, optional
894+
If True, prohibits surface modifications. See MMG3D documentation.
895+
896+
noswap : bool, optional
897+
If True, prohibits edge swapping. See MMG3D documentation.
898+
899+
nr : bool, optional
900+
Disables reorientation of the mesh. See MMG3D documentation.
901+
902+
optim : bool, optional
903+
Optimization parameter. See MMG3D documentation.
904+
905+
rn : bool, optional
906+
Removes nonmanifold elements. See MMG3D documentation.
907+
Returns:
908+
pv.PolyData: The updated mesh (same object if inplace=True).
909+
"""
910+
if not isinstance(mesh, pv.PolyData):
911+
raise TypeError("mesh must be a pyvista.PolyData")
912+
if radius <= 0:
913+
raise ValueError("radius must be > 0")
914+
if local_edge_size <= 0:
915+
raise ValueError("local_edge_size must be > 0")
916+
if global_edge_size <= 0:
917+
raise ValueError("global_edge_size must be > 0")
918+
919+
out = mesh if inplace else mesh.copy(deep=True)
920+
921+
pts = out.points.astype(float)
922+
ctr = np.asarray(center, dtype=float).reshape(3)
923+
if ctr.shape != (3,):
924+
raise ValueError("center must be a sequence of three floats")
925+
926+
# Compute mask of points inside the sphere (vectorized).
927+
d2 = np.einsum("ij,ij->i", pts - ctr, pts - ctr) # squared distance
928+
mask = d2 <= float(radius) ** 2
929+
930+
# Prepare or fetch the sizing array.
931+
n = pts.shape[0]
932+
if array_name in out.point_data:
933+
sizes = np.asarray(out.point_data[array_name], dtype=float).copy()
934+
if sizes.shape[0] != n:
935+
raise RuntimeError(f"Existing array '{array_name}' length {sizes.shape[0]} != n_points {n}")
936+
else:
937+
if global_edge_size is None:
938+
sizes = np.zeros(n, dtype=float)
939+
else:
940+
sizes = np.full(n, float(global_edge_size), dtype=float)
941+
942+
# Apply refinement.
943+
sizes[mask] = float(local_edge_size)
944+
out.point_data[array_name] = sizes
945+
946+
# Optional: tag refined points (like SimVascular's RefineID).
947+
if refine_id_name:
948+
if refine_id_name in out.point_data:
949+
rid = np.asarray(out.point_data[refine_id_name], dtype=np.int32).copy()
950+
if rid.shape[0] != n:
951+
raise RuntimeError(f"Existing array '{refine_id_name}' length {rid.shape[0]} != n_points {n}")
952+
else:
953+
rid = np.zeros(n, dtype=np.int32)
954+
rid[mask] = int(refine_id_value)
955+
out.point_data[refine_id_name] = rid
956+
write_medit_sol(out, "in.sol", array_name = "MeshSizingFunction",scale = 1, default_size = global_edge_size)
957+
pv.save_meshio("tmp.mesh", out)
958+
if not isinstance(required_triangles, type(None)):
959+
add_required("tmp.mesh", required_triangles)
960+
if platform.system() == 'Windows':
961+
if os.path.exists(modulepath + os.sep + "mmgs_O3.exe"):
962+
_EXE_ = modulepath + os.sep + "mmgs_O3.exe"
963+
else:
964+
_EXE_ = dirpath+os.sep+"Windows"+os.sep+"mmgs_O3.exe"
965+
elif platform.system() == "Linux":
966+
if os.path.exists(modulepath + os.sep + "mmgs_O3"):
967+
_EXE_ = modulepath + os.sep + "mmgs_O3"
968+
else:
969+
_EXE_ = dirpath+os.sep+"Linux"+os.sep+"mmgs_O3"
970+
elif platform.system() == "Darwin":
971+
if os.path.exists(modulepath + os.sep + "mmgs_O3"):
972+
_EXE_ = modulepath + os.sep + "mmgs_O3"
973+
else:
974+
_EXE_ = dirpath+os.sep+"Mac"+os.sep+"mmgs_O3"
975+
else:
976+
raise NotImplementedError("Operating system not supported.")
977+
devnull = open(os.devnull, 'w')
978+
executable_list = [_EXE_, "tmp.mesh", "-sol", "in.sol"]
979+
if ar is not None:
980+
executable_list.extend(["-ar", str(ar)])
981+
if hausd is not None:
982+
executable_list.extend(["-hausd", str(hausd)])
983+
if hgrad is not None:
984+
executable_list.extend(["-hgrad", str(hgrad)])
985+
if verbosity is not None:
986+
executable_list.extend(["-v", str(verbosity)])
987+
if hmax is not None:
988+
executable_list.extend(["-hmax", str(hmax)])
989+
if hmin is not None:
990+
executable_list.extend(["-hmin", str(hmin)])
991+
if hsiz is not None:
992+
executable_list.extend(["-hsiz", str(hsiz)])
993+
if noinsert is not None:
994+
executable_list.extend(["-noinsert"])
995+
if nomove is not None:
996+
executable_list.extend(["-nomove"])
997+
if nosurf is not None:
998+
executable_list.extend(["-nosurf"])
999+
if noswap is not None:
1000+
executable_list.extend(["-noswap"])
1001+
if nr is not None:
1002+
executable_list.extend(["-nr"])
1003+
if optim:
1004+
executable_list.extend(["-optim"])
1005+
if rn is not None:
1006+
executable_list.extend(["-rn", str(rn)])
1007+
if verbosity == 0:
1008+
try:
1009+
subprocess.check_call(executable_list, stdout=devnull, stderr=devnull)
1010+
except:
1011+
os.chmod(_EXE_, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1012+
subprocess.check_call(executable_list, stdout=devnull, stderr=devnull)
1013+
else:
1014+
try:
1015+
subprocess.check_call(executable_list)
1016+
except:
1017+
os.chmod(_EXE_, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1018+
subprocess.check_call(executable_list)
1019+
clean_medit("tmp.o.mesh")
1020+
remesh_data = meshio.read("tmp.o.mesh")
1021+
vertices = remesh_data.points
1022+
has_triangles = False
1023+
for cell_block in remesh_data.cells:
1024+
if cell_block.type == "triangle":
1025+
faces = cell_block.data
1026+
has_triangles = True
1027+
break
1028+
if not has_triangles:
1029+
raise NotImplementedError("Only triangular surfaces are supported.")
1030+
faces = numpy.hstack([numpy.full((faces.shape[0], 1), 3), faces])
1031+
remeshed_surface = pv.PolyData(vertices, faces.flatten())
1032+
os.remove("tmp.mesh")
1033+
os.remove("tmp.o.sol")
1034+
os.remove("tmp.o.mesh")
1035+
os.remove("in.sol")
1036+
return remeshed_surface

0 commit comments

Comments
 (0)