|
23 | 23 | "surfvolume",
|
24 | 24 | "insurface",
|
25 | 25 | "remeshsurf",
|
| 26 | + "meshrefine", |
26 | 27 | ]
|
27 | 28 |
|
28 | 29 | ##====================================================================================
|
|
43 | 44 | finddisconnsurf,
|
44 | 45 | maxsurf,
|
45 | 46 | volface,
|
| 47 | + surfseeds, |
| 48 | + maxsurf, |
| 49 | + ismember_rows, |
46 | 50 | )
|
47 | 51 | 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 | +) |
49 | 64 | from iso2mesh.modify import (
|
50 | 65 | meshcheckrepair,
|
51 | 66 | sortmesh,
|
52 | 67 | meshresample,
|
53 | 68 | removeisolatedsurf,
|
54 | 69 | removeisolatednode,
|
55 | 70 | qmeshcut,
|
| 71 | + removedupelem, |
56 | 72 | )
|
57 | 73 |
|
58 | 74 | ##====================================================================================
|
@@ -1220,3 +1236,298 @@ def remeshsurf(node, face, opt):
|
1220 | 1236 | newno[:, 2] += p0[2]
|
1221 | 1237 |
|
1222 | 1238 | 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 | + |
| 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