Пример #1
0
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        if self._verbose:
            print()
            prog = OneLineProgress(self._N_panels,
                                   msg="Calculating panel influence matrix")

        # Create panel influence matrix; first index is the influenced panel, second is the influencing panel
        #N_processes = 8
        #with mp.Pool(processes=N_processes) as pool:
        #    N_per_process = self._N_panels//N_processes
        #    args = []
        #    for i in range(N_processes):
        #        if i<N_processes-1:
        #            panels = self._mesh.panels[i*N_per_process:(i+1)*N_per_process]
        #        else:
        #            panels = self._mesh.panels[i*N_per_process:]
        #        args.append((copy.deepcopy(panels), copy.deepcopy(self._mesh.cp)))

        #    res = pool.map(get_panel_influences, args)
        #    if self._verbose:
        #        prog.display()

        #self._panel_influence_matrix = np.concatenate(res, axis=1)
        #if self._verbose:
        #    prog.display()
        self._panel_influence_matrix = np.zeros(
            (self._N_panels, self._N_panels, 3))
        for i, panel in enumerate(self._mesh.panels):
            self._panel_influence_matrix[:, i] = panel.get_ring_influence(
                self._mesh.cp)
            if self._verbose:
                prog.display()
Пример #2
0
    def _initialize_kutta_search(self, **kwargs):
        # Sets up the Kutta edge search; does everything not dependent on the freestream vector; relies on an adjacency mapping already being created

        if self._verbose:
            print()
            prog = OneLineProgress(self.N,
                                   msg="Locating potential Kutta edges")

        # Get parameters
        theta_K = np.radians(kwargs.get("kutta_angle", 90.0))
        C_theta = np.cos(theta_K)
        self._check_freestream = kwargs.get("check_freestream", True)

        # Look for adjacent panels where the angle between their normals is greater than the Kutta angle
        self._potential_kutta_panels = []

        # Loop through possible combinations
        with np.errstate(invalid='ignore'):
            for i, panel_i in enumerate(self.panels):

                # Check abutting panels for Kutta angle
                for j in panel_i.abutting_panels:

                    # Don't repeat
                    if j <= i:
                        continue

                    # Check angle
                    if inner(self.n[i], self.n[j]) <= C_theta:
                        self._potential_kutta_panels.append([i, j])

                if self._verbose:
                    prog.display()
Пример #3
0
    def _determine_panel_vertex_mapping(self):
        # Creates a list of all unique vertices and maps each panel to those vertices

        if self._verbose:
            print()
            prog = OneLineProgress(self.N,
                                   msg="Determining panel->vertex mapping")

        # Collect vertices and panel vertex indices
        self._vertices = []
        self._panel_vertex_indices = [
        ]  # First index is the number of vertices, the rest are the vertex indices
        self._poly_list_size = 0

        # Loop through panels
        i = 0  # Index of last added vertex
        for panel in self.panels:

            # Initialize panel info
            if isinstance(panel, Tri):
                panel_info = [3]
                self._poly_list_size += 4
            elif isinstance(panel, Quad):
                panel_info = [4]
                self._poly_list_size += 5

            # Check if vertices are in the list
            for vertex in panel.vertices:
                ind = self._check_for_vertex(vertex, self._vertices)

                # Not in list
                if ind == -1:
                    self._vertices.append(vertex)
                    panel_info.append(i)
                    i += 1

                # In list
                else:
                    panel_info.append(ind)

            # Store panel info
            self._panel_vertex_indices.append(panel_info)
            if self._verbose:
                prog.display()

        self._vertices = np.array(
            self._vertices
        )  # Cannot do this for _panel_vertex_indices because the length of each list element is not necessarily the same
Пример #4
0
    def _set_up_lst_sq(self):
        # Determines the A matrix to least-squares estimation of the gradient. Must be called after kutta edges are determined.

        if self._verbose:
            print()
            prog = OneLineProgress(self.N,
                                   msg="Calculating least-squares matrices")

        # Initialize
        self.A_lsq = []

        # Loop through panels
        for i, panel in enumerate(self.panels):

            # Determine which neighbors to use
            if self._gradient_type == 'quad':
                neighbors = panel.second_abutting_panels_not_across_kutta_edge
            else:
                neighbors = panel.touching_panels_not_across_kutta_edge

            # Get centroids of neighboring panels in local panel coordinates
            dp = np.einsum('ij,kj->ki', panel.A_t,
                           self.cp[neighbors] - self.cp[i][np.newaxis, :])

            # Get basis functions
            dx = dp[:, 0][:, np.newaxis]
            dy = dp[:, 1][:, np.newaxis]

            # Assemble A matrix
            if self._gradient_type == 'quad':
                A = np.concatenate((dx**2, dy**2, dx * dy, dx, dy), axis=1)
            else:
                A = dp

            # Store
            self.A_lsq.append(A)

            if self._verbose:
                prog.display()
Пример #5
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
        """

        if verbose:
            print()
            prog = OneLineProgress(4, msg="    Updating wake shape")

        # Reorder vertices for computation
        points = self._vertices[:, 1:, :].reshape(
            (self.N * (self.N_segments), 3))

        # Get velocity from body and rotation
        v_ind = velocity_from_body(points) - vec_cross(omega, points)
        if verbose: prog.display()

        # Get velocity from wake elements
        v_ind += self._get_velocity_from_filaments_and_edges(points, mu)
        if verbose: prog.display()

        # Calculate time-stepping parameter
        U = norm(v_inf)
        u = v_inf / U
        dl = self._vertices[:, 1:, :] - self._vertices[:, 0, :][:,
                                                                np.newaxis, :]
        d = vec_inner(dl, u[np.newaxis, :])
        dt = self._K * d / U
        if verbose: prog.display()

        # Shift vertices
        self._vertices[:, 1:, :] += dt[:, :, np.newaxis] * v_ind.reshape(
            (self.N, self.N_segments, 3))
        if verbose: prog.display()
Пример #6
0
    def finalize_kutta_edge_search(self, u_inf):
        """Determines where the Kutta condition should exist based on previously located adjacent panels and the freestream velocity.

        Parameters
        ----------
        u_inf : ndarray
            Freestream velocity vector (direction of the oncoming flow).
        """

        # Initialize edge storage
        self._kutta_edges = []

        if len(self._potential_kutta_panels) > 0:

            if self._verbose:
                print()
                prog = OneLineProgress(len(self._potential_kutta_panels),
                                       msg="Finalizing Kutta edge locations")

            # Loop through previously determined possibilities
            for i, j in self._potential_kutta_panels:

                # Get panel objects
                panel_i = self.panels[i]
                panel_j = self.panels[j]

                # Find first vertex shared by panels
                found = False
                for ii, vi in enumerate(panel_i.vertices):
                    for jj, vj in enumerate(panel_j.vertices):

                        # Check distance
                        if norm(vi - vj) < 1e-10:

                            # Store first shared vertex
                            v0 = vi
                            ii0 = ii
                            jj0 = jj
                            found = True
                            break

                    if found:
                        break

                # Find second shared vertex; will be adjacent to first
                poss_i_vert = [ii0 - 1, ii0 + 1]
                poss_j_vert = [(jj0 + 1) % panel_j.N, jj0 - 1]
                for i_same_dir, (ii, jj) in enumerate(
                        zip(poss_i_vert, poss_j_vert)
                ):  # i_same_dir keeps track of if ii is still increasing
                    vi = panel_i.vertices[ii]
                    vj = panel_j.vertices[jj]

                    # Check distance
                    if norm(vi - vj) < 1e-10:

                        # See if we need to check the freestream condition
                        is_kutta_edge = False
                        if not self._check_freestream:
                            is_kutta_edge = True

                        # Get edge normals
                        else:
                            edge_normals_i = panel_i.get_edge_normals()
                            edge_normals_j = panel_j.get_edge_normals()

                            # Decide which edge to use
                            if i_same_dir:
                                i_edge = ii0
                                j_edge = jj
                            else:
                                i_edge = ii
                                j_edge = jj0

                            # Check angle edge normals make with freestream
                            if inner(edge_normals_i[i_edge],
                                     u_inf) > 0.0 or inner(
                                         edge_normals_j[j_edge], u_inf) > 0.0:
                                is_kutta_edge = True

                        # Store
                        if is_kutta_edge:
                            if ii - ii0 == 1:  # Order is important for definition of circulation
                                self._kutta_edges.append(
                                    KuttaEdge(v0, vi, [i, j]))
                            else:
                                self._kutta_edges.append(
                                    KuttaEdge(vi, v0, [i, j]))
                            break

                if self._verbose:
                    prog.display()

            # Store number of edges
            self.N_edges = len(self._kutta_edges)

        else:
            self.N_edges = 0

        if self._verbose:
            print("    Found {0} Kutta edges.".format(self.N_edges))

        if self._verbose:
            print()
            prog = OneLineProgress(
                self.N, msg="Locating panels for gradient calculation")

        # Store touching and abutting panels not across Kutta edge
        for i, panel in enumerate(self.panels):

            # Loop through panels touching this one
            for j in panel.touching_panels:

                # Check for kutta edge
                for kutta_edge in self._kutta_edges:
                    pi = kutta_edge.panel_indices
                    if (pi[0] == i and pi[1] == j) or (pi[0] == j
                                                       and pi[1] == i):
                        break

                else:
                    panel.touching_panels_not_across_kutta_edge.append(j)

                    # Check if the panel is abutting
                    if j in panel.abutting_panels:
                        panel.abutting_panels_not_across_kutta_edge.append(j)

            if self._verbose:
                prog.display()

        # Store second abutting panels not across Kutta edge
        # Note we're not tracking the progress of this loop. It's super fast.
        for i, panel in enumerate(self.panels):
            for j in panel.abutting_panels_not_across_kutta_edge:

                # This panel obviously counts
                panel.second_abutting_panels_not_across_kutta_edge.append(j)

                # Get second panels
                for k in self.panels[j].abutting_panels_not_across_kutta_edge:
                    if k not in panel.second_abutting_panels_not_across_kutta_edge and k != i:
                        panel.second_abutting_panels_not_across_kutta_edge.append(
                            k)

        # Set up least-squares matrices
        self._set_up_lst_sq()

        # Initialize wake
        if self.N_edges > 0:
            if self._wake_type == "fixed":
                self.wake = StraightFixedWake(kutta_edges=self._kutta_edges,
                                              **self._wake_kwargs)
            elif self._wake_type == "full_streamline":
                self.wake = FullStreamlineWake(kutta_edges=self._kutta_edges,
                                               **self._wake_kwargs)
            elif self._wake_type == "relaxed":
                self.wake = VelocityRelaxedWake(kutta_edges=self._kutta_edges,
                                                **self._wake_kwargs)
            elif self._wake_type == "marching_streamline":
                self.wake = MarchingStreamlineWake(
                    kutta_edges=self._kutta_edges, **self._wake_kwargs)
            else:
                raise IOError(
                    "{0} is not a valid wake type.".format(wake_type))
Пример #7
0
    def _determine_panel_adjacency_mapping(self, **kwargs):
        # Stores a list of the indices to each adjacent panel for each panel

        not_determined = True

        # Check for adjacency file
        adjacency_file = kwargs.get("adjacency_file", None)
        if adjacency_file is not None:

            # Try to find file
            try:
                with open(adjacency_file, 'r') as adj_handle:

                    # Get lines
                    lines = adj_handle.readlines()
                    lines = lines[1:]  # Skip header

                    # Check number of panels
                    if len(lines) % 2 != 0:
                        raise IOError(
                            "Data error in {0}. Should have two lines for each panel!"
                            .format(adjacency_file))
                    if len(lines) // 2 != self.N:
                        raise IOError(
                            "Data error in {0}. Mesh has {0} panels. File describes mapping for {2} panels."
                            .format(adjacency_file, self.N,
                                    len(lines) // 2))

                    # Loop through lines to store mapping
                    for i, line in enumerate(lines):
                        info = line.split()
                        panel_ind = i // 2

                        # Check the panel index is correct
                        if panel_ind != int(info[0]):
                            raise IOError(
                                "Input mismatch at line {0} of {1}. Panel index should be {2}; got {3}."
                                .format(i, adjacency_file, panel_ind,
                                        int(info[0])))

                        # Store
                        if i % 2 == 0:
                            self.panels[panel_ind].abutting_panels = [
                                int(ind) for ind in info[1:]
                            ]
                        else:
                            self.panels[panel_ind].touching_panels = [
                                int(ind) for ind in info[1:]
                            ]

                not_determined = False

            except OSError:
                warnings.warn(
                    "Adjacency file not found as specified. Reverting to brute force determination."
                )

        # Brute force approach
        if not_determined:

            if self._verbose:
                print()
                prog = OneLineProgress(
                    self.N, msg="Determining panel adjacency mapping")

            # Loop through possible combinations
            for i, panel_i in enumerate(self.panels):

                for j in range(i + 1, self.N):
                    panel_j = self.panels[j]

                    # Determine if we're touching and/or abutting
                    num_shared = 0
                    for i_vert in self._panel_vertex_indices[i][1:]:

                        # Check for shared vertex
                        if i_vert in self._panel_vertex_indices[j][1:]:
                            num_shared += 1
                            if num_shared == 2:
                                break  # Don't need to keep going

                    # Touching panels (at least one shared vertex)
                    if num_shared > 0 and j not in panel_i.touching_panels:
                        panel_i.touching_panels.append(j)
                        panel_j.touching_panels.append(i)

                    # Abutting panels (two shared vertices)
                    if num_shared == 2 and j not in panel_i.abutting_panels:
                        panel_i.abutting_panels.append(j)
                        panel_j.abutting_panels.append(i)

                if self._verbose:
                    prog.display()
Пример #8
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
Пример #9
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