From 9bbcdf0e3f9e079fd561bf37eebde1d55ed93d99 Mon Sep 17 00:00:00 2001 From: Bernardo Pacini <61238730+bernardopacini@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:04:46 -0500 Subject: [PATCH] Adding sliding curves for DVGeoMulti (#231) * Adding sliding curves for intersections * Adding test for sliding curves * Adding check to ensure one coordinate in sliding curve test changed * Addressing Sabet's comments * address sabet's comments * Fixing test assertion * Updating test tolerances --------- Co-authored-by: Anil Yildirim --- pygeo/parameterization/DVGeoMulti.py | 55 +++++++--- tests/reg_tests/test_DVGeometryMulti.py | 133 ++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 14 deletions(-) diff --git a/pygeo/parameterization/DVGeoMulti.py b/pygeo/parameterization/DVGeoMulti.py index 7d9acfd5..3cbf756b 100644 --- a/pygeo/parameterization/DVGeoMulti.py +++ b/pygeo/parameterization/DVGeoMulti.py @@ -184,6 +184,7 @@ def addIntersection( project=False, marchDir=1, includeCurves=False, + slidingCurves=None, intDir=None, curveEpsDict=None, trackSurfaces=None, @@ -233,6 +234,10 @@ def addIntersection( includeCurves : bool, optional Flag to specify whether to include features curves in the inverse-distance deformation. + slidingCurves : list, optional + The list of curves to project to, but on which the mesh nodes are not frozen in their initial positions. + This allows the mesh nodes to slide along the feature curve. + intDir : int, optional If there are multiple intersection curves, this specifies which curve to choose. The sign determines the direction and the value (1, 2, 3) specifies the axis (x, y, z). @@ -270,6 +275,8 @@ def addIntersection( # Assign mutable defaults if featureCurves is None: featureCurves = [] + if slidingCurves is None: + slidingCurves = [] if curveEpsDict is None: curveEpsDict = {} if trackSurfaces is None: @@ -290,6 +297,7 @@ def addIntersection( project, marchDir, includeCurves, + slidingCurves, intDir, curveEpsDict, trackSurfaces, @@ -1006,6 +1014,7 @@ def __init__( project, marchDir, includeCurves, + slidingCurves, intDir, curveEpsDict, trackSurfaces, @@ -1195,6 +1204,10 @@ def __init__( # flag to include feature curves in ID-warping self.incCurves = includeCurves + # List of curves that allow nodes to slide on them. We only use these for the projection step, + # but these curves are not included as seeds in the curve-based deformation. + self.slidingCurves = slidingCurves + # direction to pick if we have multiple intersection curves self.intDir = intDir @@ -1433,7 +1446,7 @@ def addPointSet(self, pts, ptSetName, compMap, comm): elemIDs[:] = elemIDs + 1 # (we need to do this separetely because Fortran will actively change elemIDs contents. self.curveSearchAPI.mindistancecurve( - ptsToCurves.T, self.seam0.T, self.seamConn.T + 1, xyzProj.T, tanProj.T, dist2, elemIDs + ptsToCurves.T, self.seam0.T, self.seamConnFull.T + 1, xyzProj.T, tanProj.T, dist2, elemIDs ) # Adjust indices back to Python standards @@ -1543,7 +1556,7 @@ def update(self, ptSetName, delta): # we use the initial seam coordinates here coor = self.seam0 # bar connectivity for the remeshed elements - conn = self.seamConn + conn = self.seamConnWarp # deltas for each point (nNode, 3) in size if self.seam.shape == self.seam0.shape: dr = self.seam - self.seam0 @@ -1650,7 +1663,7 @@ def sens(self, dIdPt, ptSetName, comm): # we use the initial seam coordinates here coor = self.seam0 # bar connectivity for the remeshed elements - conn = self.seamConn + conn = self.seamConnWarp # Get the two end points for the line elements r0 = coor[conn[:, 0]] @@ -1787,7 +1800,7 @@ def project(self, ptSetName, newPts): # conn of the current curve seamBeg = self.seamBeg[curveName] seamEnd = self.seamEnd[curveName] - curveConn = self.seamConn[seamBeg:seamEnd] + curveConn = self.seamConnFull[seamBeg:seamEnd] # Project these to the combined curves using pySurf # Get number of points @@ -2769,7 +2782,8 @@ def _getIntersectionSeam(self, comm, firstCall=False): self.distFeature = {} remeshedCurves = np.zeros((0, 3), dtype=self.dtype) - remeshedCurveConn = np.zeros((0, 2), dtype="int32") + remeshedCurveConnFull = np.zeros((0, 2), dtype="int32") + remeshedCurveConnWarp = np.zeros((0, 2), dtype="int32") # loop over each curve, figure out what nodes get re-meshed, re-mesh, and append to seam... for curveName in self.featureCurveNames: @@ -2890,9 +2904,16 @@ def _getIntersectionSeam(self, comm, firstCall=False): # increment the connectivitiy data newBarsConn += len(remeshedCurves) - # append this new curve to the featureCurve data + # Append this new curve to the featureCurve data. remeshedCurves = np.vstack((remeshedCurves, newCoor)) - remeshedCurveConn = np.vstack((remeshedCurveConn, newBarsConn)) + + # By excluding sliding curves here in the 'warp' array, + # they are not used as seeds for the curved-based deformation. + # This means that points on these curves get warped like any other point. + # We also still want the 'full' connectivity because that is used for projections. + remeshedCurveConnFull = np.vstack((remeshedCurveConnFull, newBarsConn)) + if curveName not in self.slidingCurves: + remeshedCurveConnWarp = np.vstack((remeshedCurveConnWarp, newBarsConn)) # number of new nodes added in the opposite direction nNewNodesReverse = 0 @@ -2919,7 +2940,10 @@ def _getIntersectionSeam(self, comm, firstCall=False): newBarsConn = newBarsConn + len(remeshedCurves) remeshedCurves = np.vstack((remeshedCurves, newCoor)) - remeshedCurveConn = np.vstack((remeshedCurveConn, newBarsConn)) + remeshedCurveConnFull = np.vstack((remeshedCurveConnFull, newBarsConn)) + + if curveName not in self.slidingCurves: + remeshedCurveConnWarp = np.vstack((remeshedCurveConnWarp, newBarsConn)) if curveName in curveBegCoor: # finally, put the modified initial and final points back in place. @@ -2937,28 +2961,31 @@ def _getIntersectionSeam(self, comm, firstCall=False): if firstCall: # save the beginning and end indices of these elements self.seamBeg[curveName] = ( - len(finalConn) + len(remeshedCurveConn) - (nNewNodes + nNewNodesReverse) + 2 + len(finalConn) + len(remeshedCurveConnFull) - (nNewNodes + nNewNodesReverse) + 2 ) - self.seamEnd[curveName] = len(finalConn) + len(remeshedCurveConn) + self.seamEnd[curveName] = len(finalConn) + len(remeshedCurveConnFull) # Output the feature curves if self.comm.rank == 0 and self.debug: curvename = f"featureCurves_{self.counter}" - tecplot_interface.writeTecplotFEdata(remeshedCurves, remeshedCurveConn, curvename, curvename) + tecplot_interface.writeTecplotFEdata(remeshedCurves, remeshedCurveConnFull, curvename, curvename) # now we are done going over curves, # so we can append all the new curves to the "seam", # which now contains the intersection, and re-meshed feature curves # increment the conn from curves - remeshedCurveConn += len(seam) + remeshedCurveConnFull += len(seam) + remeshedCurveConnWarp += len(seam) # stack the nodes seam = np.vstack((seam, remeshedCurves)) # stack the conn - finalConn = np.vstack((finalConn, remeshedCurveConn)) + finalConnFull = np.vstack((finalConn, remeshedCurveConnFull)) + finalConnWarp = np.vstack((finalConn, remeshedCurveConnWarp)) # save the connectivity - self.seamConn = finalConn + self.seamConnFull = finalConnFull + self.seamConnWarp = finalConnWarp self.counter += 1 diff --git a/tests/reg_tests/test_DVGeometryMulti.py b/tests/reg_tests/test_DVGeometryMulti.py index 60424738..f46e63cf 100644 --- a/tests/reg_tests/test_DVGeometryMulti.py +++ b/tests/reg_tests/test_DVGeometryMulti.py @@ -315,6 +315,139 @@ def twist(val, geo, nRefAxPts=nRefAxPts): with self.assertRaises(Error): DVGeo.addPointSet(np.array([[-1.0, 0.0, 0.0]]), "test_error") + def test_slidingCurves(self): + # box1 and box2 intersect + comps = ["box1", "box2"] + ffdFiles = [os.path.join(inputDir, f"{comp}.xyz") for comp in comps] + triMeshFiles = [os.path.join(inputDir, f"{comp}.cgns") for comp in comps] + + # Define the communicator + comm = MPI.COMM_WORLD + + # Set up real component DVGeo objects + DVGeoBox1 = DVGeometry(ffdFiles[0]) + DVGeoBox2 = DVGeometry(ffdFiles[1]) + + # Set up real DVGeometryMulti object + DVGeo = DVGeometryMulti(comm=comm) + DVGeo.addComponent("box1", DVGeoBox1, triMeshFiles[0]) + DVGeo.addComponent("box2", DVGeoBox2, triMeshFiles[1]) + + # Define some feature curves + featureCurves = { + # Curves on box1 + "part_22_1d": None, + "part_23_1d": None, + # Curves on box2 + "part_35_1d": 1, + "part_37_1d": 1, + "part_39_1d": 1, + } + curveEpsDict = { + # Curves on box1 + "part_22_1d": 1e-3, + "part_23_1d": 1e-3, + # Curves on box2 + "part_35_1d": 1e-3, + "part_37_1d": 1e-3, + "part_39_1d": 1e-3, + # Intersection curve + "intersection": 1e-3, + } + + slidingCurves = [ + # Curves on box1 + "part_22_1d", + "part_23_1d", + ] + + # Define a name for the point set + ptSetName = "test_set" + + # Define a test point set + pts = np.array( + [ + [1.0, -0.4, 0.5], # curve 22 + [1.0, -0.2, 0.5], # curve 22 + [1.0, 0.2, 0.5], # curve 23 + [1.0, 0.4, 0.5], # curve 23 + ] + ) + + # Compute the processor sizes with integer division + sizes = np.zeros(comm.size, dtype="intc") + nPtsGlobal = pts.shape[0] + sizes[:] = nPtsGlobal // comm.size + + # Add the leftovers + sizes[: nPtsGlobal % comm.size] += 1 + + # Compute the processor displacements + disp = np.zeros(comm.size + 1, dtype="intc") + disp[1:] = np.cumsum(sizes) + + # Split up the point set + localPts = pts[disp[comm.rank] : disp[comm.rank + 1]] + + # Add the intersection between box1 and box2 + DVGeo.addIntersection( + "box1", + "box2", + dStarA=1.0, + dStarB=0.15, + featureCurves=featureCurves, + project=True, + includeCurves=True, + slidingCurves=slidingCurves, + curveEpsDict=curveEpsDict, + ) + + # Add a few design variables + DVGeoDict = DVGeo.getDVGeoDict() + for comp in comps: + # Create reference axis + nRefAxPts = DVGeoDict[comp].addRefAxis("box", xFraction=0.5, alignIndex="j", rotType=4) + nTwist = nRefAxPts - 1 + + # Set up a twist variable + def twist(val, geo, nRefAxPts=nRefAxPts): + for i in range(1, nRefAxPts): + geo.rot_z["box"].coef[i] = val[i - 1] + + DVGeoDict[comp].addGlobalDV(dvName=f"{comp}_twist", value=[0] * nTwist, func=twist) + + # Add the point set + DVGeo.addPointSet(localPts, ptSetName, comm=comm, applyIC=True) + + # Apply twist to box 2 + dvDict = DVGeo.getValues() + dvDict["box2_twist"] = 10 + DVGeo.setDesignVars(dvDict) + + # Update the point set + ptsUpdated = DVGeo.update(ptSetName) + + # Create the send buffer + procPoints = ptsUpdated.flatten() + sendbuf = [procPoints, sizes[comm.rank] * 3] + + # Create the receiving buffer + globalPoints = np.zeros(nPtsGlobal * 3) + recvbuf = [globalPoints, sizes * 3, disp[0:-1] * 3, MPI.DOUBLE] + + # Allgather the updated coordinates + comm.Allgatherv(sendbuf, recvbuf) + + # Reshape into a nPtsGlobal, 3 array + ptsUpdated = globalPoints.reshape((nPtsGlobal, 3)) + + # Test that the X and Z coordinates are unchanged and Y coordinates are changed + np.testing.assert_allclose(pts[:, 0], ptsUpdated[:, 0], rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(pts[:, 2], ptsUpdated[:, 2], rtol=1e-10, atol=1e-10) + + with self.assertRaises(AssertionError): + np.testing.assert_allclose(pts[:, 1], ptsUpdated[:, 1], rtol=1e-4, atol=1e-10) + @unittest.skipUnless(pysurfInstalled, "requires pySurf") class TestDVGeoMultiEdgeCases(unittest.TestCase):