def setUp(self):
        self.curDir = os.path.abspath(os.path.dirname(__file__))
        self.datFile = os.path.join(self.curDir, "naca2412.dat")
        self.rng = np.random.default_rng(1)
        self.comm = MPI.COMM_WORLD
        if self.dvName in ["upper", "lower"]:
            numCST = self.dvNum
        else:
            numCST = 4
        self.DVGeo = DVGeometryCST(self.datFile,
                                   comm=self.comm,
                                   isComplex=True,
                                   numCST=numCST)

        # Read in airfoil coordinates (use NACA 2412)
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros(
            (coords.shape[0], 1))))  # z-coordinates
        self.coords = coords.astype(complex)
        idxLE = np.argmin(coords[:, 0])
        self.idxUpper = np.arange(0, idxLE)
        self.idxLower = np.arange(idxLE, coords.shape[0])
        self.thickTE = coords[0, 1] - coords[-1, 1]
        self.ptName = "pt"

        self.sensTol = 1e-10
        self.coordTol = 1e-10
        self.CS_delta = 1e-200
    def test_addPointSet_bluntTE(
            self):  # includes a blunt trailing edge with points along it
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        nPointsTE = 6  # total points on the trailing edge
        pointsTE = np.ones((nPointsTE - 2, 3), dtype=float)
        pointsTE[:, 2] = 0  # z coordinates are zero
        pointsTE[:, 1] = np.linspace(coords[-1, 1] + 1e-4, coords[0, 1] - 1e-4,
                                     pointsTE.shape[0])
        coords = np.vstack((coords, pointsTE))
        idxLE = np.argmin(coords[:, 0])
        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        idxUpper = np.arange(1, idxLE + self.LEUpper)
        idxLower = np.arange(idxLE + self.LEUpper,
                             coords.shape[0] - nPointsTE + 2 - 1)
        thickTE = coords[0, 1] - coords[coords.shape[0] - nPointsTE + 1, 1]

        self.DVGeo.addPointSet(coords, "test")

        # Arrays are short so this is fast enough
        for idx in idxUpper:
            self.assertIn(idx, self.DVGeo.points["test"]["upper"])
        for idx in idxLower:
            self.assertIn(idx, self.DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE,
                                self.DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), self.DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), self.DVGeo.points["test"]["xMax"])
    def test_addPointSet_closed(self):
        curDir = os.path.abspath(os.path.dirname(__file__))
        datFile = os.path.join(curDir, "naca0012_closed.dat")
        comm = MPI.COMM_WORLD
        DVGeo = DVGeometryCST(datFile, comm=comm)

        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0]) + 1

        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        idxUpper = np.arange(1, idxLE)
        idxLower = np.arange(idxLE, coords.shape[0] - 2)
        thickTE = coords[0, 1] - coords[-2, 1]

        DVGeo.addPointSet(coords, "test")

        # Arrays are short so this is fast enough
        for idx in idxUpper:
            self.assertIn(idx, DVGeo.points["test"]["upper"])
        for idx in idxLower:
            self.assertIn(idx, DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE, DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), DVGeo.points["test"]["xMax"])
        self.assertFalse(DVGeo.sharp)
    def test_addPointSet_randomized(self):
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0])
        idxUpper = np.arange(0, idxLE + self.LEUpper)
        idxLower = np.arange(idxLE + self.LEUpper, coords.shape[0])
        thickTE = coords[0, 1] - coords[-1, 1]

        # Randomize the index order (do indices so we can track where they end up)
        rng = np.random.default_rng(1)
        # Maps from the original index to the new one (e.g., the first value is the index the first coordinate ends up at)
        idxShuffle = np.arange(0, coords.shape[0])
        rng.shuffle(idxShuffle)
        coordsRand = np.zeros(coords.shape)
        coordsRand[idxShuffle, :] = coords
        idxUpperRand = np.sort(idxShuffle[idxUpper])
        idxLowerRand = np.sort(idxShuffle[idxLower])

        self.DVGeo.addPointSet(coordsRand, "test")

        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        # Arrays are short so this is fast enough
        for idx in idxUpperRand:
            if idx != idxShuffle[0]:
                self.assertIn(idx, self.DVGeo.points["test"]["upper"])
        for idx in idxLowerRand:
            if idx != idxShuffle[-1]:
                self.assertIn(idx, self.DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE,
                                self.DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), self.DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), self.DVGeo.points["test"]["xMax"])
    def test_fitCST(self):
        """Test the CST parameter fitting"""
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(os.path.join(self.curDir, self.fName))
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0])
        idxUpper = np.arange(0, idxLE + self.LEUpper)
        idxLower = np.arange(idxLE + self.LEUpper, coords.shape[0])
        yTE = coords[0, 1]
        N1 = 0.5
        N2 = 1.0

        for nCST in range(2, 10):
            # Fit the CST parameters and then compute the coordinates
            # with those parameters and check that it's close
            upperCST = DVGeometryCST.computeCSTfromCoords(coords[idxUpper, 0],
                                                          coords[idxUpper, 1],
                                                          nCST,
                                                          N1=N1,
                                                          N2=N2)
            lowerCST = DVGeometryCST.computeCSTfromCoords(coords[idxLower, 0],
                                                          coords[idxLower, 1],
                                                          nCST,
                                                          N1=N1,
                                                          N2=N2)
            fitCoordsUpper = DVGeometryCST.computeCSTCoordinates(
                coords[idxUpper, 0], N1, N2, upperCST, yTE)
            fitCoordsLower = DVGeometryCST.computeCSTCoordinates(
                coords[idxLower, 0], N1, N2, lowerCST, -yTE)

            # Loosen the tolerances for the challenging e63 airfoil
            if self.fName == "e63.dat":
                if nCST < 4:
                    atol = 1e-1
                    rtol = 1.0
                else:
                    atol = 1e-2
                    rtol = 6e-1
            else:
                atol = 1e-3
                rtol = 1e-1

            np.testing.assert_allclose(fitCoordsUpper,
                                       coords[idxUpper, 1],
                                       atol=atol,
                                       rtol=rtol)
            np.testing.assert_allclose(fitCoordsLower,
                                       coords[idxLower, 1],
                                       atol=atol,
                                       rtol=rtol)
    def test_addPointSet_sorted(self):
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0])

        isUpper = np.zeros(coords.shape[0])
        isUpper[:idxLE + self.LEUpper] = 1
        isUpper = isUpper == 1
        isLower = np.logical_not(isUpper)

        thickTE = coords[0, 1] - coords[-1, 1]

        # Divide up the points among the procs (mostly evenly, but not quite to check the harder case)
        if self.N_PROCS == 1:
            nPerProc = coords.shape[0]
        else:
            nPerProc = int(coords.shape[0] // (self.N_PROCS - 0.5))
        rank = self.comm.rank
        if self.comm.rank < self.comm.size - 1:  # all but last proc takes nPerProc elements
            self.DVGeo.addPointSet(
                coords[rank * nPerProc:(rank + 1) * nPerProc, :], "test")
            idxUpper = np.where(isUpper[rank * nPerProc:(rank + 1) *
                                        nPerProc])[0]
            idxLower = np.where(isLower[rank * nPerProc:(rank + 1) *
                                        nPerProc])[0]
        else:
            self.DVGeo.addPointSet(coords[rank * nPerProc:, :], "test")
            idxUpper = np.where(isUpper[rank * nPerProc:])[0]
            idxLower = np.where(isLower[rank * nPerProc:])[0]

        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        # Arrays are short so this is fast enough
        for idx in idxUpper:
            if idx != 0:
                self.assertIn(idx, self.DVGeo.points["test"]["upper"])
        for idx in idxLower:
            if idx != coords.shape[0] - 1:
                self.assertIn(idx, self.DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE,
                                self.DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), self.DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), self.DVGeo.points["test"]["xMax"])
    def test_addPointSet_sorted(self):
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0])
        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        idxUpper = np.arange(1, idxLE + self.LEUpper)
        idxLower = np.arange(idxLE + self.LEUpper, coords.shape[0] - 1)
        thickTE = coords[0, 1] - coords[-1, 1]

        self.DVGeo.addPointSet(coords, "test")

        # Arrays are short so this is fast enough
        for idx in idxUpper:
            self.assertIn(idx, self.DVGeo.points["test"]["upper"])
        for idx in idxLower:
            self.assertIn(idx, self.DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE,
                                self.DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), self.DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), self.DVGeo.points["test"]["xMax"])
    def test_addPointSet_randomized(self):
        # Read in airfoil coordinates to test with and split up the surfaces
        coords = readCoordFile(self.datFile)
        coords = np.hstack((coords, np.zeros((coords.shape[0], 1))))
        idxLE = np.argmin(coords[:, 0])

        isUpper = np.zeros(coords.shape[0])
        isUpper[:idxLE + self.LEUpper] = 1
        isUpper = isUpper == 1
        isLower = np.logical_not(isUpper)

        thickTE = coords[0, 1] - coords[-1, 1]

        # Randomize the index order (do indices so we can track where they end up)
        rng = np.random.default_rng(1)
        # Maps from the original index to the new one (e.g., the first value is the index the first coordinate ends up at)
        idxShuffle = np.arange(0, coords.shape[0])
        rng.shuffle(idxShuffle)
        # idxShuffle = (idxShuffle + 10) % coords.shape[0]
        coordsRand = np.zeros(coords.shape)
        isUpperRand = np.full(isUpper.shape, False)
        isLowerRand = np.full(isLower.shape, False)
        coordsRand[idxShuffle, :] = coords
        isUpperRand[idxShuffle] = isUpper
        isLowerRand[idxShuffle] = isLower

        # Maps from the shuffled index to the original one (e.g., the first value is
        # the original index of the first value in the shuffled array)
        idxInverseShuffle = np.zeros(idxShuffle.shape[0])
        idxInverseShuffle[idxShuffle] = np.arange(0, coords.shape[0])

        # Divide up the points among the procs (mostly evenly, but not quite to check the harder case)
        nPerProc = int(coordsRand.shape[0] // 3.5)
        rank = self.comm.rank
        if self.comm.rank < self.comm.size - 1:  # all but last proc takes nPerProc elements
            self.DVGeo.addPointSet(coordsRand[rank * nPerProc:(rank + 1) *
                                              nPerProc, :],
                                   "test",
                                   rank=self.comm.rank)
            idxUpper = np.where(isUpperRand[rank * nPerProc:(rank + 1) *
                                            nPerProc])[0]
            idxLower = np.where(isLowerRand[rank * nPerProc:(rank + 1) *
                                            nPerProc])[0]

            # Figure out the local indices where the first and last coordinates in the dat file ended up
            idxStart = np.argwhere(
                0 == idxInverseShuffle[rank * nPerProc:(rank + 1) * nPerProc])
            idxEnd = np.argwhere(
                coords.shape[0] == idxInverseShuffle[rank *
                                                     nPerProc:(rank + 1) *
                                                     nPerProc])
        else:
            self.DVGeo.addPointSet(coordsRand[rank * nPerProc:, :],
                                   "test",
                                   rank=self.comm.rank)
            idxUpper = np.where(isUpperRand[rank * nPerProc:])[0]
            idxLower = np.where(isLowerRand[rank * nPerProc:])[0]

            # Figure out the local indices where the first and last coordinates in the dat file ended up
            idxStart = np.argwhere(0 == idxInverseShuffle[rank * nPerProc:])
            idxEnd = np.argwhere(
                coords.shape[0] == idxInverseShuffle[rank * nPerProc:])

        # Turn the single element array to a number or None if the first
        # or last points aren't in this partition
        if idxStart:
            idxStart = idxStart.item()
        else:
            idxStart = None
        if idxEnd:
            idxEnd = idxEnd.item()
        else:
            idxEnd = None

        # Don't include the points at the corners of the trailing edge because it's not guaranteed
        # that they'll be included in the upper and lower surface (which is ok)
        # Arrays are short so this is fast enough
        for idx in idxUpper:
            if idx != idxStart:
                self.assertIn(idx, self.DVGeo.points["test"]["upper"])
        for idx in idxLower:
            if idx != idxEnd:
                self.assertIn(idx, self.DVGeo.points["test"]["lower"])
        np.testing.assert_equal(thickTE,
                                self.DVGeo.points["test"]["thicknessTE"])
        self.assertEqual(min(coords[:, 0]), self.DVGeo.points["test"]["xMin"])
        self.assertEqual(max(coords[:, 0]), self.DVGeo.points["test"]["xMax"])