# Standard Python modules
from collections import OrderedDict
import time
# External modules
from baseclasses.utils import Error
from mpi4py import MPI
import numpy as np
from packaging.version import Version
from pyspline.utils import searchQuads
# Local modules
from .DVGeoSketch import DVGeoSketch
from .designVars import vspDV
# openvsp python interface
try:
    # External modules
    import openvsp
    vspInstalled = True
except ImportError:
    try:
        # External modules
        import vsp as openvsp
        vspInstalled = True
    except ImportError:
        openvsp = None
        vspInstalled = False
[docs]
class DVGeometryVSP(DVGeoSketch):
    """
    A class for manipulating OpenVSP geometry.
    The purpose of the DVGeometryVSP class is to provide translation of the VSP geometry engine to externally supplied surfaces.
    This allows the use of VSP design variables to control the MACH framework.
    There are several important limitations:
    #. Since VSP is volume-based, it cannot be used to parameterize a geometry that does not lie within a VSP body.
    #. It cannot handle *moving* intersections. A geometry with static intersections is fine as long as the intersection doesn't move
    #. It does not support complex numbers for the complex-step method.
    #. It does not support separate configurations.
    #. Because OpenVSP does not provide sensitivities, this class uses parallel finite differencing to obtain the required Jacobian matrices.
    Parameters
    ----------
    fileName : str
       filename of .vsp3 file.
    comm : MPI Intra Comm
       Comm on which to build operate the object. This is used to
       perform embarrassingly parallel finite differencing. Defaults to
       MPI.COMM_WORLD.
    scale : float
       A global scale factor from the VSP geometry to incoming (CFD) mesh
       geometry. For example, if the VSP model is in inches, and the CFD
       in meters, scale=0.0254.
    comps : list of strings
       A list of string defining the subset of the VSP components to use when
       exporting the P3D surface files
    Examples
    --------
    The general sequence of operations for using DVGeometry is as follows:
      >>> from pygeo import DVGeometryVSP
      >>> DVGeo = DVGeometryVSP("wing.vsp3", MPI_COMM_WORLD)
      >>> # Add a set of coordinates Xpt into the object
      >>> DVGeo.addPointSet(Xpt, "myPoints")
    """
    def __init__(self, fileName, comm=MPI.COMM_WORLD, scale=1.0, comps=[], projTol=0.01, name=None):
        vspOutOfDate = False
        if vspInstalled:
            vspVersionStr = openvsp.GetVSPVersion()
            words = vspVersionStr.split()
            vspVersion = words[-1]
            # VSP is installed, but version too old
            if Version(vspVersion) < Version("3.28.0"):
                vspOutOfDate = True
            # Prior to OpenVSP 3.33.0, the "s" parameter varied between [0, 0.5]
            elif Version(vspVersion) < Version("3.33.0"):
                self.SMAX = 0.5
            # After this version, the range was changed to [0, 1.0].
            else:  # Version(vsp_version) >= Version("3.33.0")
                self.SMAX = 1.0
        if not vspInstalled:
            raise ImportError(
                "The OpenVSP Python API is required in order to use DVGeometryVSP. "
                + "Ensure OpenVSP is installed properly and can be found on your path."
            )
        elif vspOutOfDate:
            raise AttributeError(
                "Out of date version of OpenVSP detected. "
                + "OpenVSP 3.28.0 or greater is required in order to use DVGeometryVSP"
            )
        if comm.rank == 0:
            print("Initializing DVGeometryVSP")
            t0 = time.time()
        super().__init__(fileName=fileName, comm=comm, scale=scale, projTol=projTol, name=name)
        if hasattr(openvsp, "VSPVehicle"):
            self.vspModel = openvsp.VSPVehicle()
        else:
            self.vspModel = openvsp
        self.exportComps = []
        # Clear the vsp model
        self.vspModel.ClearVSPModel()
        t1 = time.time()
        # read the model
        self.vspModel.ReadVSPFile(fileName)
        t2 = time.time()
        if self.comm.rank == 0:
            print("Loading the vsp model took:", (t2 - t1))
        # List of all components returned from VSP. Note that this
        # order is important. It is the order that we use to map the
        # actual geom_id by using the geom_names
        allComps = self.vspModel.FindGeoms()
        allNames = []
        for c in allComps:
            allNames.append(self.vspModel.GetContainerName(c))
        if not comps:
            # no components specified, we use all
            self.allComps = allComps[:]
        else:
            # we get the vsp comp IDs from the comps list
            self.allComps = []
            for c in comps:
                self.allComps.append(allComps[allNames.index(c)])
        # we need the names and bounding boxes of components
        self.compNames = []
        self.bbox = OrderedDict()
        self.bboxuv = self._getuv()
        for c in self.allComps:
            self.compNames.append(self.vspModel.GetContainerName(c))
            self.bbox[c] = self._getBBox(c)
        # Now, we need to form our own quad meshes for fast projections
        if comm.rank == 0:
            print("Building a quad mesh for fast projections.")
        self._getQuads()
        self.useComposite = False
        if comm.rank == 0:
            t3 = time.time()
            print("Initialized DVGeometry VSP in", (t3 - t0), "seconds.")
[docs]
    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")
        r = np.zeros(npoints)
        s = np.zeros(npoints)
        t = 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 to find the nearest point on the surface
            # 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  tesselation data of the surface
            ug = uv[i, 0] * du + self.uv[gind][0][ii]
            vg = uv[i, 1] * dv + self.uv[gind][1][jj]
            # We now convert the surface parameteric variables (u,v) to their equivalent volume parameterization (r,s,t)
            # this will serve as our initial guess for the volume projection procedure
            # In general, the conversion between the surface parametric coordinates (u, v)
            # and the first two volume parameteric coordinate (r, s)
            # are given by the equations below:
            #   u = r
            #   v_low = s
            #   v_upper = 1 - s
            # The 3rd parameteric coordinate interpolates between the the upper and lower surfaces
            #   X(r,s,t) = t * X_upper(r, s) + (1 - t) * X_lower(r, s)
            rg = ug
            if vg < 0.5:
                # This point is on the lower surface
                sg = self.SMAX * 2.0 * vg
            else:
                # This point is on the upper surface
                sg = self.SMAX * 2.0 * (1.0 - vg)
            # tg = 0.5 places the initial guess in the middle of the upper and lower surfaces for the volume
            # Note: If the point we're looking for actually lies on the surface openvsp's
            # projection algorithm (FindRSTGuess) will still quickly locate it, since our volume interpolation
            # is linear through the depth (i.e. in t)
            tg = 0.5
            # Now, find the closest volume projection
            d, r[i], s[i], t[i] = self.vspModel.FindRSTGuess(gid, 0, pnt, rg, sg, tg)
            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, rout, sout, tout = self.vspModel.FindRST(gid, 0, pnt)
                        # check if we are closer
                        if dNew < d:
                            # save this info if we found a closer projection
                            r[i] = rout
                            s[i] = sout
                            t[i] = tout
                            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 = self.vspModel.CompPntRST(self.allComps[geom[i]], 0, r[i], s[i], t[i])
            pts[i, 0] = pnt.x() * self.modelScale
            pts[i, 1] = pnt.y() * self.modelScale
            pts[i, 2] = pnt.z() * self.modelScale
        # 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, r, s, t)
        # 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 
[docs]
    def setDesignVars(self, dvDict):
        """
        Standard routine for setting design variables from a design
        variable dictionary.
        Parameters
        ----------
        dvDict : dict
            Dictionary of design variables. The keys of the dictionary
            must correspond to the design variable names. Any
            additional keys in the dv-dictionary are simply ignored.
        """
        if self.useComposite:
            dvDict = self.mapXDictToDVGeo(dvDict)
        # Just dump in the values
        for key in dvDict:
            if key in self.DVs:
                self.DVs[key].value = dvDict[key]
        # we just need to set the design variables in the VSP model and we are done
        self._updateModel()
        # update the projected coordinates
        self._updateProjectedPts()
        # Flag all the pointSets as not being up to date:
        for pointSet in self.updated:
            self.updated[pointSet] = False
        # set the jacobian flag to false
        for ptName in self.pointSets:
            self.updatedJac[ptName] = False 
[docs]
    def update(self, ptSetName, config=None):
        """
        This is the main routine for returning coordinates that have been
        updated by design variables. Multiple configs are not
        supported.
        Parameters
        ----------
        ptSetName : str
            Name of point-set to return. This must match ones of the
            given in an :func:`addPointSet()` call.
        config : str or list
            Inactive parameter for DVGeometryVSP.
            See the same method in DVGeometry.py for its normal use.
        """
        # new cfd point coordinates are the updated projections minus the offset
        newPts = self.pointSets[ptSetName].pts - self.pointSets[ptSetName].offset
        # Finally flag this pointSet as being up to date:
        self.updated[ptSetName] = True
        return newPts 
[docs]
    def writeVSPFile(self, fileName, exportSet=0):
        """
        Take the current design and write a new VSP file
        Parameters
        ----------
        fileName : str
            Name of the output VSP file
        exportSet : int
            optional input parameter to select an export set in VSP
        """
        self.vspModel.WriteVSPFile(fileName, exportSet) 
[docs]
    def getNDV(self):
        """
        Return the number of DVs
        Returns
        -------
        len(self.DVs) : int
            number of design variables
        """
        return len(self.DVs) 
[docs]
    def totalSensitivity(self, dIdpt, ptSetName, comm=None, config=None):
        r"""
        This function computes sensitivity information.
        Specifically, it computes the following:
        :math:`\frac{dX_{pt}}{dX_{DV}}^T \frac{dI}{d_{pt}}`
        Parameters
        ----------
        dIdpt : array of size (Npt, 3) or (N, Npt, 3)
            This is the total derivative of the objective or function
            of interest with respect to the coordinates in
            'ptSetName'. This can be a single array of size (Npt, 3)
            **or** a group of N vectors of size (Npt, 3, N). If you
            have many to do, it is faster to do many at once.
        ptSetName : str
            The name of set of points we are dealing with
        comm : MPI.IntraComm
            The communicator to use to reduce the final derivative. If
            comm is None, no reduction takes place.
        Returns
        -------
        dIdxDict : dict
            The dictionary containing the derivatives, suitable for
            pyOptSparse
        """
        # We may not have set the variables so the surf jac might not be computed.
        if self.pointSets[ptSetName].jac is None:
            # in this case, we updated our pts when we added our pointset,
            # therefore the reference pts are up to date.
            self._computeSurfJacobian()
        # if the jacobian for this pointset is not up to date
        # update all the points
        if not self.updatedJac[ptSetName]:
            self._computeSurfJacobian()
        # The following code computes the final sensitivity product:
        #
        #        T       T
        #   pXpt     pI
        #  ------  ------
        #   pXdv    pXpt
        #
        # Where I is the objective, Xpt are the externally coordinates
        # supplied in addPointSet
        # Make dIdpt at least 3D
        if len(dIdpt.shape) == 2:
            dIdpt = np.array([dIdpt])
        # reshape the dIdpt array from [N] * [nPt] * [3] to  [N] * [nPt*3]
        dIdpt = dIdpt.reshape((dIdpt.shape[0], dIdpt.shape[1] * 3))
        # jacobian of the projected points
        jac = self.pointSets[ptSetName].jac
        # local product
        dIdxT_local = jac.T.dot(dIdpt.T)
        # take the transpose to get the jacobian itself dI/dx
        dIdx_local = dIdxT_local.T
        # sum the results if we are provided a comm
        if comm:
            dIdx = comm.allreduce(dIdx_local, op=MPI.SUM)
        else:
            dIdx = dIdx_local
        if self.useComposite:
            dIdx = self.mapSensToComp(dIdx)
            dIdxDict = self.convertSensitivityToDict(dIdx, useCompositeNames=True)
        else:
            # Now convert to dict:
            dIdxDict = {}
            i = 0
            for dvName in self.DVs:
                arr = np.array(dIdx[:, i]).T
                dIdxDict[dvName] = arr.reshape(arr.shape[0], 1)
                i += 1
        return dIdxDict 
[docs]
    def totalSensitivityProd(self, vec, ptSetName, comm=None, config=None):
        r"""
        This function computes sensitivity information.
        Specifically, it computes the following:
        :math:`\frac{dX_{pt}}{dX_{DV}} \ vec`
        This is useful for forward AD mode.
        Parameters
        ----------
        vec : dictionary whose keys are the design variable names, and whose
              values are the derivative seeds of the corresponding design variable.
        ptSetName : str
            The name of set of points we are dealing with
        comm : MPI.IntraComm
            inactive parameter, this has no effect on the final result
            because with this method, the reduction is performed externally
        Returns
        -------
        xsdot : array (Nx3) -> Array with derivative seeds of the surface nodes.
        """
        # We may not have set the variables so the surf jac might not be computed.
        if self.pointSets[ptSetName].jac is None:
            # in this case, we updated our pts when we added our pointset,
            # therefore the reference pts are up to date.
            self._computeSurfJacobian()
        # if the jacobian for this pointset is not up to date
        # update all the points
        if not self.updatedJac[ptSetName]:
            self._computeSurfJacobian()
        # vector that has all the derivative seeds of the design vars
        newvec = np.zeros(self.getNDV())
        # populate newvec
        for i, dv in enumerate(self.DVs):
            if dv in vec:
                newvec[i] = vec[dv]
        # we need to multiply with the surface jacobian
        dPt = self.pointSets[ptSetName].jac.dot(newvec)
        return dPt 
[docs]
    def totalSensitivityTransProd(self, dIdpt, ptSetName, comm=None, config=None):
        r"""
        This function computes sensitivity information.
        Specifically, it computes the following:
        :math:`\frac{dX_{pt}}{dX_{DV}}^T \frac{dI}{d_{pt}}`
        Parameters
        ----------
        dIdpt : array of size (Npt, 3) or (N, Npt, 3)
            This is the total derivative of the objective or function
            of interest with respect to the coordinates in
            'ptSetName'. This can be a single array of size (Npt, 3)
            **or** a group of N vectors of size (Npt, 3, N). If you
            have many to do, it is faster to do many at once.
        ptSetName : str
            The name of set of points we are dealing with
        comm : MPI.IntraComm
            The communicator to use to reduce the final derivative. If
            comm is None, no reduction takes place.
        Returns
        -------
        dIdxDict : dic
            The dictionary containing the derivatives, suitable for
            pyOptSparse
        """
        # We may not have set the variables so the surf jac might not be computed.
        if self.pointSets[ptSetName].jac is None:
            # in this case, we updated our pts when we added our pointset,
            # therefore the reference pts are up to date.
            self._computeSurfJacobian()
        # if the jacobian for this pointset is not up to date
        # update all the points
        if not self.updatedJac[ptSetName]:
            self._computeSurfJacobian()
        # The following code computes the final sensitivity product:
        #
        #        T       T
        #   pXpt     pI
        #  ------  ------
        #   pXdv    pXpt
        #
        # Where I is the objective, Xpt are the externally coordinates
        # supplied in addPointSet
        # Make dIdpt at least 3D
        if len(dIdpt.shape) == 2:
            dIdpt = np.array([dIdpt])
        # reshape the dIdpt array from [N] * [nPt] * [3] to  [N] * [nPt*3]
        dIdpt = dIdpt.reshape((dIdpt.shape[0], dIdpt.shape[1] * 3))
        jac = self.pointSets[ptSetName].jac.copy()
        dIdxT_local = jac.T.dot(dIdpt.T)
        dIdx_local = dIdxT_local.T
        if comm:
            dIdx = comm.allreduce(dIdx_local, op=MPI.SUM)
        else:
            dIdx = dIdx_local
        # Now convert to dict:
        dIdxDict = {}
        i = 0
        for dvName in self.DVs:
            dIdxDict[dvName] = np.array(dIdx[:, i]).T
            i += 1
        return dIdxDict 
[docs]
    def addVariable(
        self, component, group, parm, value=None, lower=None, upper=None, scale=1.0, scaledStep=True, dh=1e-6
    ):
        """
        Add a design variable definition.
        Parameters
        ----------
        component : str
            Name of the VSP component
        group : str
            Name of the VSP group
        parm : str
            Name of the VSP parameter
        value : float or None
            The design variable. If this value is not supplied (None), then
            the current value in the VSP model will be queried and used
        lower : float or None
            Lower bound for the design variable. Use None for no lower bound
        upper : float or None
            Upper bound for the design variable. Use None for no upper bound
        scale : float
            Scale factor
        scaledStep : bool
            Flag to use a scaled step sized based on the initial value of the
            variable. It will remain constant thereafter.
        dh : float
            Step size. When scaledStep is True, the actual step is dh*value. Otherwise
            this actual step is used.
        """
        container_id = self.vspModel.FindContainer(component, 0)
        if container_id == "":
            raise Error("Bad component for DV: %s" % component)
        parm_id = self.vspModel.FindParm(container_id, parm, group)
        if parm_id == "":
            raise Error(f"Bad group or parm: {component} {group} {parm}")
        # Now we know the parmID is ok. So we just get the value
        val = self.vspModel.GetParmVal(parm_id)
        dvName = f"{component}:{group}:{parm}"
        if value is None:
            value = val
        if scaledStep:
            dh = dh * value
            if value == 0:
                raise Error(
                    "Initial value is exactly 0. scaledStep option cannot be used"
                    "Specify an explicit dh with scaledStep=False"
                )
        self.DVs[dvName] = vspDV(parm_id, dvName, component, group, parm, value, lower, upper, scale, dh) 
[docs]
    def printDesignVariables(self):
        """
        Print a formatted list of design variables to the screen
        """
        print("-" * 85)
        print("{:>30}{:>20}{:>20}{:>15}".format("Component", "Group", "Parm", "Value"))
        print("-" * 85)
        for dvName in self.DVs:
            DV = self.DVs[dvName]
            print(f"{DV.component:>30}{DV.group:>20}{DV.parm:>20}{float(DV.value):15g}") 
[docs]
    def createDesignFile(self, fileName):
        """
        Take the current set of design variables and create a .des file
        Parameters
        ----------
        fileName : str
            name of the output .des file
        """
        f = open(fileName, "w")
        f.write("%d\n" % len(self.DVs))
        for dvName in self.DVs:
            DV = self.DVs[dvName]
            f.write(f"{DV.parmID}:{DV.component}:{DV.group}:{DV.parm}:{float(DV.value):20.15g}\n")
        f.close() 
[docs]
    def writePlot3D(self, fileName, exportSet=0):
        """
        Write the current design to Plot3D file
        Parameters
        ----------
        fileName : str
            name of the output Plot3D file
        exportSet : int
            optional argument to specify the export set in VSP
        """
        for dvName in self.DVs:
            DV = self.DVs[dvName]
            # We use float here since sometimes pyOptsparse will give
            # stupid numpy zero-dimensional arrays, which swig does
            # not like.
            self.vspModel.SetParmVal(DV.parmID, float(DV.value))
        self.vspModel.Update()
        # First set the export flag for exportSet to False for everyone
        for comp in self.allComps:
            self.vspModel.SetSetFlag(comp, exportSet, False)
        for comp in self.allComps:
            # Check if this one is in our list:
            compName = self.vspModel.GetContainerName(comp)
            if compName in self.compNames:
                self.vspModel.SetSetFlag(comp, exportSet, True)
                self.exportComps.append(compName)
        # Write the export file.
        self.vspModel.ExportFile(fileName, exportSet, openvsp.EXPORT_PLOT3D) 
    # ----------------------------------------------------------------------- #
    #      THE REMAINDER OF THE FUNCTIONS NEED NOT BE CALLED BY THE USER      #
    # ----------------------------------------------------------------------- #
    def _updateModel(self):
        """
        Set each of the DVs. We have the parmID stored so it's easy.
        """
        for dvName in self.DVs:
            DV = self.DVs[dvName]
            # for angle parameters, vsp only takes in degrees between -180 and +180,
            # which creates an unnecessary discontinuity at +-180.
            # to fix this, we take the mod of the value and set it to the correct range
            # that is allowed by VSP. Because all of the FD jacobian routine also goes
            # through here to update the model, we effectively maintain consistency
            if "angle" in DV.parm.lower():
                # set this new value separately to leave the DV.value itself untouched
                new_value = ((DV.value + 180.0) % 360.0) - 180.0
                self.vspModel.SetParmVal(DV.parmID, float(new_value))
            else:
                # We use float here since sometimes pyOptsparse will give
                # numpy zero-dimensional arrays, which swig does not like
                self.vspModel.SetParmValUpdate(DV.parmID, float(DV.value))
        # update the model
        self.vspModel.Update()
    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
            r = self.pointSets[ptSetName].r
            s = self.pointSets[ptSetName].s
            t = self.pointSets[ptSetName].t
            # 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 = self.vspModel.CompPntRST(self.allComps[geom[i]], 0, r[i], s[i], t[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.modelScale
            # set the updated coordinates
            self.pointSets[ptSetName].pts = newPts
    def _getBBox(self, comp):
        """
        This function computes the bounding box of the component. We add some buffer on each
        direction because we will use this bbox to determine which components to project points
        while adding point sets.
        """
        # initialize the array
        bbox = np.zeros((3, 2))
        # we need to get the number of main surfaces on this geometry
        nSurf = self.vspModel.GetNumMainSurfs(comp)
        nuv = self.bboxuv.shape[0]
        # allocate the arrays
        nodes = np.zeros((nSurf * nuv, 3))
        # loop over the surfaces
        for iSurf in range(nSurf):
            offset = iSurf * nuv
            # evaluate the points
            ptVec = self.vspModel.CompVecPnt01(comp, iSurf, self.bboxuv[:, 0], self.bboxuv[:, 1])
            # now extract the coordinates from the vec3dvec...sigh...
            for i in range(nuv):
                nodes[offset + i, :] = (ptVec[i].x(), ptVec[i].y(), ptVec[i].z())
        # get the min/max values of the coordinates
        for i in range(3):
            # this might be faster if we specify row/column major
            bbox[i, 0] = nodes[:, i].min()
            bbox[i, 1] = nodes[:, i].max()
        # finally scale the bounding box and return
        bbox *= self.modelScale
        # also give some offset on all directions
        bbox[:, 0] -= 0.1
        bbox[:, 1] += 0.1
        return bbox.copy()
    def _getuv(self):
        """
        Creates a uniform array of u-v combinations so that we can build a quad mesh ourselves.
        """
        # we need to sample the geometry, just do uniformly now
        nu = 20
        nv = 20
        # define the points on the parametric domain to sample
        ul = np.linspace(0, 1, nu + 1)
        vl = np.linspace(0, 1, nv + 1)
        uu, vv = np.meshgrid(ul, vl)
        uu = uu.flatten()
        vv = vv.flatten()
        # now create a concentrated uv array
        uv = np.dstack((uu, vv)).squeeze()
        return uv.copy()
    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
        rl = np.zeros(0)
        sl = np.zeros(0)
        tl = 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
            rl = np.concatenate((rl, self.pointSets[ptSetName].r))
            sl = np.concatenate((sl, self.pointSets[ptSetName].s))
            tl = np.concatenate((tl, self.pointSets[ptSetName].t))
            gl = np.concatenate((gl, self.pointSets[ptSetName].geom))
        # now figure out which proc has how many points.
        sizes = np.array(self.comm.allgather(len(rl)), 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(rl) * 3)
        # create the arrays to receive the global info
        rg = np.zeros(nptsg)
        sg = np.zeros(nptsg)
        tg = 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([rl, len(rl)], [rg, sizes, disp, MPI.DOUBLE])
        self.comm.Allgatherv([sl, len(sl)], [sg, sizes, disp, MPI.DOUBLE])
        self.comm.Allgatherv([tl, len(tl)], [tg, 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 = self.vspModel.CompPntRST(self.allComps[gg[j]], 0, rg[j], sg[j], tg[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._updateModel()
                t12 = time.time()
                tvsp += t12 - t11
                t11 = time.time()
                # evaluate the points
                for j in range(nptsg):
                    pnt = self.vspModel.CompPntRST(self.allComps[gg[j]], 0, rg[j], sg[j], tg[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.modelScale
        # Now, we have perturbed points on each proc that perturbed a DV
        # reset the model.
        t11 = time.time()
        self._updateModel()
        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 _getQuads(self):
        # build the quad mesh using the internal vsp geometry
        nSurf = len(self.allComps)
        pts = np.zeros((0, 3))
        conn = np.zeros((0, 4), dtype="intc")
        sizes = np.zeros((nSurf, 2), "intc")
        cumSizes = np.zeros(nSurf + 1, "intc")
        uv = []  # this will hold tessalation points
        offset = 0
        gind = 0
        for geom in self.allComps:
            # get uv tesselation
            utess, wtess = self.vspModel.GetUWTess01(geom, 0)
            # check if these values are good, otherwise, do it yourself!
            # save these values
            uv.append([np.array(utess), np.array(wtess)])
            nu = len(utess)
            nv = len(wtess)
            nElem = (nu - 1) * (nv - 1)
            # get u,v combinations of nodes
            uu, vv = np.meshgrid(utess, wtess)
            utess = uu.flatten()
            wtess = vv.flatten()
            # get the points
            ptvec = self.vspModel.CompVecPnt01(geom, 0, utess, wtess)
            # number of nodes for this geometry
            curSize = len(ptvec)
            # initialize coordinate and connectivity arrays
            compPts = np.zeros((curSize, 3))
            compConn = np.zeros((nElem, 4), dtype="intc")
            # get the coordinates of the points
            for i in range(curSize):
                compPts[i, :] = (ptvec[i].x(), ptvec[i].y(), ptvec[i].z())
            # build connectivity array
            k = 0
            for j in range(nv - 1):
                for i in range(nu - 1):
                    compConn[k, 0] = j * nu + i
                    compConn[k, 1] = j * nu + i + 1
                    compConn[k, 2] = (j + 1) * nu + i + 1
                    compConn[k, 3] = (j + 1) * nu + i
                    k += 1
            # apply the offset to the connectivities
            compConn += offset
            # stack the results
            pts = np.vstack((pts, compPts))
            conn = np.vstack((conn, compConn))
            # number of u and v point count
            sizes[gind, :] = (nu, nv)
            # cumilative number of points
            cumSizes[gind + 1] = cumSizes[gind] + curSize
            # increment the offset
            offset += curSize
            # increment geometry index
            gind += 1
        # finally, scale the points and save the data
        self.pts0 = pts * self.modelScale
        self.conn = conn
        self.sizes = sizes
        self.cumSizes = cumSizes
        self.uv = uv 
class PointSet:
    """Internal class for storing the projection details of each pointset"""
    def __init__(self, points, pts, geom, r, s, t):
        self.points = points
        self.pts = pts
        self.geom = geom
        self.r = r
        self.s = s
        self.t = t
        self.offset = self.pts - self.points
        self.nPts = len(self.pts)
        self.jac = None