From eb3ba4094a32184f170e797806da9df0db2a0a4d Mon Sep 17 00:00:00 2001 From: JWock82 Date: Sat, 14 Dec 2024 17:56:30 -0700 Subject: [PATCH] Added new shear wall class --- PyNite/ShearWall.py | 630 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 630 insertions(+) create mode 100644 PyNite/ShearWall.py diff --git a/PyNite/ShearWall.py b/PyNite/ShearWall.py new file mode 100644 index 0000000..c682c5e --- /dev/null +++ b/PyNite/ShearWall.py @@ -0,0 +1,630 @@ +from math import isclose +from numpy import average + +from PyNite.FEModel3D import FEModel3D +from prettytable import PrettyTable + +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +class ShearWall(): + + def __init__(self): + + self.model = FEModel3D() + self._L = None + self._H = None + self._t = None + self._ky_mod = 0.35 + self._mesh_size = 1 + self._openings = [] + self._flanges = [] + self._supports = [] + self._stories = [] + self._shears = [] + self._axials = [] + self._materials = [] + self.piers = {} + + @property + def L(self): + return self._L + + @L.setter + def L(self, value): + self._L = value + + @property + def H(self): + return self._H + + @H.setter + def H(self, value): + self._H = value + + @property + def mesh_size(self): + return self._mesh_size + + @mesh_size.setter + def mesh_size(self, value): + self._mesh_size = value + + @property + def ky_mod(self): + return self._ky_mod + + @ky_mod.setter + def ky_mod(self, value): + self._ky_mod = value + + def add_load_combo(self, name, factors, combo_type='strength'): + self.model.add_load_combo(name, factors, combo_type) + + def add_material(self, name, E, G, nu, rho, t, x_start=None, x_end=None, y_start=None, y_end=None): + if x_start is None: x_start = 0 + if x_end is None: x_end = self._L + if y_start is None: y_start = 0 + if y_end is None: y_end = self._H + self._materials.append([name, E, G, nu, rho, t, x_start, x_end, y_start, y_end]) + + def add_opening(self, name, x_start, y_start, width, height, tie=None): + self._openings.append([name, x_start, y_start, width, height, None]) + + def add_flange(self, thickness, width, x, y_start, y_end, material, side): + self._flanges.append([thickness, width, x, y_start, y_end, material, side]) + + def add_support(self, elevation=None, x_start=None, x_end=None): + if elevation is None: elevation = 0 + if x_start is None: x_start = 0 + if x_end is None: x_end = self._L + self._supports.append([elevation, x_start, x_end]) + + def add_story(self, story_name, elevation, x_start=None, x_end=None): + + # Validate input + if elevation is None: elevation = self._H + if x_start is None: x_start = 0 + if x_end is None: x_end = self._L + + # Add the story to the model + self._stories.append([story_name, elevation, x_start, x_end]) + + # Add a load combination to use when calculating the story's stiffness + self.model.add_load_combo('Stiffness: ' + story_name, {story_name: 1.0}, 'stiffness') + + # Add a 100 kip story shear to the model to use when calculating the story's stiffness + self.add_shear(story_name, 100, case=story_name) + + def add_shear(self, story_name, force, case='Case 1'): + self._shears.append([story_name, force, case]) + + def add_axial(self, story_name, force, case='Case 1'): + self._axials.append([story_name, force, case]) + + def generate(self): + + # Add materials to the model + for material in self._materials: + name, E, G, nu, rho = material[0:5] + self.model.add_material(name, E, G, nu, rho) + + # Identify mesh control points + x_control = [0, self._L] + y_control = [0, self._H] + + for material in self._materials: + x_control.append(material[6]) + x_control.append(material[7]) + y_control.append(material[8]) + y_control.append(material[9]) + + z_control = [0] + for flg in self._flanges: + if flg[6] == 'NS': z_control.append(flg[1]) + else: z_control.append(-flg[1]) + x_control.append(flg[2]) + y_control.append(flg[3]) + y_control.append(flg[4]) + + for support in self._supports: + x_control.append(support[1]) + x_control.append(support[2]) + y_control.append(support[0]) + + for story in self._stories: + x_control.append(story[2]) + x_control.append(story[3]) + y_control.append(story[1]) + + # While opening control points are auto-generated by the wall's mesh, we have no way of generating them for the flange meshes. We'll add some control points for the sake of the flanges. Duplicate control point values in the wall be be automatically resolved by Pynite. + for opng in self._openings: + y_control.append(opng[2]) + y_control.append(opng[2] + opng[4]) + + # Add the wall mesh to the model + self.model.add_rectangle_mesh('Wall', self._mesh_size, self._L, self._H, 12, self._materials[0][0], 1, self.ky_mod, x_control=x_control, y_control=y_control) + + # Add the openings to the mesh + self.model.add_material('Tie', 1, 1, 0, 0) + for opng in self._openings: + + name, x_start, y_start, width, height, AE = opng + self.model.meshes['Wall'].add_rect_opening(name, x_start, y_start, width, height) + + # Add any ties over the opening + if AE is not None: + + i_node_name = self.model.unique_name(self.model.nodes, 'N') + self.model.add_node(i_node_name, x_start, y_start + height, 0) + + j_node_name = self.model.unique_name(self.model.nodes, 'N') + self.model.add_node(j_node_name, x_start + width, y_start + height, 0) + + tie_name = self.model.unique_name(self.model.Members, 'Tie ') + self.model.add_member(tie_name, i_node_name, j_node_name, 'Tie', 1, 1, 1, AE) + self.model.def_releases(tie_name, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1) + + # Add the flanges to the mesh + for i, flg in enumerate(self._flanges): + + # Read in the flange's parameters + t, b, x, y_start, y_end, material, side = flg + + # Determine which side of the wall to place the flange on and define control points for the flange mesh so nodes line up properly with other meshes + if side == 'NS': + z = 0 + flg_x_control = [val for val in z_control if round(val, 10) >= 0 and round(val, 10) <= b] + else: + z = -b + flg_x_control = [b - (-val) for val in z_control if round(val, 10) <= 0 and round(val, 10) >= -b] + + flg_y_control = [y - y_start for y in y_control if round(y, 10) >= round(y_start, 10) and round(y, 10) <= round(y_end, 10)] + + # Add the flange to the model + self.model.add_rectangle_mesh('Flg'+str(i+1), self._mesh_size, b, y_end-y_start, t, material, 1, self.ky_mod, [x, y_start, z], 'YZ', flg_x_control, flg_y_control) + + # Generate the meshes + self.model.meshes['Wall'].generate() + + for i, flg in enumerate(self._flanges): + self.model.meshes['Flg'+str(i+1)].generate() + + # Merge the flange nodes with the rest of the wall + self.model.merge_duplicate_nodes() + + # Step through each plate in the model + for plate in self.model.quads.values(): + + # Step through each material in the wall + for material in self._materials: + + # Get the material properties + name, E, G, nu, rho, t, x_start, x_end, y_start, y_end = material + + # Determine if the current plate is part of a flange + if isclose(plate.i_node.X, plate.j_node.X): + # Flanges already have material properties and thicknesses assigned properly + pass + else: + # Determine if the current plate is this material + if round(plate.i_node.X, 10) >= round(x_start, 10) and round(plate.m_node.X, 10) <= round(x_end, 10) and round(plate.i_node.Y, 10) >= round(y_start, 10) and round(plate.m_node.Y, 10) <= round(y_start, 10): + + # Assign material properties to the plate + plate.E = E + plate.nu = nu + plate.t = t + + # Add supports + for support in self._supports: + elevation, x_start, x_end = support + for node in self.model.nodes.values(): + if isclose(node.Y, elevation) and round(node.X, 10) >= round(x_start, 10) and round(node.X, 10) <= round(x_end, 10): + self.model.def_support(node.name, True, True, True, True, True, True) + + # Add shear forces to the wall + for story in self._stories: + + # Read in parameters for this story + story_name, elevation, x_start, x_end = story + + # Initialize a list of story nodes + node_list = [] + + # Step through each node in the model + for node in self.model.nodes.values(): + + # Check if this node belongs to this story + if isclose(node.Y, elevation) and node.X >= x_start and node.X <= x_end and isclose(node.Z, 0): + + # Add the node to the list of nodes in the current story + node_list.append(node) + + # Add shear and axial forces to all the nodes in the story + for node in node_list: + + # Determine how many nodes are in the current story + num_nodes = len(node_list) + + # Step through each shear force in the model + for shear in self._shears: + + # Read in parameters for this shear + story, force, case = shear + + # Determine if this shear acts on this story + if story == story_name: + self.model.add_node_load(node.name, 'FX', force/num_nodes, case) + + # Step through each axial force in the model + for axial in self._axials: + + # Read in parameters for this axial force + story, force, case = axial + + # Determine if this axial force acts on this story + if story == story_name: + self.model.add_node_load(node.name, 'FY', -force/num_nodes, case) + + # Create a dictionary of piers in the wall + self._identify_piers() + + def _identify_piers(self): + + # Reset all piers in the wall + self.piers = {} + + # Create a list of x and y coordinates that represent the edges of the wall + x_vals = [0, self._L] + y_vals = [0, self._H] + + # Add the edges of the openings to the lists + for opng in self._openings: + x_vals.append(opng[1]) + x_vals.append(opng[1] + opng[3]) + y_vals.append(opng[2]) + y_vals.append(opng[2] + opng[4]) + + # Sort the lists (ascending) + x_vals = sorted(x_vals) + y_vals = sorted(y_vals) + + # Remove duplicate (or near duplicate) values + unique_list = [] + for i in range(len(x_vals) - 1): + # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value + if not isclose(x_vals[i], x_vals[i+1]): + unique_list.append(x_vals[i]) + unique_list.append(x_vals[-1]) # The last value will always be a keeper + x_vals = unique_list + + unique_list = [] + for i in range(len(y_vals) - 1): + # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value + if not isclose(y_vals[i], y_vals[i+1]): + unique_list.append(y_vals[i]) + unique_list.append(y_vals[-1]) # The last value will always be a keeper + y_vals = unique_list + + # Divide the wall into vertical strip piers using the left and right edges of each opening as strip boundaries + self.piers = {} + for i in range(len(x_vals) - 1): + width = x_vals[i+1] - x_vals[i] + height = self._H + x = x_vals[i] + y = 0 + self.piers['P' + str(i+1)] = Pier('P' + str(i+1), x, y, width, height) + + # Divide the strip piers further into rectanglular piers using the top and bottom of each opening as pier boundaries + new_piers = {} + pier_count = 1 + for pier in self.piers.values(): + for i in range(len(y_vals) - 1): + width = pier.width + height = y_vals[i+1] - y_vals[i] + x = pier.x + y = y_vals[i] + new_piers['P' + str(pier_count)] = Pier('P' + str(pier_count), x, y, width, height) + pier_count += 1 + self.piers = new_piers + + # Delete any piers that fall within an opening + delete_list = [] + for pier in self.piers.values(): + # Check if this pier is inside any of the openings + for opng in self._openings: + if (round(pier.x, 10) >= round(opng[1], 10) + and round(pier.x + pier.width, 10) <= round(opng[1] + opng[3], 10) + and round(pier.y, 10) >= round(opng[2], 10) + and round(pier.y + pier.height, 10) <= round(opng[2] + opng[4], 10)): + delete_list.append(pier.name) + break + + for pier in delete_list: + del self.piers[pier] + + # Working horizontally (left to right), rejoin any rectangles that share a vertical edge to form a larger rectangle + found_duplicate = True + while found_duplicate == True: + + found_duplicate = False + piers_copy = self.piers.copy() + + for key1, pier1 in piers_copy.items(): + + for key2, pier2 in piers_copy.items(): + + # Check for piers that need to be merged + if (key1 != key2 + and isclose(pier1.y, pier2.y) + and isclose(pier1.x + pier1.width, pier2.x) + and isclose(pier1.height, pier2.height)): + + # Merge the piers in the `self.piers` dictionary + self.piers[key1].width = pier1.width + pier2.width + + # Delete the 2nd pier from the `self.piers` dictionary + del self.piers[key2] + + # Since the `self.piers` dictionary has changed we need `piers_copy` to get updated. Flag that we found a duplicate and break the loops. + found_duplicate = True + break + + # Break the `for` loop if a duplicate was found so we can get an updated copy of `self.piers` + if found_duplicate == True: + break + + # Working vertically (bottom to top), rejoin any rectangles that share a horizontal edge to form a larger rectangle + found_duplicate = True + while found_duplicate == True: + + found_duplicate = False + piers_copy = self.piers.copy() + + for key1, pier1 in piers_copy.items(): + + for key2, pier2 in piers_copy.items(): + + if (key1 != key2 + and isclose(pier1.x, pier2.x) + and isclose(pier1.y + pier1.height, pier2.y) + and isclose(pier1.width, pier2.width)): + + # Merge the piers in the `self.piers` dictionary + self.piers[key1].height = pier1.height + pier2.height + + # Delete the 2nd pier from the `self.piers` dictionary + del self.piers[key2] + + # Since the `self.piers` dictionary has changed we need `piers_copy` to get updated. Flag that we found a duplicate and break the loops. + found_duplicate = True + break + + # Break the `for` loop if a duplicate was found so we can get an updated copy of `self.piers` + if found_duplicate == True: + break + + # Generate a list of new keys in ascending order + new_keys = [f'P{i+1}' for i in range(len(self.piers))] + + # Replace the old dicionary with one that has updated keys + self.piers = dict(zip(new_keys, self.piers.values())) + for key, pier in self.piers.items(): + pier.name = key + + # Assign plates to each pier + for plate in self.model.quads.values(): + Y_avg = (plate.i_node.Y + plate.m_node.Y)/2 + X_avg = (plate.i_node.X + plate.m_node.X)/2 + for pier in self.piers.values(): + if (round(X_avg, 10) >= round(pier.x, 10) + and round(X_avg, 10) <= round(pier.x + pier.width, 10) + and round(Y_avg, 10) >= round(pier.y, 10) + and round(Y_avg, 10) <= round (pier.y + pier.height, 10)): + pier.plates.append(plate) + + def draw_piers(self, show=False): + + fig, ax = plt.subplots() + + ax.patch.set_facecolor((0.8, 0.8, 0.8)) + + for pier in self.piers.values(): + self._add_rectangle(ax, pier.x, pier.y, pier.width, pier.height, pier.name) + + # Adjust the aspect ratio of the plot + ax.set_aspect('equal') + + # Slim down the margins + plt.tight_layout() + + # show plot or return it + if show == True: plt.show() + else: return plt + + def _add_rectangle(self, ax, x, y, w, h, name): + + # create rectangle + rect = Rectangle((x, y), w, h, linewidth=1, edgecolor='r', facecolor='white') + ax.add_patch(rect) + + # add name to center of rectangle + ax.text(x + w/2, y + h/2, name, ha='center', va='center') + + # set plot limits + ax.set_xlim(0, max(ax.get_xlim()[1], x + w)) + ax.set_ylim(0, max(ax.get_ylim()[1], y + h)) + + def _sort_openings(self): + + # Sort the openings based on y-coordinates + n = len(self._openings) + for i in range(n): + for j in range(0, n-i-1): + if self._openings[j][2] > self._openings[j+1][2]: + self._openings[j], self._openings[j+1] = self._openings[j+1], self._openings[j] + + # Sort the openings based on x-coordinates + n = len(self._openings) + for i in range(n): + for j in range(0, n-i-1): + if self._openings[j][1] > self._openings[j+1][1]: + self._openings[j], self._openings[j+1] = self._openings[j+1], self._openings[j] + + def stiffness(self, story_name): + + # TODO: Validate that the specified story exists in the shear wall + + # Step through each story in the model to find the one we're looking for + for story in self._stories: + + # Determine if this story is the one we are interested in + if story[0] == story_name: + + # Exit the loop + break + + # 100 kips is being applied to the story for the purpose of determining stiffness + V = 100 + + # Initialize the maximum wall deflection to zero + d_max = 0 + + # Step through each node in the model + for node in self.model.nodes.values(): + + # Determine if this node is in this story + if round(node.X, 10) >= round(story[2], 10) and round(node.X, 10) <= round(story[3], 10) and isclose(story[1], node.Y) and isclose(node.Z, 0): + + # Check if this deflection is the largest in the story + if node.DX['Stiffness: ' + story_name] > d_max: d_max = node.DX['Stiffness: ' + story_name] + + # Return the story's stiffness: + return V/(d_max*12) + + def render(self, color_map='Txy', combo_name='Combo 1'): + + from PyNite.Visualization import Renderer + renderer = Renderer(self.model) + renderer.annotation_size = 0.25 + renderer.render_loads = True + renderer.combo_name = combo_name + renderer.color_map = color_map + renderer.scalar_bar = True + renderer.deformed_shape = True + renderer.deformed_scale = 300 + renderer.labels = False + renderer.render_model() + + def screenshots(self, combo_name='Combo 1', dir_path='./'): + + from PyNite.Rendering import Renderer + + renderer = Renderer(self.model) + renderer.window_width = 750 + renderer.window_height = 750 + renderer.annotation_size = self.mesh_size/6 + renderer.deformed_shape = True + renderer.deformed_scale = 400 + renderer.render_loads = True + renderer.scalar_bar = True + renderer.combo_name = combo_name + renderer.labels = False + + # Save the shear plot screenshot to this file's directory + renderer.color_map = 'Txy' + renderer.screenshot(dir_path + '/shear_wall_screenshot1.png', interact=True) + + # Save the shear plot screenshot to this file's directory + renderer.color_map = 'Sy' + renderer.screenshot(dir_path + '/shear_wall_screenshot2.png', interact=False, reset_camera=False) + + # Save the pier screenshot to this file's directory + pier_sketch = self.draw_piers(show=False) + pier_sketch.savefig(dir_path + '/shear_wall_piers.png', format='png') + + def print_piers(self, combo_name='Combo 1'): + """Tabulates and prints pier results for the shear wall + """ + + # Create a PrettyTable object + table = PrettyTable() + + # Define the headers + table.field_names = ["ID", "Length", "Height", "M/(VL)", "V", "M", "P"] + + # Add rows to the table + for pier_id, pier in self.piers.items(): + P, M, V, M_VL = pier.sum_forces(combo_name) + table.add_row([pier.name, pier.width, pier.height, M_VL, V, M, P]) + + # Print the table + print('+-------------------+') + print('| Wall Pier Results |') + print('+-------------------+') + print(table) + + def report(self): + + from reporting import render_report, convert_image_to_html + + image_stream = io.BytesIO() + self.draw_piers(show=False).savefig(image_stream, format='png') + image_data = image_stream.getvalue() + + pier_sketch_html = convert_image_to_html(image_data) + + kwargs = {} + kwargs['pier_sketch'] = pier_sketch_html + + return render_report('report_SRMSW.html', kwargs) + +#%% +class Pier(): + + def __init__(self, name, x, y, width, height): + self.name = name + self.x = x # The location of the left side of the pier + self.y = y # The height of the bottom of the pier + self.width = width + self.height = height + self.plates = [] + + def sum_forces(self, combo_name='Combo 1'): + + # Initialize the forces in the plate + P, M, V = 0, 0, 0 + + # Step through each plate in the pier + for plate in self.plates: + + # Determine if this plate is at the bottom of the pier + if isclose(plate.i_node.Y, self.y): + + # Find and sum the axial forces in this plate + Pi = plate.F(combo_name)[1][0] + Pj = plate.F(combo_name)[7][0] + P += -Pi - Pj + + # Find and sum the moments about the pier's center in this plate + xi = plate.i_node.X - (self.x + self.width/2) + xj = plate.j_node.X - (self.x + self.width/2) + Mi = plate.F(combo_name)[1][0]*xi + Mj = plate.F(combo_name)[7][0]*xj + M += -Mi - Mj + + # Find and sum the shear forces in this plate + # Check if this is a flange plate or a web plate + if isclose(plate.i_node.X, plate.j_node.X): + Vi = -plate.F(combo_name)[2][0] + Vj = -plate.F(combo_name)[8][0] + else: + Vi = -plate.F(combo_name)[0][0] + Vj = -plate.F(combo_name)[6][0] + V += -Vi - Vj + + # Calculate the shear span ratio + M_VL = M/(V*self.width) + + # Return the summed forces and shear span ratio + return P, M, V, M_VL