Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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)