Skip to content

Commit 78cedea

Browse files
committed
[feat] meshrefine is partially working
1 parent 30a8f7c commit 78cedea

File tree

6 files changed

+557
-88
lines changed

6 files changed

+557
-88
lines changed

iso2mesh/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
barydualmesh,
5959
meshcentroid,
6060
highordertet,
61+
ismember_rows,
6162
)
6263

6364
from .utils import (
@@ -104,6 +105,7 @@
104105
surfvolume,
105106
insurface,
106107
remeshsurf,
108+
meshrefine,
107109
)
108110

109111
from .modify import (
@@ -124,7 +126,6 @@
124126
cart2sph,
125127
sortrows,
126128
mergemesh,
127-
meshrefine,
128129
mergesurf,
129130
surfboolean,
130131
meshresample,
@@ -283,5 +284,6 @@
283284
"proj2mesh",
284285
"dist2surf",
285286
"regpt2surf",
287+
"ismember_rows",
286288
]
287289
__license__ = """GNU General Public License v3 and later"""

iso2mesh/core.py

Lines changed: 312 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"surfvolume",
2424
"insurface",
2525
"remeshsurf",
26+
"meshrefine",
2627
]
2728

2829
##====================================================================================
@@ -43,16 +44,31 @@
4344
finddisconnsurf,
4445
maxsurf,
4546
volface,
47+
surfseeds,
48+
maxsurf,
49+
ismember_rows,
4650
)
4751
from iso2mesh.utils import *
48-
from iso2mesh.io import saveoff, readoff, saveinr, readtetgen, savesurfpoly, readmedit
52+
from iso2mesh.io import (
53+
saveoff,
54+
readoff,
55+
saveinr,
56+
readtetgen,
57+
savesurfpoly,
58+
readmedit,
59+
savetetgennode,
60+
savetetgenele,
61+
readgts,
62+
savegts,
63+
)
4964
from iso2mesh.modify import (
5065
meshcheckrepair,
5166
sortmesh,
5267
meshresample,
5368
removeisolatedsurf,
5469
removeisolatednode,
5570
qmeshcut,
71+
removedupelem,
5672
)
5773

5874
##====================================================================================
@@ -1220,3 +1236,298 @@ def remeshsurf(node, face, opt):
12201236
newno[:, 2] += p0[2]
12211237

12221238
return newno, newfc
1239+
1240+
1241+
def meshrefine(node, elem, *args):
1242+
"""
1243+
meshrefine - refine a tetrahedral mesh by adding new nodes or constraints
1244+
1245+
Usage:
1246+
newnode, newelem, newface = meshrefine(node, elem, face=None, opt=None)
1247+
1248+
Description:
1249+
This function refines a tetrahedral mesh by inserting new nodes or applying
1250+
sizing constraints.
1251+
1252+
Inputs:
1253+
node: Nx3 or Nx4 array of mesh nodes. If Nx4, the 4th column is used as a
1254+
size field.
1255+
elem: Mx4 or Mx5 array of tetrahedral elements (1-based indexing).
1256+
face (optional): Px3 array of triangle faces (1-based indexing).
1257+
opt: options for mesh refinement. Can be one of:
1258+
- Nx3 array of new nodes to be inserted (must be inside or on the mesh)
1259+
- Vector of desired edge-lengths (length = len(node)) or max volumes (len = len(elem))
1260+
- Dictionary with fields:
1261+
- newnode: same as above Nx3 array
1262+
- reratio: radius-edge ratio (default 1.414)
1263+
- maxvol: max tetrahedron volume
1264+
- sizefield: node-based or element-based sizing
1265+
- extcmdopt: tetgen options for inserting external nodes
1266+
- extlabel: label for new external elements (default: 0)
1267+
- extcorelabel: label for core of external mesh (default: -1)
1268+
1269+
Outputs:
1270+
newnode: refined node list
1271+
newelem: refined element list
1272+
newface: surface faces (Px3), last column denotes boundary ID
1273+
1274+
Examples:
1275+
# Inserting internal nodes
1276+
innernodes = np.array([[1,1,1], [2,2,2], [3,3,3]])
1277+
newnode, newelem, _ = meshrefine(node, elem, innernodes)
1278+
1279+
# Inserting external nodes
1280+
extnodes = np.array([[-5,-5,25], [-5,5,25], [5,5,25], [5,-5,25]])
1281+
opt = {'newnode': extnodes, 'extcmdopt': '-Y'}
1282+
newnode, newelem, _ = meshrefine(node, elem, opt)
1283+
1284+
Author:
1285+
Qianqian Fang <[email protected]>
1286+
1287+
License:
1288+
Part of Iso2Mesh Toolbox (http://iso2mesh.sf.net)
1289+
"""
1290+
newpt = None
1291+
sizefield = None
1292+
face = None
1293+
opt = {}
1294+
1295+
if node.shape[1] == 4:
1296+
sizefield = node[:, 3]
1297+
node = node[:, :3]
1298+
1299+
if len(args) == 1:
1300+
if isinstance(args[0], dict):
1301+
opt = args[0]
1302+
elif len(args[0]) == node.shape[0] or len(args[0]) == elem.shape[0]:
1303+
sizefield = np.array(args[0])
1304+
else:
1305+
newpt = np.array(args[0])
1306+
elif len(args) >= 2:
1307+
face = np.array(args[0])
1308+
if isinstance(args[1], dict):
1309+
opt = args[1]
1310+
elif len(args[1]) == node.shape[0] or len(args[1]) == elem.shape[0]:
1311+
sizefield = np.array(args[1])
1312+
else:
1313+
newpt = np.array(args[1])
1314+
else:
1315+
raise ValueError("meshrefine requires at least 3 inputs")
1316+
1317+
if isinstance(opt, dict) and "newnode" in opt:
1318+
newpt = np.array(opt["newnode"])
1319+
if isinstance(opt, dict) and "sizefield" in opt:
1320+
sizefield = np.array(opt["sizefield"])
1321+
1322+
exesuff = fallbackexeext(getexeext(), "tetgen")
1323+
1324+
deletemeshfile(mwpath("pre_refine.*"))
1325+
deletemeshfile(mwpath("post_refine.*"))
1326+
1327+
moreopt = ""
1328+
setquality = False
1329+
1330+
if isinstance(opt, dict) and "reratio" in opt:
1331+
moreopt += f" -q {opt['reratio']:.10f} "
1332+
setquality = True
1333+
if isinstance(opt, dict) and "maxvol" in opt:
1334+
moreopt += f" -a{opt['maxvol']:.10f} "
1335+
1336+
externalpt = np.empty((0, 3))
1337+
if isinstance(opt, dict) and "extcmdopt" in opt and newpt is not None:
1338+
from scipy.spatial import Delaunay
1339+
1340+
try:
1341+
tri = Delaunay(node[:, :3])
1342+
isinside = tri.find_simplex(newpt[:, :3]) >= 0
1343+
except Exception:
1344+
isinside = np.zeros(newpt.shape[0], dtype=bool)
1345+
externalpt = newpt[~isinside]
1346+
newpt = newpt[isinside]
1347+
1348+
if newpt is not None and len(newpt) > 0 and newpt.shape[0] < 4:
1349+
newpt = np.vstack([newpt, np.tile(newpt[0], (4 - newpt.shape[0], 1))])
1350+
1351+
if newpt is not None and len(newpt) > 0:
1352+
savetetgennode(newpt, mwpath("pre_refine.1.a.node"))
1353+
moreopt = " -i "
1354+
1355+
if sizefield is not None:
1356+
if len(sizefield) == node.shape[0]:
1357+
with open(mwpath("pre_refine.1.mtr"), "w") as f:
1358+
f.write(f"{len(sizefield)} 1\n")
1359+
np.savetxt(f, sizefield, fmt="%.16g")
1360+
moreopt += " -qma "
1361+
else:
1362+
with open(mwpath("pre_refine.1.vol"), "w") as f:
1363+
f.write(f"{len(sizefield)}\n")
1364+
rownum = np.arange(1, sizefield.size + 1).reshape(-1, 1)
1365+
np.savetxt(
1366+
f, np.hstack((rownum, sizefield.reshape(-1, 1)), fmt="%d %.16g")
1367+
)
1368+
moreopt += " -qma "
1369+
1370+
if elem.shape[1] == 3 and not setquality:
1371+
if newpt is not None:
1372+
raise ValueError("Inserting new point cannot be used for surfaces")
1373+
nedge = savegts(node, elem, mwpath("pre_refine.gts"))
1374+
exesuff = fallbackexeext(getexeext(), "gtsrefine")
1375+
elif elem.shape[1] == 3:
1376+
savesurfpoly(node, elem, None, None, None, None, mwpath("pre_refine.poly"))
1377+
else:
1378+
savetetgennode(node, mwpath("pre_refine.1.node"))
1379+
savetetgenele(elem, mwpath("pre_refine.1.ele"))
1380+
1381+
print("refining the input mesh ...")
1382+
1383+
if elem.shape[1] == 3 and not setquality:
1384+
if isinstance(opt, dict) and "scale" in opt:
1385+
moreopt += f" -n {int(round(nedge * opt['scale']))} "
1386+
else:
1387+
raise ValueError("You must give opt.scale value for refining a surface")
1388+
1389+
if isinstance(opt, dict) and "moreopt" in opt:
1390+
moreopt += opt["moreopt"]
1391+
1392+
if elem.shape[1] == 3 and not setquality:
1393+
status = os.system(
1394+
f'"{mcpath("gtsrefine", exesuff)}" {moreopt} < "{mwpath("pre_refine.gts")}" > "{mwpath("post_refine.gts")}"'
1395+
)
1396+
if status:
1397+
raise RuntimeError("gtsrefine command failed")
1398+
newnode, newelem = readgts(mwpath("post_refine.gts"))
1399+
newface = newelem
1400+
elif elem.shape[1] == 3:
1401+
status = os.system(
1402+
f'"{mcpath("tetgen1.5", exesuff)}" {moreopt} -p -A "{mwpath("pre_refine.poly")}"'
1403+
)
1404+
if status:
1405+
raise RuntimeError("tetgen command failed")
1406+
newnode, newelem, newface = readtetgen(mwpath("pre_refine.1"))
1407+
elif moreopt:
1408+
status = os.system(
1409+
f'"{mcpath("tetgen", exesuff)}" {moreopt} -r "{mwpath("pre_refine.1")}"'
1410+
)
1411+
if status:
1412+
raise RuntimeError("tetgen command failed")
1413+
newnode, newelem, newface = readtetgen(mwpath("pre_refine.2"))
1414+
else:
1415+
newnode = node
1416+
newelem = elem
1417+
newface = face
1418+
1419+
if (
1420+
externalpt.size > 0
1421+
): # user request to insert nodes that are outside of the original mesh
1422+
from scipy.spatial import ConvexHull, Delaunay
1423+
1424+
# create a mesh including the external points
1425+
externalpt = np.unique(externalpt, axis=0)
1426+
allnode = np.vstack([newnode, externalpt])
1427+
1428+
# define the convex hull as the external surface
1429+
outface = ConvexHull(allnode, qhull_options="QJ").simplices + 1
1430+
outface = np.sort(outface, axis=1)
1431+
1432+
face = volface(newelem[:, :4])[0] # adjust for 1-based indexing
1433+
inface = np.sort(face[:, :3], axis=1)
1434+
1435+
# define the surface that bounds the newly extended convex hull space
1436+
bothsides = removedupelem(np.vstack([outface, inface]))
1437+
1438+
# define a seed point to avoid meshing the interior space
1439+
holelist = surfseeds(newnode, face[:, :3])
1440+
1441+
# mesh the extended space
1442+
ISO2MESH_TETGENOPT = jsonopt("extcmdopt", "-Y", opt)
1443+
try:
1444+
if bothsides.shape[0] >= inface.shape[0]:
1445+
no, el, _ = surf2mesh(
1446+
allnode,
1447+
bothsides,
1448+
None,
1449+
None,
1450+
1,
1451+
10,
1452+
None,
1453+
holelist,
1454+
0,
1455+
"tetgen",
1456+
ISO2MESH_TETGENOPT,
1457+
)
1458+
else:
1459+
no, el, _ = surf2mesh(
1460+
allnode,
1461+
bothsides,
1462+
None,
1463+
None,
1464+
1,
1465+
10,
1466+
None,
1467+
None,
1468+
0,
1469+
"tetgen",
1470+
ISO2MESH_TETGENOPT,
1471+
)
1472+
except:
1473+
bothsides = maxsurf(finddisconnsurf(bothsides), allnode)[0]
1474+
if bothsides.shape[0] >= inface.shape[0]:
1475+
no, el, _ = surf2mesh(
1476+
allnode,
1477+
bothsides,
1478+
None,
1479+
None,
1480+
1,
1481+
10,
1482+
None,
1483+
holelist,
1484+
0,
1485+
"tetgen",
1486+
ISO2MESH_TETGENOPT,
1487+
)
1488+
else:
1489+
no, el, _ = surf2mesh(
1490+
allnode,
1491+
bothsides,
1492+
None,
1493+
None,
1494+
1,
1495+
10,
1496+
None,
1497+
None,
1498+
0,
1499+
"tetgen",
1500+
ISO2MESH_TETGENOPT,
1501+
)
1502+
1503+
# map the new node coordinates back to the original node list
1504+
rounded_no = np.round(no, 10)
1505+
rounded_allnode = np.round(allnode, 10)
1506+
_, map = ismember_rows(rounded_no, rounded_allnode)
1507+
snid = np.arange(len(newnode) + 1, len(allnode) + 1) # 0-based indexing
1508+
1509+
# add unmapped nodes
1510+
if np.any(map == 0):
1511+
missing = no[map == 0]
1512+
oldsize = allnode.shape[0]
1513+
allnode = np.vstack([allnode, missing])
1514+
map[map == 0] = np.arange(oldsize, allnode.shape[0]) + 1
1515+
1516+
# merge the external space with the original mesh
1517+
el2 = map[el[:, :4] - 1] + 1
1518+
1519+
# label all new elements with -1
1520+
if newelem.shape[1] == 5:
1521+
extlabel = jsonopt("extlabel", 0, opt)
1522+
extcorelabel = jsonopt("extcorelabel", -1, opt)
1523+
fifth_col = np.full((el2.shape[0], 1), extlabel)
1524+
el2 = np.hstack([el2, fifth_col])
1525+
iselm = np.isin(el2[:, :4], snid).astype(int)
1526+
el2[np.sum(iselm, axis=1) >= 3, 4] = extcorelabel
1527+
1528+
# merge nodes/elements and replace the original ones
1529+
newnode = allnode
1530+
newelem = np.vstack([newelem, el2])
1531+
1532+
print("mesh refinement is complete")
1533+
return newnode, newelem, newface

0 commit comments

Comments
 (0)