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)
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
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)
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)
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]
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
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, :]
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]
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
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