def _updateProjectedPts(self): """ internally updates the coordinates of the projected points """ for ptSetName in self.pointSets: # get the current coordinates of projection points n = len(self.pointSets[ptSetName].points) newPts = np.zeros((n, 3)) # newPts should get the new projected coords # get the info geom = self.pointSets[ptSetName].geom u = self.pointSets[ptSetName].u v = self.pointSets[ptSetName].v # This can all be done with arrays if we group points wrt geometry for i in range(n): # evaluate the new projected point coordinates pnt = openvsp.CompPnt01(self.allComps[geom[i]], 0, u[i], v[i]) # update the coordinates newPts[i, :] = (pnt.x(), pnt.y(), pnt.z()) # scale vsp coordinates to mesh coordinates, do it safely above for now newPts *= self.vspScale # set the updated coordinates self.pointSets[ptSetName].pts = newPts
def _computeSurfJacobian(self): """ This routine comptues the jacobian of the VSP surface with respect to the design variables. Since our point sets are rigidly linked to the VSP projection points, this is all we need to calculate. The input pointSets is a list or dictionary of pointSets to calculate the jacobian for. this routine runs in parallel, so it is important that any call leading to this subroutine is performed synchronously among self.comm VSP has a bug they refuse to fix. In a non-deterministic way, the spanwise u-v mapping can differ from run to run. Seems like there are two modes. The differences are small, but if we end up doing the FD with results from another processor the difference is large enough to completely mess up the sensitivity. Due to this we compute both baseline point and perturbed point on the processor that perturbs a given DV this is slightly slower but avoids this issue. the final gradient has some error still, but much more managable and unimportant compared to errors introduced by FD itself. See issue https://github.com/mdolab/pygeo/issues/58 for updates. """ # timing stuff: t1 = time.time() tvsp = 0 teval = 0 tcomm = 0 # counts nDV = self.getNDV() dvKeys = list(self.DVs.keys()) nproc = self.comm.size rank = self.comm.rank # arrays to collect local pointset info ul = np.zeros(0) vl = np.zeros(0) gl = np.zeros(0, dtype="intc") for ptSetName in self.pointSets: # initialize the Jacobians self.pointSets[ptSetName].jac = np.zeros( (3 * self.pointSets[ptSetName].nPts, nDV)) # first, we need to vstack all the point set info we have # counts of these are also important, saved in ptSet.nPts ul = np.concatenate((ul, self.pointSets[ptSetName].u)) vl = np.concatenate((vl, self.pointSets[ptSetName].v)) gl = np.concatenate((gl, self.pointSets[ptSetName].geom)) # now figure out which proc has how many points. sizes = np.array(self.comm.allgather(len(ul)), dtype="intc") # displacements for allgather disp = np.array([np.sum(sizes[:i]) for i in range(nproc)], dtype="intc") # global number of points nptsg = np.sum(sizes) # create a local new point array. We will use this to get the new # coordinates as we perturb DVs. We just need one (instead of nDV times the size) # because we get the new points, calculate the jacobian and save it right after ptsNewL = np.zeros(len(ul) * 3) # create the arrays to receive the global info ug = np.zeros(nptsg) vg = np.zeros(nptsg) gg = np.zeros(nptsg, dtype="intc") # Now we do an allGatherv to get a long list of all pointset information self.comm.Allgatherv([ul, len(ul)], [ug, sizes, disp, MPI.DOUBLE]) self.comm.Allgatherv([vl, len(vl)], [vg, sizes, disp, MPI.DOUBLE]) self.comm.Allgatherv([gl, len(gl)], [gg, sizes, disp, MPI.INT]) # we now have all the point info on all procs. tcomm += time.time() - t1 # We need to evaluate all the points on respective procs for FD computations # allocate memory pts0 = np.zeros((nptsg, 3)) # evaluate the points for j in range(nptsg): pnt = openvsp.CompPnt01(self.allComps[gg[j]], 0, ug[j], vg[j]) pts0[j, :] = (pnt.x(), pnt.y(), pnt.z()) # determine how many DVs this proc will perturb. n = 0 for iDV in range(len(dvKeys)): # I have to do this one. if iDV % nproc == rank: n += 1 # allocate the approriate sized numpy array for the perturbed points ptsNew = np.zeros((n, nptsg, 3)) # perturb the DVs on different procs and compute the new point coordinates. i = 0 # Counter on local Jac for iDV in range(len(dvKeys)): # I have to do this one. if iDV % nproc == rank: # Step size for this particular DV dh = self.DVs[dvKeys[iDV]].dh # Perturb the DV dvSave = self.DVs[dvKeys[iDV]].value.copy() self.DVs[dvKeys[iDV]].value += dh # update the vsp model t11 = time.time() self._updateVSPModel() t12 = time.time() tvsp += t12 - t11 t11 = time.time() # evaluate the points for j in range(nptsg): pnt = openvsp.CompPnt01(self.allComps[gg[j]], 0, ug[j], vg[j]) ptsNew[i, j, :] = (pnt.x(), pnt.y(), pnt.z()) t12 = time.time() teval += t12 - t11 # now we can calculate the jac and put it back in ptsNew ptsNew[i, :, :] = (ptsNew[i, :, :] - pts0[:, :]) / dh # Reset the DV self.DVs[dvKeys[iDV]].value = dvSave.copy() # increment the counter i += 1 # scale the points ptsNew *= self.vspScale # Now, we have perturbed points on each proc that perturbed a DV # reset the model. t11 = time.time() self._updateVSPModel() t12 = time.time() tvsp += t12 - t11 ii = 0 # loop over the DVs and scatter the perturbed points to original procs for iDV in range(len(dvKeys)): # Step size for this particular DV dh = self.DVs[dvKeys[iDV]].dh t11 = time.time() # create the send/recv buffers for the scatter if iDV % nproc == rank: sendbuf = [ ptsNew[ii, :, :].flatten(), sizes * 3, disp * 3, MPI.DOUBLE ] else: sendbuf = [np.zeros((0, 3)), sizes * 3, disp * 3, MPI.DOUBLE] recvbuf = [ptsNewL, MPI.DOUBLE] # scatter the info from the proc that perturbed this DV to all procs self.comm.Scatterv(sendbuf, recvbuf, root=(iDV % nproc)) t12 = time.time() tcomm += t12 - t11 # calculate the jacobian here for the pointsets offset = 0 for ptSet in self.pointSets: # number of points in this pointset nPts = self.pointSets[ptSet].nPts # indices to extract correct points from the long pointset array ibeg = offset * 3 iend = ibeg + nPts * 3 # ptsNewL has the jacobian itself... self.pointSets[ptSet].jac[0:nPts * 3, iDV] = ptsNewL[ibeg:iend].copy() # TODO when OpenVSP fixes the bug in spanwise u-v distribution, we can disable the line above and # go back to the proper way below. we also need to clean up the evaluations themselves # self.pointSets[ptSet].jac[0:nPts*3, iDV] = (ptsNewL[ibeg:iend] - self.pointSets[ptSet].pts.flatten())/dh # increment the offset offset += nPts # pertrub the local counter on this proc. # This loops over the DVs that this proc perturbed if iDV % nproc == rank: ii += 1 t2 = time.time() if rank == 0: print("FD jacobian calcs with dvgeovsp took", (t2 - t1), "seconds in total") print("updating the vsp model took", tvsp, "seconds") print("evaluating the new points took", teval, "seconds") print("communication took", tcomm, "seconds") # set the update flags for ptSet in self.pointSets: self.updatedJac[ptSet] = True
def addPointSet(self, points, ptName, **kwargs): """ Add a set of coordinates to DVGeometry The is the main way that geometry, in the form of a coordinate list is given to DVGeometry to be manipulated. Parameters ---------- points : array, size (N,3) The coordinates to embed. These coordinates *should* all project into the interior of the FFD volume. ptName : str A user supplied name to associate with the set of coordinates. Thisname will need to be provided when updating the coordinates or when getting the derivatives of the coordinates. Returns ------- dMax_global : float The maximum distance betwen the points added and the projection on the VSP surfaces on any processor. """ self.ptSetNames.append(ptName) # save this name so that we can zero out the jacobians properly # ADFlow checks self.points to see if something is added or not. self.points[ptName] = True points = np.array(points).real.astype("d") # we need to project each of these points onto the VSP geometry, # get geometry and surface IDs, u, v values, and coordinates of the projections. # then calculate the self.offset variable using the projected points. # first, to get a good initial guess on the geometry and u,v values, # we can use the adt projections in pyspline if len(points) > 0: # faceID has the index of the corresponding quad element. # uv has the parametric u and v weights of the projected point. faceID, uv = searchQuads(self.pts0.T, (self.conn + 1).T, points.T) uv = uv.T faceID -= 1 # Convert back to zero-based indexing. # after this point we should have the projected points. else: faceID = np.zeros((0), "intc") uv = np.zeros((0, 2), "intc") # now we need to figure out which surfaces the points got projected to # From the faceID we can back out what component each one is # connected to. This way if we have intersecting components we # only change the ones that are apart of the two surfaces. cumFaceSizes = np.zeros(len(self.sizes) + 1, "intc") for i in range(len(self.sizes)): nCellI = self.sizes[i][0] - 1 nCellJ = self.sizes[i][1] - 1 cumFaceSizes[i + 1] = cumFaceSizes[i] + nCellI * nCellJ compIDs = np.searchsorted(cumFaceSizes, faceID, side="right") - 1 # coordinates to store the projected points pts = np.zeros(points.shape) # npoints * 3 list containing the geomID, u and v values # this can be improved if we can group points that get # projected to the same geometry. npoints = len(points) geom = np.zeros(npoints, dtype="intc") u = np.zeros(npoints) v = np.zeros(npoints) # initialize one 3dvec for projections pnt = openvsp.vec3d() # Keep track of the largest distance between cfd and vsp surfaces dMax = 1e-16 t1 = time.time() for i in range(points.shape[0]): # this is the geometry our point got projected to in the adt code gind = compIDs[i] # index gid = self.allComps[gind] # ID # set the coordinates of the point object pnt.set_xyz(points[i, 0] * self.meshScale, points[i, 1] * self.meshScale, points[i, 2] * self.meshScale) # first, we call the fast projection code with the initial guess # this is the global index of the first node of the projected element nodeInd = self.conn[faceID[i], 0] # get the local index of this node nn = nodeInd - self.cumSizes[gind] # figure out the i and j indices of the first point of the element # we projected this point to ii = np.mod(nn, self.sizes[gind, 0]) jj = np.floor_divide(nn, self.sizes[gind, 0]) # calculate the global u and v change in this element du = self.uv[gind][0][ii + 1] - self.uv[gind][0][ii] dv = self.uv[gind][1][jj + 1] - self.uv[gind][1][jj] # now get this points u,v coordinates on the vsp geometry and add # compute the initial guess using the tessalation data of the surface ug = uv[i, 0] * du + self.uv[gind][0][ii] vg = uv[i, 1] * dv + self.uv[gind][1][jj] # project the point d, u[i], v[i] = openvsp.ProjPnt01Guess(gid, 0, pnt, ug, vg) geom[i] = gind # if we dont have a good projection, try projecting again to surfaces # with the slow code. if d > self.projTol: # print('Guess code failed with projection distance',d) # for now, we need to check for all geometries separately. # Just pick the one that yields the smallest d gind = 0 for gid in self.allComps: # only project if the point is in the bounding box of the geometry if ((self.bbox[gid][0, 0] < points[i, 0] < self.bbox[gid][0, 1]) and (self.bbox[gid][1, 0] < points[i, 1] < self.bbox[gid][1, 1]) and (self.bbox[gid][2, 0] < points[i, 2] < self.bbox[gid][2, 1])): # project the point onto the VSP geometry dNew, surf_indx_out, uout, vout = openvsp.ProjPnt01I( gid, pnt) # check if we are closer if dNew < d: # save this info if we found a closer projection u[i] = uout v[i] = vout geom[i] = gind d = dNew gind += 1 # check if the final d is larger than our previous largest value dMax = max(d, dMax) # We need to evaluate this pnt to get its coordinates in physical space pnt = openvsp.CompPnt01(self.allComps[geom[i]], 0, u[i], v[i]) pts[i, 0] = pnt.x() * self.vspScale pts[i, 1] = pnt.y() * self.vspScale pts[i, 2] = pnt.z() * self.vspScale # some debug info dMax_global = self.comm.allreduce(dMax, op=MPI.MAX) t2 = time.time() if self.comm.rank == 0 or self.comm is None: print("DVGeometryVSP note:\nAdding pointset", ptName, "took", t2 - t1, "seconds.") print( "Maximum distance between the added points and the VSP geometry is", dMax_global) # Create the little class with the data self.pointSets[ptName] = PointSet(points, pts, geom, u, v) # Set the updated flag to false because the jacobian is not up to date. self.updated[ptName] = False self.updatedJac[ptName] = False return dMax_global
def test_2(self, train=False, refDeriv=False): """ Test 2: OpenVSP wing test """ # we skip parallel tests for now if not train and self.N_PROCS > 1: self.skipTest("Skipping the parallel test for now.") def sample_uv(nu, nv): # function to create sample uv from the surface and save these points. u = np.linspace(0, 1, nu + 1) v = np.linspace(0, 1, nv + 1) uu, vv = np.meshgrid(u, v) # print (uu.flatten(), vv.flatten()) uv = np.array((uu.flatten(), vv.flatten())) return uv refFile = os.path.join(self.base_path, "ref/test_DVGeometryVSP_02.ref") with BaseRegTest(refFile, train=train) as handler: handler.root_print("Test 2: OpenVSP NACA 0012 wing") vspFile = os.path.join(self.base_path, "../../input_files/naca0012.vsp3") DVGeo = DVGeometryVSP(vspFile) dh = 1e-6 openvsp.ClearVSPModel() openvsp.ReadVSPFile(vspFile) geoms = openvsp.FindGeoms() DVGeo = DVGeometryVSP(vspFile) comp = "WingGeom" # loop over sections # normally, there are 9 sections so we should loop over range(9) for the full test # to have it run faster, we just pick 2 sections for i in [0, 5]: # Twist DVGeo.addVariable(comp, "XSec_%d" % i, "Twist", lower=-10.0, upper=10.0, scale=1e-2, scaledStep=False, dh=dh) # loop over coefs # normally, there are 7 coeffs so we should loop over range(7) for the full test # to have it run faster, we just pick 2 sections for j in [0, 4]: # CST Airfoil shape variables group = "UpperCoeff_%d" % i var = "Au_%d" % j DVGeo.addVariable(comp, group, var, lower=-0.1, upper=0.5, scale=1e-3, scaledStep=False, dh=dh) group = "LowerCoeff_%d" % i var = "Al_%d" % j DVGeo.addVariable(comp, group, var, lower=-0.5, upper=0.1, scale=1e-3, scaledStep=False, dh=dh) # now lets generate ourselves a quad mesh of these cubes. uv_g = sample_uv(8, 8) # total number of points ntot = uv_g.shape[1] # rank on this proc rank = MPI.COMM_WORLD.rank # first, equally divide nuv = ntot // MPI.COMM_WORLD.size # then, add the remainder if rank < ntot % MPI.COMM_WORLD.size: nuv += 1 # allocate the uv array on this proc uv = np.zeros((2, nuv)) # print how mant points we have MPI.COMM_WORLD.Barrier() # loop over the points and save all that this proc owns ii = 0 for i in range(ntot): if i % MPI.COMM_WORLD.size == rank: uv[:, ii] = uv_g[:, i] ii += 1 # get the coordinates nNodes = len(uv[0, :]) openvsp.CompVecPnt01(geoms[0], 0, uv[0, :], uv[1, :]) # extract node coordinates and save them in a numpy array coor = np.zeros((nNodes, 3)) for i in range(nNodes): pnt = openvsp.CompPnt01(geoms[0], 0, uv[0, i], uv[1, i]) coor[i, :] = (pnt.x(), pnt.y(), pnt.z()) # Add this pointSet to DVGeo DVGeo.addPointSet(coor, "test_points") # We will have nNodes*3 many functions of interest... dIdpt = np.zeros((nNodes * 3, nNodes, 3)) # set the seeds to one in the following fashion: # first function of interest gets the first coordinate of the first point # second func gets the second coord of first point etc.... for i in range(nNodes): for j in range(3): dIdpt[i * 3 + j, i, j] = 1 # first get the dvgeo result funcSens = DVGeo.totalSensitivity(dIdpt.copy(), "test_points") # now perturb the design with finite differences and compute FD gradients DVs = DVGeo.getValues() funcSensFD = {} for x in DVs: # perturb the design xRef = DVs[x].copy() DVs[x] += dh DVGeo.setDesignVars(DVs) # get the new points coorNew = DVGeo.update("test_points") # calculate finite differences funcSensFD[x] = (coorNew.flatten() - coor.flatten()) / dh # set back the DV DVs[x] = xRef.copy() # now loop over the values and compare # when this is run with multiple procs, VSP sometimes has a bug # that leads to different procs having different spanwise # u-v distributions. as a result, the final values can differ up to 1e-5 levels # this issue does not come up if this tests is ran with a single proc biggest_deriv = 1e-16 for x in DVs: err = np.array(funcSens[x].squeeze()) - np.array(funcSensFD[x]) maxderiv = np.max(np.abs(funcSens[x].squeeze())) normalizer = np.median(np.abs(funcSensFD[x].squeeze())) if np.abs(normalizer) < 1: normalizer = np.ones(1) normalized_error = err / normalizer if maxderiv > biggest_deriv: biggest_deriv = maxderiv handler.assert_allclose(normalized_error, 0.0, name=f"{x}_grad_normalized_error", rtol=1e0, atol=5e-5) # make sure that at least one derivative is nonzero self.assertGreater(biggest_deriv, 0.005)