Example #1
0
    def get_vortex_influence(self, points):
        """Determines the velocity vector induced by this edge at arbitrary
        points, assuming a horseshoe vortex is shed from this edge.

        Parameters
        ----------
        points : ndarray
            An array of points where the first index is the point index and 
            the second index is the coordinate.

        Returns
        -------
        ndarray
            The velocity vector induced at each point.
        """

        # Determine displacement vectors
        r0 = points - self.vertices[0, :]
        r1 = points - self.vertices[1, :]

        # Determine displacement vector magnitudes
        r0_mag = vec_norm(r0)
        r1_mag = vec_norm(r1)

        # Calculate influence of bound segment
        with np.errstate(divide='ignore'):
            d = (np.pi * r0_mag * r1_mag *
                 (r0_mag * r1_mag + vec_inner(r0, r1)))
            n = 0.25 * ((r0_mag + r1_mag) / d)
            n = np.nan_to_num(n, copy=False)
            return n[:, np.newaxis] * vec_cross(r0, r1)
Example #2
0
    def get_ring_influence(self, points):
        """Determines the velocity vector induced by this panel at arbitrary
        points, assuming a vortex ring (0th order) model and a unit positive
        vortex strength.

        Parameters
        ----------
        points : ndarray
            An array of points where the first index is the point index and 
            the second index is the coordinate.

        Returns
        -------
        ndarray
            The velocity vector induced at each point.
        """

        # Determine displacement vectors
        r = points[np.newaxis, :, :] - self.vertices[:, np.newaxis, :]
        r_mag = vec_norm(r)

        # Calculate influence
        v = np.zeros_like(points)
        with np.errstate(divide='ignore'):
            for i in range(self.N):
                d = (r_mag[i - 1] * r_mag[i] *
                     (r_mag[i - 1] * r_mag[i] + vec_inner(r[i - 1], r[i])))
                n = (r_mag[i - 1] + r_mag[i]) / d
                n = np.nan_to_num(n, copy=False)
                v += vec_cross(r[i - 1], r[i]) * n[:, np.newaxis]

        return 0.25 / np.pi * v
Example #3
0
    def _get_filament_influences(self, points):
        # Determines the unit vortex influence from the wake filaments on the given points

        # Determine displacement vectors: first index is point, second is filament, third is segment, fourth is vector component
        if self._end_infinite:
            r0 = points[:, np.newaxis, np.newaxis, :] - self._vertices[
                np.newaxis, :, :
                -2, :]  # Don't add the last segment at this point
            r1 = points[:, np.newaxis,
                        np.newaxis, :] - self._vertices[np.newaxis, :, 1:-1, :]
        else:
            r0 = points[:, np.newaxis,
                        np.newaxis, :] - self._vertices[np.newaxis, :, :-1, :]
            r1 = points[:, np.newaxis,
                        np.newaxis, :] - self._vertices[np.newaxis, :, 1:, :]

        # Determine displacement vector magnitudes
        r0_mag = vec_norm(r0)
        r1_mag = vec_norm(r1)

        # Calculate influence of each segment
        inf = np.sum(
            ((r0_mag + r1_mag) /
             (r0_mag * r1_mag *
              (r0_mag * r1_mag + vec_inner(r0, r1))))[:, :, :, np.newaxis] *
            vec_cross(r0, r1),
            axis=2)

        # Add influence of last segment, if needed
        if self._end_infinite:

            # Determine displacement vector magnitudes
            r = r1[:, :, -1, :]
            r_mag = vec_norm(r)
            u = self._vertices[:, -1, :] - self._vertices[:, -2, :]
            u /= vec_norm(u)[:, np.newaxis]

            # Calculate influence
            inf += vec_cross(u[np.newaxis, :, :], r) / (
                r_mag *
                (r_mag - vec_inner(u[np.newaxis, :, :], r)))[:, :, np.newaxis]

        return 0.25 / np.pi * np.nan_to_num(inf)
Example #4
0
    def _get_filament_influences(self, points):
        # Determines the unit vortex influence from the wake filaments on the given points

        # Determine displacement vectors: first index is point, second is filament, third is segment, fourth is vector component
        r0 = points[:, np.newaxis, np.newaxis, :] - self._vertices[
            np.newaxis, :, :self.N_segments, :]
        r1 = points[:, np.newaxis,
                    np.newaxis, :] - self._vertices[np.newaxis, :,
                                                    1:self.N_segments + 1, :]

        # Determine displacement vector magnitudes
        r0_mag = vec_norm(r0)
        r1_mag = vec_norm(r1)

        # Calculate influence of each segment
        inf = np.sum(
            ((r0_mag + r1_mag) /
             (r0_mag * r1_mag *
              (r0_mag * r1_mag + vec_inner(r0, r1))))[:, :, :, np.newaxis] *
            vec_cross(r0, r1),
            axis=2)

        return 0.25 / np.pi * np.nan_to_num(inf)
Example #5
0
    def get_edge_normals(self):
        """Calculates the vectors which point outward from the panel, in the (average) plane of the panel, normal to each edge.

        Returns
        -------
        ndarray
            Array of edge normal vectors.
        """

        # Get normal
        n = self._calc_normal()

        # Get edge tangents
        t = np.roll(self.vertices, -1, axis=0) - self.vertices

        # Get outward normals
        n_out = vec_cross(t, n)
        return n_out / vec_norm(n_out)[:, np.newaxis]
Example #6
0
    def set_filament_direction(self, v_inf, omega):
        """Resets the counter for determining how far along the wake has been solved. Does not update filament vertices (yeah it's a misnomer, but hey, consistency).

        Parameters
        ----------
        v_inf : ndarray
            Freestream velocity vector.

        omega : ndarray
            Angular rate vector.
        """

        # Get filament starting directions (required for offsetting the initial point to avoid infinite velocities)
        origins = self._vertices[:, 0, :]
        self._filament_dirs = v_inf[np.newaxis, :] - vec_cross(omega, origins)
        self._filament_dirs /= vec_norm(self._filament_dirs)[:, np.newaxis]

        # Reset number of segments which have been set
        self.N_segments = 0
Example #7
0
    def set_filament_direction(self, v_inf, omega):
        """Updates the initial direction of the vortex filaments based on the velocity params.

        Parameters
        ----------
        v_inf : ndarray
            Freestream velocity vector.

        omega : ndarray
            Angular rate vector.
        """

        # Determine directions
        origins = self._vertices[:, 0, :]
        self._filament_dirs = v_inf[np.newaxis, :] - vec_cross(omega, origins)
        self._filament_dirs /= vec_norm(self._filament_dirs)[:, np.newaxis]

        # Set vertices
        self._vertices = origins[:, np.newaxis, :] + np.linspace(
            0.0, self.N_segments * self.l, self.N_segments + 1
        )[np.newaxis, :, np.newaxis] * self._filament_dirs[:, np.newaxis, :]
Example #8
0
    def set_filament_direction(self, v_inf, omega):
        """Updates the direction of the vortex filaments based on the velocity params.

        Parameters
        ----------
        v_inf : ndarray
            Freestream velocity vector.

        omega : ndarray
            Angular rate vector.
        """

        # Freestream direction
        if self._type == "freestream":
            u = v_inf / norm(v_inf)
            self.filament_dirs[:] = u

        # Freestream with rotation
        elif self._type == "freestream_and_rotation":
            self.filament_dirs = v_inf[np.newaxis, :] - vec_cross(
                omega, self._vertices)
            self.filament_dirs /= vec_norm(self.filament_dirs)[:, np.newaxis]
Example #9
0
    def solve(self, **kwargs):
        """Solves the panel equations to determine the flow field around the mesh.

        Parameters
        ----------
        method : str, optional
            Method for computing the least-squares solution to the system of equations. May be 'direct' or 'svd'. 'direct' solves the equation (A*)Ax=(A*)b using a standard linear algebra solver. 'svd' solves the equation Ax=b in a least-squares sense using the singular value decomposition. 'direct' is much faster but may be susceptible to numerical error due to a poorly conditioned system. 'svd' is more reliable at producing a stable solution. Defaults to 'direct'.

        wake_iterations : int, optional
            How many times the shape of the wake should be updated and the flow resolved. Only used if the mesh has been set with a "full_streamline" or "relaxed" wake. For "marching_streamline" wakes, the number of iterations is equal to the number of filament segments in the wake and this setting is ignored. Defaults to 2.

        export_wake_series : bool, optional
            Whether to export a vtk of the solver results after each wake iteration. Only used if the mesh has been set with an iterative wake. Defaults to False.

        wake_series_title : str, optional
            Gives a common file name and location for the wake series export files. Each file will be stored as "<wake_series_title>_<iteration_number>.vtk". May include a file path. Required if "export_wake_series" is True.

        verbose : bool, optional

        Returns
        -------
        F : ndarray
            Force vector in mesh coordinates.

        M : ndarray
            Moment vector in mesh coordinates.
        """

        # Begin timer
        self._verbose = kwargs.get("verbose", False)

        # Get kwargs
        method = kwargs.get("method", "direct")
        dont_iterate_on_wake = not (
            isinstance(self._mesh.wake, VelocityRelaxedWake)
            or isinstance(self._mesh.wake, FullStreamlineWake)
            or isinstance(self._mesh.wake, MarchingStreamlineWake))

        # Non-iterative wake options
        if dont_iterate_on_wake:
            wake_iterations = 0
            export_wake_series = False

        # Iterative wake options
        else:

            # Number of iterations
            wake_iterations = kwargs.get("wake_iterations", 2)
            if isinstance(self._mesh.wake, MarchingStreamlineWake):
                wake_iterations = self._mesh.wake.N_segments_final

            # Wake series export
            export_wake_series = kwargs.get("export_wake_series", False)
            if export_wake_series:
                wake_series_title = kwargs.get("wake_series_title")
                if wake_series_title is None:
                    raise IOError(
                        "'wake_series_title' is required if 'export_wake_series' is true."
                    )

        # Iterate on wake
        for i in range(wake_iterations + 1):
            if self._verbose and not dont_iterate_on_wake:
                print("\nWake Iteration {0}/{1}".format(i, wake_iterations))
                print("========================")
            if self._verbose:
                print()
                start_time = time.time()
                print(
                    "    Solving singularity strengths (this may take a while)...",
                    flush=True,
                    end='')

            # Get wake influence matrix
            wake_influence_matrix = self._mesh.wake.get_influence_matrix(
                points=self._mesh.cp,
                u_inf=self._u_inf,
                omega=self._omega,
                N_panels=self._N_panels)

            # Specify A matrix
            A = np.zeros((self._N_panels + 1, self._N_panels))
            A[:-1] = np.einsum('ijk,ik->ij', self._panel_influence_matrix,
                               self._mesh.n)
            if not isinstance(wake_influence_matrix, float):
                A[:-1] += np.einsum('ijk,ik->ij', wake_influence_matrix,
                                    self._mesh.n)
            A[-1] = 1.0

            # Specify b vector
            b = np.zeros(self._N_panels + 1)
            b[:-1] = self._b

            # Direct method
            if method == 'direct':
                b = np.matmul(A.T, b[:, np.newaxis])
                A = np.matmul(A.T, A)
                self._mu = np.linalg.solve(A, b).flatten()

            # Singular value decomposition
            elif method == "svd":
                self._mu, res, rank, s_a = np.linalg.lstsq(A, b, rcond=None)

            # Clear up memory
            del A
            del b

            # Print computation results
            if self._verbose:
                print("Finished. Time: {0}".format(time.time() - start_time))
                print()
                print("    Solver Results:")
                print("        Sum of doublet strengths: {0}".format(
                    np.sum(self._mu)))
                try:
                    print("        Maximum residual magnitude: {0}".format(
                        np.max(np.abs(res))))
                    print("        Average residual magnitude: {0}".format(
                        np.average(np.abs(res))))
                    print("        Median residual magnitude: {0}".format(
                        np.median(np.abs(res))))
                    del res
                except:
                    pass

                if method == "svd":
                    print("        Rank of A matrix: {0}".format(rank))
                    print("        Max singular value of A: {0}".format(
                        np.max(s_a)))
                    print("        Min singular value of A: {0}".format(
                        np.min(s_a)))
                    del s_a

            if self._verbose:
                print()
                prog = OneLineProgress(
                    4, msg="    Calculating derived quantities")

            # Determine velocities at each control point induced by panels
            self._v = self._v_inf_and_rot + np.einsum(
                'ijk,j', self._panel_influence_matrix, self._mu)
            if self._verbose: prog.display()

            # Determine wake induced velocities
            self._v += np.sum(wake_influence_matrix *
                              self._mu[np.newaxis, :, np.newaxis],
                              axis=1)
            del wake_influence_matrix
            if self._verbose: prog.display()

            # Include doublet sheet principal value in the velocity
            self._v -= 0.5 * self._mesh.get_gradient(self._mu)
            if self._verbose: prog.display()

            # Determine coefficients of pressure
            V = vec_norm(self._v)
            self._C_P = 1.0 - (V * V) / self._V_inf**2
            if self._verbose: prog.display()

            # export vtk
            if export_wake_series:
                self.export_vtk(wake_series_title + "_{0}.vtk".format(i + 1))

            # Update wake
            if not dont_iterate_on_wake and i < wake_iterations:  # Don't update the wake if this is the last iteration
                self._mesh.wake.update(self.get_velocity_induced_by_body,
                                       self._mu, self._v_inf, self._omega,
                                       self._verbose)

        # Determine force acting on each panel
        self._dF = -(0.5 * self._rho * self._V_inf**2 * self._mesh.dA *
                     self._C_P)[:, np.newaxis] * self._mesh.n

        # Sum force components (doing it component by component allows numpy to employ a more stable addition scheme)
        self._F = np.zeros(3)
        self._F[0] = np.sum(self._dF[:, 0])
        self._F[1] = np.sum(self._dF[:, 1])
        self._F[2] = np.sum(self._dF[:, 2])

        # Determine moment contribution due to each panel
        self._dM = vec_cross(self._mesh.r_CG, self._dF)

        # Sum moment components
        self._M = np.zeros(3)
        self._M[0] = np.sum(self._dM[:, 0])
        self._M[1] = np.sum(self._dM[:, 1])
        self._M[2] = np.sum(self._dM[:, 2])

        # Set solved flag
        self._solved = True

        return self._F, self._M
Example #10
0
    def update(self, velocity_from_body, mu, v_inf, omega, verbose):
        """Updates the shape of the wake based on solved flow results.

        Parameters
        ----------
        velocity_from_body : callable
            Function which will return the velocity induced by the body at a given set of points.

        mu : ndarray
            Vector of doublet strengths.

        v_inf : ndarray
            Freestream velocity vector.

        omega : ndarray
            Angular rate vector.

        verbose : bool
        """

        # Update number of segments
        self.N_segments += 1

        if verbose:
            print()
            prog = OneLineProgress(
                self.N_segments + 1,
                msg="    Updating wake shape with {0} segments".format(
                    self.N_segments))

        # Initialize storage
        new_locs = np.zeros((self.N, self.N_segments, 3))

        # Get starting locations (offset slightly from origin to avoid singularities)
        curr_loc = self._vertices[:, 0, :] + self._filament_dirs * 0.01

        if verbose: prog.display()

        # Loop through filament segments (the first vertex never changes)
        next_loc = np.zeros((self.N, 3))
        for i in range(1, self.N_segments + 1):

            # Determine velocities at current point
            v0 = velocity_from_body(curr_loc) + v_inf[
                np.newaxis, :] - vec_cross(omega, curr_loc)
            v0 += self._get_velocity_from_other_filaments_and_edges(
                curr_loc, mu)

            # Guess of next location
            next_loc = curr_loc + self.l * v0 / vec_norm(v0)[:, np.newaxis]

            # Iteratively correct
            for j in range(self._corrector_iterations):

                # Velocities at next location
                v1 = velocity_from_body(next_loc) + v_inf[np.newaxis, :]
                v1 += self._get_velocity_from_other_filaments_and_edges(
                    next_loc, mu)

                # Correct location
                v_avg = 0.5 * (v0 + v1)
                next_loc = curr_loc + self.l * v_avg / vec_norm(
                    v_avg)[:, np.newaxis]

            # Store
            new_locs[:, i - 1, :] = np.copy(next_loc)

            # Move downstream
            curr_loc = np.copy(next_loc)

            if verbose: prog.display()

        # Store the new locations
        self._vertices[:, 1:self.N_segments + 1, :] = new_locs
Example #11
0
    def get_influence_matrix(self, **kwargs):
        """Create wake influence matrix; first index is the influenced panels, second is the influencing panel, third is the velocity component.

        Parameters
        ----------
        points : ndarray
            Array of points at which to calculate the influence.

        N_panels : int
            Number of panels in the mesh to which this wake belongs.

        Returns
        -------
        ndarray
            Trailing vortex influences.
        """

        # Get kwargs
        points = kwargs.get("points")

        # Initialize storage
        N = len(points)
        vortex_influence_matrix = np.zeros((N, kwargs["N_panels"], 3))

        # Get influence of edges
        for edge in self._kutta_edges:

            # Get indices of panels defining the edge
            p_ind = edge.panel_indices

            # Get infulence
            V = edge.get_vortex_influence(points)

            # Store
            vortex_influence_matrix[:, p_ind[0]] = -V
            vortex_influence_matrix[:, p_ind[1]] = V

        # Determine displacement vector magnitudes
        r = points[:, np.newaxis, :] - self._vertices[np.newaxis, :, :]
        r_mag = vec_norm(r)

        # Calculate influences
        V = 0.25 / np.pi * vec_cross(
            self.filament_dirs[np.newaxis, :, :],
            r) / (r_mag *
                  (r_mag - vec_inner(self.filament_dirs[np.newaxis, :, :], r))
                  )[:, :, np.newaxis]
        for i in range(self.N):

            # Add for outbound panels
            outbound_panels = self.outbound_panels[i]
            if len(outbound_panels) > 0:
                vortex_influence_matrix[:, outbound_panels[0], :] -= V[:, i, :]
                vortex_influence_matrix[:, outbound_panels[1], :] += V[:, i, :]

            # Add for inbound panels
            inbound_panels = self.inbound_panels[i]
            if len(inbound_panels) > 0:
                vortex_influence_matrix[:, inbound_panels[0], :] += V[:, i, :]
                vortex_influence_matrix[:, inbound_panels[1], :] -= V[:, i, :]

        return vortex_influence_matrix