def _calc_area(self): # Calculates the panel area from the two constituent triangles A = 0.5 * norm( cross(self.vertices[1] - self.vertices[0], self.vertices[2] - self.vertices[0])) return A + 0.5 * norm( cross(self.vertices[2] - self.vertices[0], self.vertices[3] - self.vertices[0]))
def calc_local_coords(self, **kwargs): """Calculates panel local coords (dependent on flow properties). Parameters ---------- M : float Freestream Mach number. """ # Get kwargs M = kwargs["M"] c_0 = kwargs["c_0"] C_0 = kwargs["C_0"] B_0 = kwargs["B_0"] s = kwargs["s"] B = kwargs["B"] # Calculate tangent vector compressible norms (if applicable) if hasattr(self, "t"): self.t_comp_norm = np.zeros(3) for i, t in enumerate(self.t): self.t_comp_norm[i] = inner(t, t)-M**2*inner(c_0, t)**2 # Calculate conormal vector self.n_co = self.n-M**2*inner(c_0, self.n)*c_0 # Check inclination self.n_co = np.einsum('ij,j', B_0, self.n) self._incl = inner(self.n, self.n_co) if abs(self._incl)<1e-10: raise MachInclinedError self._r = np.sign(self._incl) # Get panel coordinate directions v_0 = cross(self.n, c_0) v_0 /= norm(v_0) u_0 = cross(v_0, self.n) u_0 /= norm(u_0) # Calculate transformation matrix # It should be that det(A) = B**2 (see Epton & Magnus pp. E.3-16) self._A = np.zeros((3,3)) denom = abs(self._incl)**-0.5 self._A[0,:] = denom*np.einsum('ij,j', C_0, u_0) self._A[1,:] = self._r*s/B*np.einsum('ij,j', C_0, v_0) self._A[2,:] = B*denom*self.n # Calculate area Jacobian self._J = 1.0/B*denom
def __init__(self, **kwargs): # Store vertices self.vertices = np.zeros((3,3)) self.vertices[0] = kwargs["v0"] self.vertices[1] = kwargs["v1"] self.vertices[2] = kwargs["v2"] self._projected = kwargs.get("projected", False) # Calculate area and normal vector n = cross(self.vertices[1]-self.vertices[0], self.vertices[2]-self.vertices[1]) N = norm(n) self.A = 0.5*N self.n = n/N # Check for zero area in projected panel if self.A<1e-10 and self._projected: self.null_panel = True else: self.null_panel = False # Calculate edge tangents self.t = np.roll(self.vertices, 1, axis=0)-self.vertices self.t /= np.linalg.norm(self.t, axis=1, keepdims=True) self._calc_geom_props()
def __init__(self, **kwargs): super().__init__(**kwargs) # Store type self._type = kwargs.get("fixed_direction_type", "freestream_and_rotation") # Initialize filaments self._vertices, self.inbound_panels, self.outbound_panels = self._arrange_kutta_vertices( ) # Store number of filaments and segments self.N = len(self._vertices) self.N_segments = 1 # Initialize filament directions self.filament_dirs = np.zeros((self.N, 3)) # Get direction for custom wake if self._type == "custom": try: u = np.array(kwargs.get("custom_dir")) u /= norm(u) self.filament_dirs[:] = u except: raise IOError( "'custom_dir' is required for non-iterative wake type 'custom'." )
def _calc_normal(self): # Calculates the panel unit normal vector # Assumes the panel is planar d1 = self.vertices[1] - self.vertices[0] d2 = self.vertices[2] - self.vertices[1] N = cross(d1, d2) return N / norm(N)
def __init__(self, **kwargs): # Store vertices self.vertices = np.zeros((4,3)) self.vertices[0] = kwargs.get("v0") self.vertices[1] = kwargs.get("v1") self.vertices[2] = kwargs.get("v2") self.vertices[3] = kwargs.get("v3", self.vertices[2]) # Will get removed by _check_collapsed_vertices() # Store edge number self.edge = kwargs.get("edge", [0]) # Determine if this is a projected panel self._projected = kwargs.get("projected", False) # Check for collapsed points self._check_collapsed_vertices(kwargs.get("tol", 1e-8)) # Calculate midpoints self.midpoints = 0.5*(self.vertices+np.roll(self.vertices, 1, axis=0)) # Calculate normal vector; this is simpler than the method used in PAN AIR, which is able to handle # the case where the midpoints and center point do not lie in a flat plane [Epton & Magnus section D.2] self.n = cross(self.midpoints[1]-self.midpoints[0], self.midpoints[2]-self.midpoints[1]) self.n /= norm(self.n) # Other calculations self._calc_geom_props() self._calc_skewness() # Setup projected panel if not self._projected: self._initialize_projected_panel() # Initialize subpanels self.subpanels = [] for i in range(self.N): # Outer subpanel self.subpanels.append(Subpanel(v0=self.midpoints[i-1], v1=self.vertices[i], v2=self.midpoints[i], projected=self._projected)) # Inner subpanel self.subpanels.append(Subpanel(v0=self.midpoints[i], v1=self.center, v2=self.midpoints[i-1], projected=self._projected)) # Initialize half panels (only if the panel is not already triangular) if self.N==4: self.half_panels = [] for i in range(self.N): self.half_panels.append(Subpanel(v0=self.vertices[i-2], v1=self.vertices[i-1], v2=self.vertices[i])) else: self.half_panels = False
def _load_stl(self, stl_file): # Loads mesh from an stl file # Load stl file raw_mesh = stl.mesh.Mesh.from_file(stl_file) # Initialize storage N = raw_mesh.v0.shape[0] self.N = N self.panels = [] bad_facets = [] # Loop through panels and initialize objects for i in range(N): # Check for finite area if norm(raw_mesh.normals[i]) == 0.0: self.N -= 1 warnings.warn("Panel {0} has zero area. Skipping...".format(i)) bad_facets.append(i) continue # Initialize panel = Tri(v0=raw_mesh.v0[i], v1=raw_mesh.v1[i], v2=raw_mesh.v2[i]) self.panels.append(panel) self.panels = np.array(self.panels) # Store panel information self.cp = np.zeros((self.N, 3)) self.n = np.zeros((self.N, 3)) self.dA = np.zeros(self.N) for i in range(self.N): self.n[i], self.dA[i], self.cp[i] = self.panels[i].get_info() # Get vertex list good_facets = [i for i in range(N) if i not in bad_facets] raw_vertices = np.concatenate( (raw_mesh.v0[good_facets], raw_mesh.v1[good_facets], raw_mesh.v2[good_facets])) self._vertices, inverse_indices = np.unique(raw_vertices, return_inverse=True, axis=0) self._panel_vertex_indices = [] for i in range(self.N): self._panel_vertex_indices.append([3, *inverse_indices[i::self.N]])
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 __init__(self, **kwargs): # Store vertices self.vertices = np.zeros((3, 3)) self.vertices[0] = kwargs.get("v0") self.vertices[1] = kwargs.get("v1") self.vertices[2] = kwargs.get("v2") self.N = 3 super().__init__(**kwargs) # Set up local coordinate transformation n, _, _ = self.get_info() self.A_t = np.zeros((3, 3)) self.A_t[0] = self.vertices[1] - self.vertices[0] self.A_t[0] /= norm(self.A_t[0]) self.A_t[1] = cross(n, self.A_t[0]) self.A_t[2] = n
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 __init__(self, **kwargs): # Store vertices self.vertices = np.zeros((4, 3)) self.vertices[0] = kwargs.get("v0") self.vertices[1] = kwargs.get("v1") self.vertices[2] = kwargs.get("v2") self.vertices[3] = kwargs.get("v3") self.midpoints = 0.5 * (self.vertices + np.roll(self.vertices, 1, axis=0)) self.N = 4 super().__init__(**kwargs) # Set up local coordinate transformation n = self._calc_normal() self.A_t = np.zeros((3, 3)) self.A_t[0] = self.midpoints[1] - self.midpoints[0] self.A_t[0] /= norm(self.A_t[0]) self.A_t[1] = cross(n, self.A_t[0]) self.A_t[2] = n
def get_vtk_data(self, **kwargs): """Returns a list of vertices and line indices describing this wake. Parameters ---------- length : float, optional Length of the final filament segment, if set as infinite. Defaults to 20 times the filament segment length. """ # Get kwargs l = kwargs.get("length", 20.0 * self.l) # Initialize storage vertices = [] line_vertex_indices = [] # Loop through filaments i = 0 for j in range(self.N): # Add vertices for k in range(self.N_segments + 1): vertices.append(self._vertices[j, k]) # Add indices if k != self.N_segments: line_vertex_indices.append([2, i + k, i + k + 1]) # Treat infinite end segment if self._end_infinite: u = vertices[-1] - vertices[-2] u /= norm(u) vertices[-1] = vertices[-2] + u * l # Increment index i += self.N_segments + 1 return vertices, line_vertex_indices, self.N * self.N_segments
def set_condition(self, **kwargs): """Sets the atmospheric conditions for the computation. V_inf : list Freestream velocity vector. rho : float Freestream density. angular_rate : list, optional Body-fixed angular rate vector (given in rad/s). Defaults to [0.0, 0.0, 0.0]. """ # Set solved flag self._solved = False # Get freestream self._v_inf = np.array(kwargs["V_inf"]) self._V_inf = norm(self._v_inf) self._u_inf = self._v_inf / self._V_inf self._rho = kwargs["rho"] self._omega = np.array(kwargs.get("angular_rate", [0.0, 0.0, 0.0])) # Create part of b vector dependent upon v_inf and rotation v_rot = vec_cross(self._omega, self._mesh.cp) self._v_inf_and_rot = self._v_inf - v_rot self._b = -vec_inner(self._v_inf - v_rot, self._mesh.n) # Get solid body rotation self._omega = np.array(kwargs.get("angular_rate", [0.0, 0.0, 0.0])) # Finish Kutta edge search on mesh self._mesh.finalize_kutta_edge_search(self._u_inf) # Update wake self._mesh.wake.set_filament_direction(self._v_inf, self._omega)
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 _calc_area(self): # Calculates the panel area return 0.5 * norm( cross(self.vertices[1] - self.vertices[0], self.vertices[2] - self.vertices[0]))
def _calc_normal(self): # Calculates the normal based off of the edge midpoints n = cross(self.midpoints[1] - self.midpoints[0], self.midpoints[2] - self.midpoints[1]) return n / norm(n)