Skip to content

Commit fd06a01

Browse files
committed
[test] tested and fixed meshacylinders
1 parent 757769e commit fd06a01

File tree

3 files changed

+235
-53
lines changed

3 files changed

+235
-53
lines changed

iso2mesh/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
meshabox,
99
meshacylinder,
1010
meshcylinders,
11+
meshcylinders,
1112
meshanellip,
1213
meshunitsphere,
1314
meshasphere,
@@ -178,6 +179,7 @@
178179
"mesh2vol",
179180
"meshabox",
180181
"meshacylinder",
182+
"meshcylinders",
181183
"meshanellip",
182184
"meshasphere",
183185
"meshcentroid",

iso2mesh/geometry.py

Lines changed: 195 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"meshabox",
1313
"meshacylinder",
1414
"meshcylinders",
15+
"meshcylinders",
1516
"meshanellip",
1617
"meshunitsphere",
1718
"meshasphere",
@@ -29,7 +30,7 @@
2930
from iso2mesh.core import surf2mesh, vol2restrictedtri
3031
from iso2mesh.trait import meshreorient, volface, surfedge
3132
from iso2mesh.utils import *
32-
from iso2mesh.modify import removeisolatednode
33+
from iso2mesh.modify import removeisolatednode, meshcheckrepair
3334

3435
# _________________________________________________________________________________________________________
3536

@@ -309,6 +310,26 @@ def meshgrid5(*args):
309310

310311

311312
def meshgrid6(*args):
313+
"""
314+
Generate a tetrahedral mesh from an N-D rectangular lattice by splitting
315+
each hypercube into 6 tetrahedra.
316+
317+
Parameters:
318+
v1, v2, v3, ... : array-like
319+
Numeric vectors defining the lattice in each dimension.
320+
Each vector must be of length >= 1.
321+
322+
Returns:
323+
node : ndarray
324+
Coordinates of the nodes in the factorial lattice created from (v1, v2, v3, ...).
325+
Each row corresponds to a node.
326+
elem : ndarray
327+
Integer array defining the simplices (tetrahedra) as indices into rows of `node`.
328+
329+
Notes:
330+
This function is part of the iso2mesh toolbox (http://iso2mesh.sf.net)
331+
Originally authored by John D'Errico, with modifications by Qianqian Fang.
332+
"""
312333
# dimension of the lattice
313334
n = len(args)
314335

@@ -456,72 +477,193 @@ def latticegrid(*args):
456477
# _________________________________________________________________________________________________________
457478

458479

459-
def extrudecurve(c0, c1, curve, ndiv):
460-
if len(c0) != len(c1) or len(c0) != 3:
461-
raise ValueError("c0 and c1 must be 3D points of the same dimension!")
462-
463-
if ndiv < 1:
464-
raise ValueError("ndiv must be at least 1!")
465-
466-
curve = np.array(curve)
467-
if curve.shape[1] != 3:
468-
raise ValueError("curve must be a Nx3 array!")
469-
470-
ncurve = curve.shape[0]
471-
nodes = np.zeros((ndiv * ncurve, 3))
472-
for i in range(ndiv):
473-
alpha = i / (ndiv - 1) # linear interpolation factor
474-
point = (1 - alpha) * c0 + alpha * c1
475-
nodes[i * ncurve : (i + 1) * ncurve, :] = curve + point
476-
477-
elem = np.zeros((ncurve * (ndiv - 1) * 2, 4), dtype=int)
478-
for i in range(ndiv - 1):
479-
for j in range(ncurve):
480-
if j < ncurve - 1:
481-
elem[i * ncurve * 2 + j * 2, :] = [
482-
i * ncurve + j,
483-
(i + 1) * ncurve + j,
484-
(i + 1) * ncurve + (j + 1),
485-
i * ncurve + (j + 1),
486-
]
487-
elem[i * ncurve * 2 + j * 2 + 1, :] = [
488-
(i + 1) * ncurve + j,
489-
(i + 1) * ncurve + (j + 1),
490-
i * ncurve + (j + 1),
491-
i * ncurve + j,
492-
]
493-
494-
return nodes, elem
480+
def extrudecurve(
481+
xy, yz, Nx=30, Nz=30, Nextrap=0, spacing=1, anchor=None, dotopbottom=0
482+
):
483+
"""
484+
Create a triangular surface mesh by swinging a 2D spline along another 2D spline curve.
485+
486+
Parameters:
487+
xy : ndarray
488+
A 2D spline path, along which the surface is extruded, defined on the x-y plane.
489+
yz : ndarray
490+
A 2D spline which will move along the path to form a surface, defined on the y-z plane.
491+
Nx : int, optional
492+
The count of sample points along the extrusion path (xy), default is 30.
493+
Nz : int, optional
494+
The count of sample points along the curve to be extruded (yz), default is 30.
495+
Nextrap : int, optional
496+
Number of points to extrapolate outside of the xy/yz curves, default is 0.
497+
spacing : float, optional
498+
Define a spacing scaling factor for spline interpolations, default is 1.
499+
anchor : list or ndarray, optional
500+
The 3D point in the extruded curve plane (yz) that is aligned at the nodes long the extrusion path.
501+
If not provided, it is set as the point on the interpolated yz with the largest y-value.
502+
dotopbottom : int, optional
503+
If set to 1, tessellated top and bottom faces will be added, default is 0.
504+
505+
Returns:
506+
node : ndarray
507+
3D node coordinates for the generated surface mesh.
508+
face : ndarray
509+
Triangular face patches of the generated surface mesh, each row represents a triangle.
510+
yz0 : ndarray
511+
Sliced yz curve at the start.
512+
yz1 : ndarray
513+
Sliced yz curve at the end.
514+
515+
-- this function is part of iso2mesh toolbox (http://iso2mesh.sf.net)
516+
"""
517+
from scipy.interpolate import splev, splrep
518+
519+
# Compute interpolation points along the xy curve
520+
xrange = np.max(xy[:, 0]) - np.min(xy[:, 0])
521+
dx = xrange / Nx
522+
xi = np.arange(
523+
np.min(xy[:, 0]) - Nextrap * dx,
524+
np.max(xy[:, 0]) + Nextrap * dx + spacing * dx / 2,
525+
spacing * dx,
526+
)
527+
pxy = splrep(xy[:, 0], xy[:, 1])
528+
529+
# Evaluate the interpolated y values and gradients
530+
yi = splev(xi, pxy)
531+
dy = np.gradient(yi)
532+
dxi = np.gradient(xi)
533+
534+
nn = np.sqrt(dxi**2 + dy**2)
535+
normaldir = np.vstack((dxi / nn, dy / nn)).T
536+
537+
# Compute interpolation points along the yz curve
538+
zrange = np.max(yz[:, 1]) - np.min(yz[:, 1])
539+
dz = zrange / Nz
540+
zi = np.arange(
541+
np.min(yz[:, 1]) - Nextrap * dz,
542+
np.max(yz[:, 1]) + Nextrap * dz + spacing * dz / 2,
543+
spacing * dz,
544+
)
545+
pyz = splrep(yz[:, 1], yz[:, 0])
546+
547+
yyi = splev(zi, pyz)
548+
549+
# Determine anchor point if not provided
550+
if anchor is None:
551+
loc = np.argmax(yyi)
552+
anchor = [0, yyi[loc], zi[loc]]
553+
554+
# Initialize node and face arrays
555+
node = np.zeros((len(zi) * len(xi), 3))
556+
face = np.zeros((2 * (len(zi) - 1) * (len(xi) - 1), 3), dtype=int)
557+
558+
# Generate the base yz profile points
559+
xyz = np.column_stack((np.zeros_like(yyi), yyi, zi))
560+
for i in range(len(xi)):
561+
# Compute local rotation matrix
562+
rot2d = np.array(
563+
[[normaldir[i, 0], -normaldir[i, 1]], [normaldir[i, 1], normaldir[i, 0]]]
564+
)
565+
offset = [xi[i], yi[i], anchor[2]]
566+
newyz = xyz.copy()
567+
newyz[:, :2] = (rot2d @ (newyz[:, :2] - anchor[:2]).T).T + offset[:2]
568+
node[i * len(zi) : (i + 1) * len(zi), :] = newyz
569+
570+
# Create faces between segments
571+
if i > 0:
572+
a = np.arange(len(zi) - 1)
573+
b = a + 1
574+
f1 = np.stack(
575+
(a + (i - 1) * len(zi), a + i * len(zi), b + (i - 1) * len(zi)), axis=-1
576+
)
577+
f2 = np.stack(
578+
(b + (i - 1) * len(zi), a + i * len(zi), b + i * len(zi)), axis=-1
579+
)
580+
face[(i - 1) * 2 * (len(zi) - 1) : (i) * 2 * (len(zi) - 1)] = np.vstack(
581+
(f1, f2)
582+
)
583+
584+
# Save yz slices for later output
585+
if i == Nextrap:
586+
yz0 = newyz[Nextrap : len(zi) - Nextrap, :]
587+
if i == len(xi) - Nextrap - 1:
588+
yz1 = newyz[Nextrap : len(zi) - Nextrap, :]
589+
590+
# Add two flat polygons on the top and bottom of the contours
591+
# to ensure the enclosed surface is not truncated by meshfix
592+
if dotopbottom == 1:
593+
from scipy.spatial import Delaunay
594+
595+
C = np.vstack((np.arange(0, len(xi) - 1), np.arange(1, len(xi)))).T
596+
C = np.vstack((C, [[len(xi) - 1, 0]]))
597+
dt = Delaunay(np.column_stack((xi, yi)))
598+
io = dt.find_simplex(np.column_stack((xi, yi))) >= 0
599+
endface = dt.simplices[io]
600+
endface = (endface - 1) * len(zi) + 1
601+
face = np.vstack((face, endface, endface + len(zi) - 1))
602+
603+
# Check and repair mesh geometry
604+
node, face = meshcheckrepair(node, face, "deep")
605+
606+
return node, face, yz0, yz1
495607

496608

497609
# _________________________________________________________________________________________________________
498610

499611

500-
def meshcylinders(c0, c1, r, tsize=0, maxvol=0, ndiv=20):
501-
if np.any(np.array(r) <= 0):
502-
raise ValueError("Radius must be greater than zero.")
612+
def meshcylinders(c0, v, seglen, r, tsize=None, maxvol=None, ndiv=20):
613+
"""
614+
create the surface and (optionally) tetrahedral mesh of multiple segments of 3D cylinders
615+
616+
author: Qianqian Fang, <q.fang at neu.edu>
503617
504-
if np.array(c0).shape != (3,) or np.array(c1).shape != (3,):
505-
raise ValueError("c0 and c1 must be 3D points.")
618+
Parameters:
619+
c0: cylinder list axis's starting point
620+
v: directional vector of the cylinder
621+
seglen: a scalar or a vector denoting the length of each
622+
cylinder segment along the direction of v
623+
args: tsize, maxvol, ndiv - see meshacylinder for details
506624
507-
if len(r) == 1:
508-
r = [r[0], r[0]]
625+
Returns:
626+
node, face, elem - see meshacylinder for details
509627
510-
r = np.array(r).flatten()
628+
-- this function is part of iso2mesh toolbox (http://iso2mesh.sf.net)
629+
"""
630+
seglen = np.cumsum(seglen)
631+
c0 = np.array(c0)
632+
v = np.array(v)
633+
ncyl, fcyl = meshacylinder(c0, c0 + v * seglen[0], r, 0, 0, ndiv)
634+
635+
if len(seglen) == 1:
636+
node = ncyl
637+
face = fcyl
638+
return node, face
639+
640+
for i in range(1, len(seglen)):
641+
ncyl1, fcyl1 = meshacylinder(
642+
c0 + v * seglen[i - 1], c0 + v * seglen[i], r, 0, 0, ndiv
643+
)
644+
fcyl1 = [[(np.array(f[0]) + ncyl.shape[0]).tolist(), f[1]] for f in fcyl1]
645+
fcyl1 = fcyl1[:-2] + [fcyl1[-1]]
646+
fcyl.extend(fcyl1)
647+
ncyl = np.vstack((ncyl, ncyl1))
511648

512-
if len(r) == 2:
513-
r = np.array([r[0], r[0], r[1]])
649+
ncyl, I, J = np.unique(
650+
np.round(ncyl, 10), axis=0, return_index=True, return_inverse=True
651+
)
514652

515-
len_axis = np.linalg.norm(np.array(c1) - np.array(c0))
653+
fcyl = [[(J[np.array(f[0]) - 1] + 1).tolist(), f[1]] for f in fcyl]
516654

517-
if tsize == 0:
518-
tsize = min(r) * 0.1
655+
if tsize == 0 and maxvol == 0:
656+
return ncyl, fcyl
519657

520-
if maxvol == 0:
521-
maxvol = tsize**3 * 0.2
658+
if not tsize:
659+
tsize = seglen[-1] * 0.1
660+
if not maxvol:
661+
maxvol = tsize * tsize * tsize
522662

523-
node, face, elem = meshacylinder(c0, c1, r, tsize, maxvol, ndiv)
663+
centroid = np.cumsum(np.concatenate(([0], seglen[:-1]))) + seglen[-1] * 0.5
664+
seeds = c0 + v * centroid[:, None]
524665

666+
node, elem, face = surf2mesh(ncyl, fcyl, None, None, 1, maxvol, seeds, None, 0)
525667
return node, face, elem
526668

527669

test/run_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,44 @@ def test_meshacylinder(self):
160160
self.assertEqual(round(sum(elemvolume(no, fc)), 4), 1045.2322)
161161
self.assertEqual(round(sum(elemvolume(no, el[:, :4])), 4), 1402.8993)
162162

163+
def test_meshacylinders_plc(self):
164+
(
165+
no,
166+
fc,
167+
) = meshcylinders([10, 20, 30], [0, 1, 1], [2, 4], 3, 0, 0, 8)
168+
expected_fc = [
169+
[[[18, 19, 14, 12]], [1]],
170+
[[[12, 14, 7, 6]], [1]],
171+
[[[6, 7, 2, 1]], [1]],
172+
[[[1, 2, 5, 4]], [1]],
173+
[[[4, 5, 11, 10]], [1]],
174+
[[[10, 11, 17, 16]], [1]],
175+
[[[16, 17, 23, 22]], [1]],
176+
[[[22, 23, 19, 18]], [1]],
177+
[[[18, 12, 6, 1, 4, 10, 16, 22]], [2]],
178+
[[[19, 14, 7, 2, 5, 11, 17, 23]], [3]],
179+
[[[19, 21, 15, 14]], [1]],
180+
[[[14, 15, 9, 7]], [1]],
181+
[[[7, 9, 3, 2]], [1]],
182+
[[[2, 3, 8, 5]], [1]],
183+
[[[5, 8, 13, 11]], [1]],
184+
[[[11, 13, 20, 17]], [1]],
185+
[[[17, 20, 24, 23]], [1]],
186+
[[[23, 24, 21, 19]], [1]],
187+
[[[21, 15, 9, 3, 8, 13, 20, 24]], [3]],
188+
]
189+
self.assertEqual(fc, expected_fc)
190+
self.assertEqual(np.sum(no), 1568)
191+
192+
def test_meshacylinders(self):
193+
no, fc, el = meshcylinders([10, 20, 30], [0, 1, 1], [2, 4], 3, 1, 10, 8)
194+
self.assertAlmostEqual(
195+
sum(elemvolume(no, fc[fc[:, -1] == 1, :3])), 155.86447684358734, 3
196+
)
197+
self.assertAlmostEqual(
198+
sum(elemvolume(no, el[el[:, -1] == 2, :4])), 144.0000000027395, 3
199+
)
200+
163201

164202
class Test_trait(unittest.TestCase):
165203
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)