diff --git a/hypervehicle/components/component.py b/hypervehicle/components/component.py index 71e07cf..ba6b1a5 100644 --- a/hypervehicle/components/component.py +++ b/hypervehicle/components/component.py @@ -106,6 +106,8 @@ def __init__( # Processed objects self.patches = {} # Parametric patches (continuous) + self.patch_res_r = {} # corresponding stl resolution in 'r' direction + self.patch_res_s = {} # corresponding stl resolution in 's' direction # VTK Attributes self.grids = None # Structured grids @@ -256,10 +258,20 @@ def surface(self, resolution: int = None): "No patches have been generated. " + "Please call .generate_patches()." ) + # Create case list + case_list = [] + for k, patch in self.patches.items(): + if isinstance(stl_resolution, int): + res_r = stl_resolution + res_s = stl_resolution + else: + res_r = self.patch_res_r[k] + res_s = self.patch_res_s[k] + case_list.append([k, patch, res_r, res_s]) + # Prepare multiprocessing arguments iterable - def wrapper(key: str, patch): + def wrapper(key: str, patch, res_r: int, res_s: int): flip = True if key.split("_")[-1] == "mirrored" else False - res = stl_resolution #if "swept" in key: # # Swept fuselage component @@ -269,13 +281,12 @@ def wrapper(key: str, patch): # else int(stl_resolution / 4) * 4 # ) # flip = True if "1" in key else False - surface = parametricSurfce2stl( - patch, res, flip_faces=flip, **self._clustering + patch, res_r, res_s, flip_faces=flip, **self._clustering ) return (key, surface) - multiprocess = True # flag to disable multiprocessing for debugging + multiprocess = False # flag to disable multiprocessing for debugging self.surfaces = {} if multiprocess is True: # Initialise surfaces and pool @@ -283,15 +294,15 @@ def wrapper(key: str, patch): # Submit tasks print(f"START: Creating stl - multiprocessor run.") - for result in pool.starmap(wrapper, self.patches.items()): + for result in pool.starmap(wrapper, case_list): self.surfaces[result[0]] = result[1] print(" DONE: Creating stl - multiprocess.") else: - for case in self.patches.items(): + for case in case_list: k = case[0] pat = case[1] print(f"START: Creating stl for '{k}'.") - result = wrapper(k, pat) + result = wrapper(k, pat, case[2], case[3]) self.surfaces[result[0]] = result[1] print(" DONE: Creating stl.") diff --git a/hypervehicle/components/swept.py b/hypervehicle/components/swept.py index d06652b..8f0042d 100644 --- a/hypervehicle/components/swept.py +++ b/hypervehicle/components/swept.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict from hypervehicle.components.component import Component from hypervehicle.geometry import SweptPatch, SweptPatchfromEdges from hypervehicle.components.constants import SWEPT_COMPONENT @@ -47,7 +47,7 @@ def __init__( self, cross_sections: List[List], close_ends: bool = True, - stl_resolution: int = 1, + stl_resolution: int | Dict[str, int] = 1, verbosity: int = 1, name: str = None, ) -> None: @@ -64,10 +64,10 @@ def __init__( 4 paths. stl_resolution : int, optional Defines same stl_resolution to all edges. - If 0 - then switch to stl_step automatic stl resolutions - stl_step : float, optional - stl resolution is calculated automatically to achieve desired step - size along edges + stl_resolution : dict [str, int] + Defines different stl resolution for different edges. Keys are + are: 'e0', 'e1', ... 'eN', 'sweep' for edges in first cross-section + and for swept edges. """ self.cross_sections = cross_sections self.close_ends = close_ends @@ -111,6 +111,16 @@ def check(self, small_number=1e-12): f"close_ends={self.close_ends} and N_edge={self.n_edges} is " + f"not supported." ) + if self.close_ends and isinstance(self.stl_resolution, Dict): + flag = 0 + if self.stl_resolution['e0'] != self.stl_resolution['e2']: + print("edge 'e0' and 'e2' don't have same stl_resolution.") + flag = 1 + if self.stl_resolution['e1'] != self.stl_resolution['e3']: + print("edge 'e1' and 'e3' don't have same stl_resolution.") + flag = 1 + if flag > 0: + raise Exception(f"stl_resolution not compatible for close_end.") def generate_patches(self): for ne in range(self.n_edges): @@ -120,6 +130,12 @@ def generate_patches(self): edges.append(cs[ne]) p = SweptPatchfromEdges(edges=edges) self.patches[k] = p + if isinstance(self.stl_resolution, int): + self.patch_res_r[k] = self.stl_resolution + self.patch_res_s[k] = self.stl_resolution + else: + self.patch_res_r[k] = self.stl_resolution['sweep'] + self.patch_res_s[k] = self.stl_resolution[f'e{ne}'] if self.close_ends == True: edges = self.cross_sections[0] # front @@ -128,9 +144,21 @@ def generate_patches(self): north = ReversedPath(edges[2]) west = ReversedPath(edges[3]) self.patches["swept_patch_end_0"] = CoonsPatch(south=south, north=north, west=west, east=east) + if isinstance(self.stl_resolution, int): + self.patch_res_r["swept_patch_end_0"] = self.stl_resolution + self.patch_res_s["swept_patch_end_0"] = self.stl_resolution + else: + self.patch_res_r["swept_patch_end_0"] = self.stl_resolution['e0'] + self.patch_res_s["swept_patch_end_0"] = self.stl_resolution['e1'] edges = self.cross_sections[-1] # rear south = ReversedPath(edges[0]) east = ReversedPath(edges[3]) north = edges[2] west = edges[1] self.patches["swept_patch_end_1"] = CoonsPatch(south=south, north=north, west=west, east=east) + if isinstance(self.stl_resolution, int): + self.patch_res_r["swept_patch_end_1"] = self.stl_resolution + self.patch_res_s["swept_patch_end_1"] = self.stl_resolution + else: + self.patch_res_r["swept_patch_end_1"] = self.stl_resolution['e0'] + self.patch_res_s["swept_patch_end_1"] = self.stl_resolution['e1'] \ No newline at end of file diff --git a/hypervehicle/utilities.py b/hypervehicle/utilities.py index be8b8e5..c839269 100644 --- a/hypervehicle/utilities.py +++ b/hypervehicle/utilities.py @@ -13,7 +13,8 @@ def parametricSurfce2stl( parametric_surface, - triangles_per_edge: int, + triangles_per_edge_r: int, + triangles_per_edge_s: int, si: float = 1.0, sj: float = 1.0, mirror_y=False, @@ -37,8 +38,11 @@ def parametricSurfce2stl( sj : float, optional The clustering in the j-direction. The default is 1.0. - triangles_per_edge : int - The resolution for the stl object. + triangles_per_edge_r : int + The resolution for the stl object - x/i direction. + + triangles_per_edge_s : int + The resolution for the stl object - y/j direction. mirror_y : bool, optional Create mirror image about x-z plane. The default is False. @@ -58,8 +62,8 @@ def parametricSurfce2stl( """ # TODO - allow different ni and nj discretisation - ni = triangles_per_edge - nj = triangles_per_edge + ni = triangles_per_edge_r + nj = triangles_per_edge_s if verbosity > 1: print(f"Triangles per edge: (ni, nj) = ({ni}, {nj})") @@ -81,83 +85,53 @@ def parametricSurfce2stl( print(f"r_list = {r_list}") print(f"s_list = {s_list}") - # Create vertices for corner points of each quad cell - # columns x, y, z for each vertex row - # quad exterior vertices + quad centres - vertices: np.ndarray = np.zeros(((ni + 1) * (nj + 1) + ni * nj, 3)) - centre_ix: int = (ni + 1) * (nj + 1) - + # For stl generation we split each cell into 4x triangles with indece [0, 3] + # p01-------p11 + # | \ 2 / | + # | \ / | + # | 3 pc 1 | + # | / \ | + # | / 0 \ | + # p00-------p10 + + N_triangles = 4*(ni)*(nj) + stl_mesh = mesh.Mesh(np.zeros(N_triangles, dtype=mesh.Mesh.dtype)) + # mesh.vectors contains a list defining three corners of each triangle. + t = 0 # For vertices along the x direction (i) - for i, r in enumerate(r_list): - # For vertices along the y direction (j) - for j, s in enumerate(s_list): - # Evaluate position - pos = parametric_surface(r, s) - - # Assign vertex - vertices[j * (ni + 1) + i] = np.array([pos.x, y_mult * pos.y, pos.z]) - - # Create vertices for centre point of each quad cell - try: - # Try index to bail before calling surface - vertices[centre_ix + (j * ni + i)] - - r0 = r_list[i] - r1 = r_list[i + 1] - s0 = s_list[j] - s1 = s_list[j + 1] - - # Get corner points - pos00 = parametric_surface(r0, s0) - pos10 = parametric_surface(r1, s0) - pos01 = parametric_surface(r0, s1) - pos11 = parametric_surface(r1, s1) - - # Evaluate quad centre coordinate - pos_x = 0.25 * (pos00.x + pos10.x + pos01.x + pos11.x) - pos_y = 0.25 * (pos00.y + pos10.y + pos01.y + pos11.y) - pos_z = 0.25 * (pos00.z + pos10.z + pos01.z + pos11.z) - - # Assign quad centre vertices - vc = np.array([pos_x, y_mult * pos_y, pos_z]) - vertices[centre_ix + (j * ni + i)] = vc - - except IndexError: - # Index out of bounds - pass - - # Create list of faces, defining the face vertices - faces = [] for i in range(ni): + # For vertices along the y direction (j) for j in range(nj): - p00 = j * (nj + 1) + i # bottom left - p10 = j * (nj + 1) + i + 1 # bottom right - p01 = (j + 1) * (ni + 1) + i # top left - p11 = (j + 1) * (ni + 1) + i + 1 # top right - - pc = centre_ix + j * min(ni, nj) + i # vertex at centre of cell - - if mirror_y or flip_faces: - faces.append([p00, pc, p10]) - faces.append([p10, pc, p11]) - faces.append([p11, pc, p01]) - faces.append([p01, pc, p00]) - else: - faces.append([p00, p10, pc]) - faces.append([p10, p11, pc]) - faces.append([p11, p01, pc]) - faces.append([p01, p00, pc]) - - faces = np.array(faces) - - # Create the STL mesh object - stl_mesh = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) - for ix, face in enumerate(faces): - # For each face - for c in range(3): - # For each coordinate x/y/z - stl_mesh.vectors[ix][c] = vertices[face[c], :] - + # get corner points for current cell (quad) + pos00 = parametric_surface(r_list[i], s_list[j]) + pos10 = parametric_surface(r_list[i+1], s_list[j]) + pos01 = parametric_surface(r_list[i], s_list[j+1]) + pos11 = parametric_surface(r_list[i+1], s_list[j+1]) + # get centre for current cell (quad) + pos_x = 0.25 * (pos00.x + pos10.x + pos01.x + pos11.x) + pos_y = 0.25 * (pos00.y + pos10.y + pos01.y + pos11.y) + pos_z = 0.25 * (pos00.z + pos10.z + pos01.z + pos11.z) + + # add triangle 0 [p00, p10, pc] + stl_mesh.vectors[t][0] = np.array([pos00.x, y_mult * pos00.y, pos00.z]) + stl_mesh.vectors[t][1] = np.array([pos10.x, y_mult * pos10.y, pos10.z]) + stl_mesh.vectors[t][2] = np.array([pos_x, y_mult * pos_y, pos_z]) + t += 1 + # add triangle 1 [p10, p11, pc] + stl_mesh.vectors[t][0] = np.array([pos10.x, y_mult * pos10.y, pos10.z]) + stl_mesh.vectors[t][1] = np.array([pos11.x, y_mult * pos11.y, pos11.z]) + stl_mesh.vectors[t][2] = np.array([pos_x, y_mult * pos_y, pos_z]) + t += 1 + # add triangle 2 [p11, p01, pc] + stl_mesh.vectors[t][0] = np.array([pos11.x, y_mult * pos11.y, pos11.z]) + stl_mesh.vectors[t][1] = np.array([pos01.x, y_mult * pos01.y, pos01.z]) + stl_mesh.vectors[t][2] = np.array([pos_x, y_mult * pos_y, pos_z]) + t += 1 + # add triangle 3 [p01, p00, pc] + stl_mesh.vectors[t][0] = np.array([pos01.x, y_mult * pos01.y, pos01.z]) + stl_mesh.vectors[t][1] = np.array([pos00.x, y_mult * pos00.y, pos00.z]) + stl_mesh.vectors[t][2] = np.array([pos_x, y_mult * pos_y, pos_z]) + t += 1 return stl_mesh