Source code for opstool.preprocessing.section.var_sec_mesh

from __future__ import annotations

import re

import numpy as np
import pyvista as pv

# from sectionproperties.pre.geometry import Geometry
from shapely import LineString

pv.set_plot_theme("document")


[docs] def var_line_string( pointsi: list, pointsj: list, path: list, n_sec: float = 2, loc_sec: list | None = None, closure: bool = False, y_degree: int = 1, y_sym_plane: str = "j-0", z_degree: int = 2, z_sym_plane: str = "j-0", ): """Returns the varied line string. Parameters ---------- pointsi : list[list[float, float]] I end line points. pointsj : list[list[float, float]] J end line points. path : list Coordinate path of section normal, such as [(x1, y1, z1), (x2, y2, z2), ... , (xn, yn, zn)]. n_sec : float, optional The number of sections within each line segment between two coords in Arg `path`, by default 2. loc_sec : list or 1D numpy array, optional, default=None The section position of each element in the range 0-1. If not None, it overwrites n_sec. If None, it will be evenly divided according to Arg *n_sec*. closure: bool, default=False If True, the line is a closed loop. y_degree : int, optional The polynomial order of the y-axis dimension variation of the section, 1=linear, 2=parabolic, by default 1. y_sym_plane : str, optional, by default "j-0" When `y_degree`=2, specify the position of the symmetry plane, where the derivative is 0. The format is "i-{d}" or "j-{d}", which means that the distance from end i or end j is d. For example, "j-0" refers to end j, and "j-1.5" refers to the position 1.5 away from end j. z_degree : int, optional The polynomial order of the z-axis dimension variation of the section, 1=linear, 2=parabolic, by default 2. z_sym_plane : str, optional, by default "j-0" When `z_degree`=2, specify the position of the symmetry plane, where the derivative is 0. The format is "i-{d}" or "j-{d}", which means that the distance from end i or end j is d. For example, "j-0" refers to end j, and "j-1.5" refers to the position 1.5 away from end j. Returns -------- list[list[list[float, float]]] A list of lines, the number of which is determined by `path` and `n_sec`. """ length, cum_length, _ = _get_path_len(path, n_sec, loc_sec) if closure: if pointsi[-1] != pointsi[0]: pointsi = list(pointsi) pointsi.append(pointsi[0]) if pointsj[-1] != pointsj[0]: pointsj = list(pointsj) pointsj.append(pointsj[0]) linesi = LineString(pointsi) linesj = LineString(pointsj) yi, zi = linesi.xy yj, zj = linesj.xy cum_points = [] for x in cum_length: points_ = [] for i in range(len(yi)): y = _get_coord( 0, yi[i], length, yj[i], x, degree=y_degree, sym_plane=y_sym_plane ) z = _get_coord( 0, zi[i], length, zj[i], x, degree=z_degree, sym_plane=z_sym_plane ) points_.append((y, z)) cum_points.append(points_) return cum_points
[docs] def vis_var_sec( sec_meshes: list, path: list, n_sec: float = 2, loc_sec: list | None = None, on_notebook: bool = False, show_outline: bool = True, ): """Visualize varied section meshes. .. warning:: Please carefully check your section local axes. For visualization purposes, this function defaults to using vecxz=[-1,0,0] for vertical elements and vecxz=[0,0,1] for non-vertical ones. Parameters ---------- sec_meshes : list Section meshes list generated by :class:`opstool.preprocessing.SecMesh`. path : list Coordinate path of section normal, such as [(x1, y1, z1), (x2, y2, z2), ... , (xn, yn, zn)]. n_sec : float, optional The number of sections within each line segment between two coords in Arg `path`, by default 2. loc_sec : list or 1D numpy array, optional, default=None The section position of each element in the range 0-1. If not None, it overwrites n_sec. If None, it will be evenly divided according to Arg *n_sec*. on_notebook : bool, optional, by default False If True, display on the notebook. show_outline: bool, optional, by default False If True, display bound outline. """ _, _, cum_coord = _get_path_len(path, n_sec, loc_sec) plotter = pv.Plotter(notebook=on_notebook) cum_coord = np.array(cum_coord, dtype=np.float32) point_plot1 = pv.PolyData(cum_coord) plotter.add_mesh( point_plot1, color="black", point_size=5, render_points_as_spheres=True ) path = np.array(path, dtype=np.float32) point_plot2 = pv.PolyData(path) plotter.add_mesh( point_plot2, color="#8f1402", point_size=10, render_points_as_spheres=True ) line_cells = [] for i in range(len(path) - 1): line_cells.extend([2, i, i + 1]) line_plot = _generate_mesh(path, line_cells, kind="line") plotter.add_mesh( line_plot, color="black", render_lines_as_tubes=False, line_width=2, ) local_axes = _get_path_local_axes(path, n_sec, loc_sec) for i in range(len(sec_meshes)): sec_mesh = sec_meshes[i] center0 = cum_coord[i] _, vecy, vecz = local_axes[i] if not sec_mesh.is_centring: sec_mesh.centring() points = sec_mesh.points points = points[:, 0].reshape((-1, 1)) @ np.reshape(vecy, (1, 3)) + points[ :, 1 ].reshape((-1, 1)) @ np.reshape(vecz, (1, 3)) points += center0 for name, faces in sec_mesh.cells_map.items(): faces = np.insert(faces, 0, values=3, axis=1) face_plot = _generate_mesh(points, faces, kind="face") plotter.add_mesh( face_plot, color=sec_mesh.color_map[name], show_edges=True, opacity=1 ) for rdata in sec_mesh.rebar_data: color = rdata["color"] r = rdata["dia"] / 2 rebar_xy = np.array(rdata["rebar_xy"]) rebar_xy = rebar_xy[:, 0].reshape((-1, 1)) @ np.reshape( vecy, (1, 3) ) + rebar_xy[:, 1].reshape((-1, 1)) @ np.reshape(vecz, (1, 3)) rebar_xy += center0 spheres = [] for coord in rebar_xy: spheres.append(pv.Sphere(radius=r, center=coord)) merged = pv.MultiBlock(spheres) plotter.add_mesh(merged, color=color) if show_outline: plotter.show_bounds( grid=False, location="outer", show_zaxis=True, # color="black", ) plotter.add_axes() plotter.view_isometric() plotter.show(title="opstool")
def _get_path_len(path, n_sec, loc=None): n = len(path) if loc is not None: xs = loc else: xs = np.linspace(0, 1, n_sec) length = 0 cum_length = [] cum_coord = [] for i in range(n - 1): p1 = np.array(path[i]) p2 = np.array(path[i + 1]) seg = np.sqrt(np.sum((p2 - p1) ** 2)) for li in xs: cum_length.append(length + seg * li) cum_coord.append(p1 + (p2 - p1) * li) length += seg cum_length = np.array(cum_length, dtype=np.float32) cum_coord = np.array(cum_coord, dtype=np.float32) return length, cum_length, cum_coord def _get_path_local_axes(path, n_sec, loc): n = len(path) if loc is not None: n_sec = len(loc) local_axes = [] for i in range(n - 1): p1 = np.array(path[i]) p2 = np.array(path[i + 1]) local_x = (p2 - p1) / np.linalg.norm(p2 - p1) global_z = np.array([0, 0, 1]) cos2 = local_x @ global_z / (np.linalg.norm(local_x) * np.linalg.norm(global_z)) if np.abs(1 - cos2) < 1e-12: vecxz = [-1, 0, 0] else: vecxz = [0, 0, 1] local_y = np.cross(vecxz, local_x) local_z = np.cross(local_x, local_y) local_y /= np.linalg.norm(local_y) local_z /= np.linalg.norm(local_z) for j in range(n_sec): local_axes.append([local_x, local_y, local_z]) return local_axes def _get_coord(x1, y1, x2, y2, x, degree=1, sym_plane="j-0"): if degree == 1: y = y1 + (y2 - y1) * (x - x1) / (x2 - x1) elif degree == 2: d = float(re.findall(r"\d+\.?\d*", sym_plane)[0]) if sym_plane[0].lower() == "j": a = (y2 - y1) / (x2**2 - x1**2 - 2 * (x2 + d) * (x2 - x1)) b = -2 * a * (x2 + d) c = y1 - a * x1**2 - b * x1 y = a * x**2 + b * x + c elif sym_plane[0].lower() == "i": a = (y2 - y1) / (x2**2 - x1**2 - 2 * (x1 - d) * (x2 - x1)) b = -2 * a * (x1 - d) c = y1 - a * x1**2 - b * x1 y = a * x**2 + b * x + c else: raise ValueError(f"Error arg sym_plane={sym_plane}!") else: raise ValueError("Currently only support degree=1 or 2!") return y def _generate_mesh(points, cells, kind="line"): """ generate the mesh from the points and cells """ if kind == "line": pltr = pv.PolyData() pltr.points = points pltr.lines = cells elif kind == "face": pltr = pv.PolyData() pltr.points = points pltr.faces = cells else: raise ValueError(f"not supported {kind}!") return pltr
[docs] def get_lobatto_loc(n_sec: int): """Get the location of the Lobatto integration sections. Paramaters ----------- n_sec: int, The number of integration sections. Returns -------- 1d Numpy Array: The position of each beam element in the range 0-1. """ xi = np.zeros(n_sec) if n_sec == 2: xi[0] = -1.0 xi[1] = 1.0 elif n_sec == 3: xi[0] = -1.0 xi[1] = 0.0 xi[2] = 1.0 elif n_sec == 4: xi[0] = -1.0 xi[1] = -0.44721360 xi[2] = 0.44721360 xi[3] = 1.0 elif n_sec == 5: xi[0] = -1.0 xi[1] = -0.65465367 xi[2] = 0.0 xi[3] = 0.65465367 xi[4] = 1.0 elif n_sec == 6: xi[0] = -1.0 xi[1] = -0.7650553239 xi[2] = -0.2852315164 xi[3] = 0.2852315164 xi[4] = 0.7650553239 xi[5] = 1.0 elif n_sec == 7: xi[0] = -1.0 xi[1] = -0.8302238962 xi[2] = -0.4688487934 xi[3] = 0.0 xi[4] = 0.4688487934 xi[5] = 0.8302238962 xi[6] = 1.0 elif n_sec == 8: xi[0] = -1.0 xi[1] = -0.8717401485 xi[2] = -0.5917001814 xi[3] = -0.2092992179 xi[4] = 0.2092992179 xi[5] = 0.5917001814 xi[6] = 0.8717401485 xi[7] = 1.0 elif n_sec == 9: xi[0] = -1.0 xi[1] = -0.8997579954 xi[2] = -0.6771862795 xi[3] = -0.3631174638 xi[4] = 0.0 xi[5] = 0.3631174638 xi[6] = 0.6771862795 xi[7] = 0.8997579954 xi[8] = 1.0 elif n_sec == 10: xi[0] = -1.0 xi[1] = -0.9195339082 xi[2] = -0.7387738651 xi[3] = -0.4779249498 xi[4] = -0.1652789577 xi[5] = 0.1652789577 xi[6] = 0.4779249498 xi[7] = 0.7387738651 xi[8] = 0.9195339082 xi[9] = 1.0 loc = 0.5 * (xi + 1.0) return loc
[docs] def get_legendre_loc(n_sec: int): """Get the location of the Legendre integration sections. Paramaters ----------- n_sec: int, The number of integration sections. Returns -------- 1d Numpy Array: The position of each beam element in the range 0-1. """ xi = np.zeros(n_sec) if n_sec == 1: xi[0] = 0.0 elif n_sec == 2: xi[0] = -0.577350269189626 xi[1] = 0.577350269189626 elif n_sec == 3: xi[0] = -0.774596669241483 xi[1] = 0.0 xi[2] = 0.774596669241483 elif n_sec == 4: xi[0] = -0.861136311594053 xi[1] = -0.339981043584856 xi[2] = 0.339981043584856 xi[3] = 0.861136311594053 elif n_sec == 5: xi[0] = -0.906179845938664 xi[1] = -0.538469310105683 xi[2] = 0.0 xi[3] = 0.538469310105683 xi[4] = 0.906179845938664 elif n_sec == 6: xi[0] = -0.932469514203152 xi[1] = -0.661209386466265 xi[2] = -0.238619186083197 xi[3] = 0.238619186083197 xi[4] = 0.661209386466265 xi[5] = 0.932469514203152 elif n_sec == 7: xi[0] = -0.949107912342759 xi[1] = -0.741531185599394 xi[2] = -0.405845151377397 xi[3] = 0.0 xi[4] = 0.405845151377397 xi[5] = 0.741531185599394 xi[6] = 0.949107912342759 elif n_sec == 8: xi[0] = -0.960289856497536 xi[1] = -0.796666477413627 xi[2] = -0.525532409916329 xi[3] = -0.183434642495650 xi[4] = 0.183434642495650 xi[5] = 0.525532409916329 xi[6] = 0.796666477413627 xi[7] = 0.960289856497536 elif n_sec == 9: xi[0] = -0.968160239507626 xi[1] = -0.836031107326636 xi[2] = -0.613371432700590 xi[3] = -0.324253423403809 xi[4] = 0.0 xi[5] = 0.324253423403809 xi[6] = 0.613371432700590 xi[7] = 0.836031107326636 xi[8] = 0.968160239507626 elif n_sec == 10: xi[0] = -0.973906528517172 xi[1] = -0.865063366688985 xi[2] = -0.679409568299024 xi[3] = -0.433395394129247 xi[4] = -0.148874338981631 xi[5] = 0.148874338981631 xi[6] = 0.433395394129247 xi[7] = 0.679409568299024 xi[8] = 0.865063366688985 xi[9] = 0.973906528517172 loc = 0.5 * (xi + 1.0) return loc
# class VarSecMesh: # """A class for generating meshes with variable fiber cross-sections. # Parameters # ---------- # meshi : Instance of the class ``SecMesh``. # I end section. # meshj : Instance of the class ``SecMesh``. # J end section. # path : list # Coordinate path of section normal, # such as [(x1, y1), (x2, y2), ... , (xn, yn)] # n_sec : float, optional # The number of sections within each line segment between two coords in Arg `path`, # by default 2. # y_degree : int, optional # The polynomial order of the y-axis dimension variation of the section, # 1=linear, 2=parabolic, by default 1. # y_sym_plane : str, optional, by default "j-0" # When `y_degree`=2, specify the position of the symmetry plane, where the derivative is 0. # The format is "i-{d}" or "j-{d}", which means that the distance from end i or end j is d. # For example, "j-0" refers to end j, and "j-1.5" refers to the position 1.5 away from end j. # z_degree : int, optional # The polynomial order of the z-axis dimension variation of the section, # 1=linear, 2=parabolic, by default 2. # z_sym_plane : str, optional, by default "j-0" # When `z_degree`=2, specify the position of the symmetry plane, where the derivative is 0. # The format is "i-{d}" or "j-{d}", which means that the distance from end i or end j is d. # For example, "j-0" refers to end j, and "j-1.5" refers to the position 1.5 away from end j. # """ # def __init__(self, meshi: SecMesh, meshj: SecMesh, # path: list, n_sec: float = 2, # y_degree: int = 1, y_sym_plane: str = "j-0", # z_degree: int = 2, z_sym_plane: str = "j-0"): # if meshi.center is None or meshj.center is None: # raise ValueError("meshi and meshj must run centring() method!") # for i in range(len(path)): # p = np.atleast_1d(path[i]) # if len(p) == 1: # path[i] = np.append(p, [0, 0]) # elif len(p) == 2: # path[i] = np.append(p, [0]) # else: # path[i] = p # path = np.array(path, dtype=np.float32) # self.meshi = meshi # self.meshj = meshj # self.path = path # self.n_sec = n_sec # self.y_degree = y_degree # self.y_sym_plane = y_sym_plane # self.z_degree = z_degree # self.z_sym_plane = z_sym_plane # self.new_sec_mesh = [] # self.new_sec_props = [] # self.path_length = None # self.path_cum_length = None # self.path_cum_coord = None # def get_sec_mesh(self): # """Get the fiber section mesh on the `path`. # Returns # ------- # list, # A list of instances of class ``SecMesh`, # the number of which is determined by `path` and `n_sec`. # """ # length, cum_length, cum_coord = _get_path_len(self.path, self.n_sec) # self.path_length, self.path_cum_length, self.path_cum_coord = length, cum_length, cum_coord # for x in self.path_cum_length: # group_map, mesh_size_map, mat_ops_map = _get_map(self.meshi, self.meshj, length, x, # self.y_degree, self.y_sym_plane, # self.z_degree, self.z_sym_plane) # rebar_data = _get_rebar_data(self.meshi, self.meshj, length, x, # self.y_degree, self.y_sym_plane, self.z_degree, self.z_sym_plane) # secx = SecMesh() # secx.assign_group(group_map) # secx.assign_mesh_size(mesh_size_map) # secx.assign_ops_matTag(mat_ops_map) # if not self.meshi.color_map: # for i, name in enumerate(self.meshi.group_map.keys()): # secx.color_map[name] = secx.colors_default[i] # else: # secx.color_map = self.meshi.color_map # secx.rebar_data = rebar_data # secx.mesh() # secx.centring() # self.new_sec_mesh.append(secx) # return self.new_sec_mesh # def get_sec_props(self, Eref: float = 1.0): # """Solving Section Geometry Properties on the `path`. # Parameters # ---------- # Eref: float, default=1.0 # Reference modulus of elasticity, it is important to analyze the composite section. # See `sectionproperties doc <https://sectionproperties.readthedocs.io/en/latest/rst/post.html>`_ # Returns # ------- # list[dict] # Each element is a dict of properties for each section. # """ # for sec_mesh in self.new_sec_mesh: # sec_props = sec_mesh.get_sec_props(Eref) # self.new_sec_props.append(sec_props) # return self.new_sec_props # def view(self, on_notebook: bool = False): # """Visualize fiber cross-sections on path. # Parameters # ---------- # on_notebook : bool, optional, by default False # If True, display in the jupyter notebook. # """ # plotter = pv.Plotter(notebook=on_notebook) # point_plot1 = pv.PolyData(self.path_cum_coord) # plotter.add_mesh(point_plot1, color='black', # point_size=5, render_points_as_spheres=True) # point_plot2 = pv.PolyData(self.path) # plotter.add_mesh(point_plot2, color='#8f1402', # point_size=10, render_points_as_spheres=True) # line_cells = [] # for i in range(len(self.path) - 1): # line_cells.extend([2, i, i + 1]) # line_plot = _generate_mesh(self.path, line_cells, kind="line") # plotter.add_mesh( # line_plot, # color='black', # render_lines_as_tubes=False, # line_width=2, # ) # local_axes = _get_path_local_axes(self.path, self.n_sec) # for i in range(len(self.new_sec_mesh)): # sec_mesh = self.new_sec_mesh[i] # center0 = self.path_cum_coord[i] # _, vecy, vecz = local_axes[i] # points = sec_mesh.points # points = (points[:, 0].reshape((-1, 1)) @ np.reshape(vecy, (1, 3)) + # points[:, 1].reshape((-1, 1)) @ np.reshape(vecz, (1, 3))) # points += center0 # for name, faces in sec_mesh.cells_map.items(): # faces = np.insert(faces, 0, values=3, axis=1) # face_plot = _generate_mesh( # points, faces, kind="face" # ) # plotter.add_mesh( # face_plot, color=sec_mesh.color_map[name], show_edges=True, opacity=1 # ) # for rdata in sec_mesh.rebar_data: # color = rdata["color"] # r = rdata["dia"] / 2 # rebar_xy = np.array(rdata["rebar_xy"]) # rebar_xy = (rebar_xy[:, 0].reshape((-1, 1)) @ np.reshape(vecy, (1, 3)) + # rebar_xy[:, 1].reshape((-1, 1)) @ np.reshape(vecz, (1, 3))) # rebar_xy += center0 # spheres = [] # for coord in rebar_xy: # spheres.append(pv.Sphere(radius=r, center=coord)) # merged = pv.MultiBlock(spheres) # plotter.add_mesh(merged, color=color) # plotter.add_axes() # plotter.view_isometric() # plotter.show(title="opstool") # def _get_map(meshi, meshj, length, x, y_degree, y_sym_plane, z_degree, z_sym_plane): # group_map = dict() # mesh_size_map = dict() # mat_ops_map = dict() # for name in meshi.group_map.keys(): # mesh_size_map[name] = (meshi.mesh_size_map[name] + # meshj.mesh_size_map[name]) / 2 # if meshi.mat_ops_map and meshj.mat_ops_map: # mat_ops_map[name] = meshi.mat_ops_map[name] # geom_i = meshi.group_map[name].geom # geom_j = meshj.group_map[name].geom # ext_i = np.array(geom_i.exterior.coords) # ext_j = np.array(geom_j.exterior.coords) # ext_i -= np.array(meshi.center) # ext_j -= np.array(meshj.center) # ext_i = _sort_xy(ext_i) # ext_j = _sort_xy(ext_j) # yi, zi = ext_i[:, 0], ext_i[:, 1] # yj, zj = ext_j[:, 0], ext_j[:, 1] # ys, zs = [], [] # for i in range(len(yi)): # y = _get_coord(0, yi[i], length, yj[i], x, # degree=y_degree, sym_plane=y_sym_plane) # z = _get_coord(0, zi[i], length, zj[i], x, # degree=z_degree, sym_plane=z_sym_plane) # ys.append(y) # zs.append(z) # ext_points = [[ys[i_], zs[i_]] for i_ in range(len(ys))] # int_points = [] # for inti, intj in zip(geom_i.interiors, geom_j.interiors): # int_i = np.array(inti.coords) # int_j = np.array(intj.coords) # int_i -= np.array(meshi.center) # int_j -= np.array(meshj.center) # yi, zi = int_i[:, 0], int_i[:, 1] # yj, zj = int_j[:, 0], int_j[:, 1] # ys, zs = [], [] # for i in range(len(yi)): # y = _get_coord(0, yi[i], length, yj[i], x, # degree=y_degree, sym_plane=y_sym_plane) # z = _get_coord(0, zi[i], length, zj[i], x, # degree=z_degree, sym_plane=z_sym_plane) # ys.append(y) # zs.append(z) # int_points.append([[ys[i_], zs[i_]] for i_ in range(len(ys))]) # ply = Polygon(ext_points, int_points) # geometry = Geometry(geom=ply, material=meshi.group_map[name].material) # group_map[name] = geometry # return group_map, mesh_size_map, mat_ops_map # def _get_rebar_data(meshi, meshj, length, x, y_degree, y_sym_plane, z_degree, z_sym_plane): # rebar_data = [] # for datai, dataj in zip(meshi.rebar_data, meshj.rebar_data): # data = dict() # data["dia"] = datai["dia"] # data["matTag"] = datai["matTag"] # data['color'] = datai['color'] # rebar_xyi, rebar_xyj = np.array( # datai["rebar_xy"]), np.array(dataj["rebar_xy"]) # rebar_xyi, rebar_xyj = _sort_xy(rebar_xyi), _sort_xy(rebar_xyj) # xy = [] # for xyi, xyj in zip(rebar_xyi, rebar_xyj): # y = _get_coord(0, xyi[0], length, xyj[0], x, # degree=y_degree, sym_plane=y_sym_plane) # z = _get_coord(0, xyi[1], length, xyj[1], x, # degree=z_degree, sym_plane=z_sym_plane) # xy.append([y, z]) # data["rebar_xy"] = xy # rebar_data.append(data) # return rebar_data # def _sort_xy(points): # x, y = points[:, 0], points[:, 1] # x0, y0 = np.mean(x), np.mean(y) # r = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) # angles = np.where((y - y0) > 0, np.arccos((x - x0) / r), # 2 * np.pi - np.arccos((x - x0) / r)) # mask = np.argsort(angles) # points_ = points[mask] # return points_