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()
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()
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
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()
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()
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))
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()
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
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