Source code for NaxToPy.Modules.Fasteners.N2PUpdateFastener

import time
import csv
import numpy as np
from typing import Union
from NaxToPy.Core.Errors.N2PLog import N2PLog
from NaxToPy.Core.N2PModelContent import N2PModelContent, N2PProperty, N2PNastranInputData, N2PNode
from NaxToPy.Core.N2PModelContent import N2PElement


[docs] class N2PUpdateFastener: """ This class stores information about fasteners and provides methods to update their stiffness and generate a new FEM Input File (.bdf, .fem, .dat, etc.) file with the updated data. Fastener information can be input either as a dictionary (for a single type of fastener) or as a list of dictionaries (for multiple types). Each dictionary must include at least the keys for diameter ["D"] and elastic modulus ["E"]. Optional keys include head height ["h_head"], nut height ["h_nut"], Poisson ratio ["nu"], shear modulus ["G"], connection type ("bolt" or "rivet") ["connection_type"], shear type ("simple" or "double") ["shear_type"], and the beta value for Nelson method ["beta"]. IDs of fasteners to be updated can be provided as either a single list (for a single type of fastener) or as a list of lists (for multiple types). If no list of IDs is provided, all fasteners in the model will be updated. When multiple types of fasteners are used, the order of the list of dictionaries must match the order of the list of ID lists to ensure proper mapping of variables. Supported methods for stiffness calculation include HUTH, TATE_ROSENFELD, SWIFT, BOEING, GRUMMAN, and NELSON. Example: Example 1: >>> import NaxToPy as n2p >>> model = n2p.load_model("model.dat") >>> info1 = {"D": 4.8, ... "E": 110000} >>> info2 = {"D": 7.2, ... "E": 70000, ... "connection_type": "rivet", ... "shear_type": "double"} >>> ele1 = [1000, 1001, 1002] >>> ele2 = [2000, 2001, 2002] >>> update1 = N2PUpdateFastener(model, [info1, info2], [ele1, ele2], "BOEING") >>> update1.calculate() # This will update the model in memory with the information of the object. >>> model.ModelInputData.rebuild_file() # With the model updated, the model is rewritten Example 2: >>> model = n2p.load_model("model.dat") >>> update2 = N2PUpdateFastener(model, {}) # Create the object without information >>> update2.IDList = [3000, 3001, 3002] # Add the information using it properties >>> update2.StiffnessMethod = "GRUMMAN" >>> update2.FastenerInformation = {3000: {"D": 4.8, "E": 70000, "connection_type": "rivet"}, ... 3001: {"D": 6.8, "E": 70000, "connection_type": "bolt"}, ... 3002: {"D": 7.2, "E": 70000, "shear_type": "double"}} >>> update2.calculate() # This will update the model in memory with the information of the object. >>> model.ModelInputData.rebuild_file() # With the model updated, the model is rewritten """ def __init__(self, model: N2PModelContent, information: Union[dict, list], id_list: list = None, stiffness_method: str = "HUTH"): self.__model = model self.__stiffness_method = stiffness_method # Extract information from the model tinit = time.time() self.__c_rivet, self.__p_rivet, self.__nodes_crivet = self.__info_extraction() N2PLog.Debug.D504(time.time(), tinit) # If the list of fasteners to be updated is not provided, all the fasteners of the model are considered and the # entry of the information is just a simple dictionary, so we overwrite it in order to have a list of length one, # hence matching both lengths (IDs list and information list) if id_list is None: id_list = [list(self.__c_rivet.keys())] if type(information) == dict: info_list = [information] else: raise TypeError("IF FASTENER LIST IS NOT PROVIDED, THE INFORMATION OF THE FASTENER MUST BE A DICTIONARY") elif isinstance(id_list[0], list) and type(information) == list: info_list = information else: if isinstance(id_list[0],int) and type(information)==dict: #Check if it is a single list or a list of lists id_list = [id_list] info_list = [information] else: raise TypeError("IF THERE IS A SINGLE TYPE OF FASTENER, THE FASTENERS MUST BE INTRODUCED AS A SINGLE LIST AND INFORMATION AS A DICTIONARY.") self.__id_list = id_list self.__info_list = info_list # Check that the info_list is equal to the number of lists if len(info_list) != len(id_list): raise ValueError("LENGTH OF FASTENER INFORMATION DICT MUST MATCH WITH LENGTH OF FASTENER LIST") # Complete information about fasteners with default values when not provided for info_fasteners in info_list: if 'connection_type' not in info_fasteners or not info_fasteners['connection_type']: info_fasteners['connection_type'] = "bolt" if 'shear_type' not in info_fasteners or not info_fasteners['shear_type']: info_fasteners['shear_type'] = "simple" if 'h_head' not in info_fasteners or not info_fasteners['h_head']: info_fasteners['h_head'] = 0 if 'h_nut' not in info_fasteners or not info_fasteners['h_nut']: info_fasteners['h_nut'] = 0 if 'nu' not in info_fasteners or not info_fasteners['nu']: info_fasteners['nu'] = 0.33 if 'G' not in info_fasteners or not info_fasteners['G']: info_fasteners['G'] = info_fasteners['E'] / (2 * (1 + info_fasteners['nu'])) if 'beta' not in info_fasteners or not info_fasteners['beta']: info_fasteners['beta'] = 0.5 # Map each fastener with its properties self.__mapping_fastener_properties() def __mapping_fastener_properties(self): self.__fastener_properties = {fastener: prop for prop, IDs in zip(self.__info_list, self.__id_list) for fastener in IDs} def __read_bolts(self, path: str) -> dict: """ Reads a csv containing the information about fasteners to be updated Args: path (str): path to BDF file Returns: dict: A dictionary where keys are fastener IDs (int) and values are lists containing fastener information. """ with open(path, "r") as file: content = csv.reader(file) return {int(row[0]): [_.strip() for _ in row] for row in content} def __axial_stiffness(self, fastener_ID: int, plate1_thickness: float, plate2_thickness: float) -> float: """ Calculate the axial stiffness of a fastener, based on its elastic modulus, diameter, and the thicknesses of the plates it joins, along with optional parameters for the height of the head and nut. Args: fastener_ID (int): ID of the fastener to be updated. plate1_thickness (float): Thickness of the first plate. plate2_thickness (float): Thickness of the second plate. Returns: axial_stiffness (float): Axial stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] head_height = info_rivet["h_head"] nut_height = info_rivet["h_nut"] E_f = info_rivet["E"] diameter = info_rivet["D"] # Effective length effective_length = plate1_thickness + plate2_thickness + head_height / 3 + nut_height / 3 # Calculation of axial stiffness axial_stiffness = E_f * np.pi * (diameter ** 2) / (4 * effective_length) return axial_stiffness def __huth_shear(self, fastener_ID: int, E_1: float, t_1: float, plate1_property_type: str, E_2: float, t_2: float, plate2_property_type: str) -> float: """ Calculate the shear stiffness of a fastener by using Huth formula, based on its elastic modulus, diameter, the elastic moduli and thicknesses of the plates it joins, along with other parameters. Args: fastener_ID (int): ID of the fastener to be updated E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. plate1_property_type (str): Type of property of plate 1 ("PSHELL" or "PCOMP"). E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. plate2_property_type (str): Type of property of plate 2 ("PSHELL" or "PCOMP"). Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] connection_type = info_rivet["connection_type"] shear_type = info_rivet["shear_type"] E_f = info_rivet["E"] d = info_rivet["D"] # Calculate parameters based on connection type if connection_type == "bolt": a = 2 / 3 b1 = 3 if (plate1_property_type == "PSHELL") else 4.2 b2 = 3 if (plate2_property_type == "PSHELL") else 4.2 elif connection_type == "rivet": a = 2 / 5 b1 = 2.2 if (plate1_property_type == "PSHELL") else 4.2 b2 = 2.2 if (plate2_property_type == "PSHELL") else 4.2 # Determine shear type if shear_type == "simple": n = 1 elif shear_type == "double": n = 2 # Calculate Ec0 Ec0 = ((t_1 + t_2) / (2 * d)) ** a # Calculate Ec1 Ec1 = ((1 / (t_1 * E_1)) + (1 / (2 * t_1 * E_f))) * (b1 / n) # Calculate Ec2 Ec2 = ((1 / (t_2 * n * E_2)) + (1 / (2 * t_2 * E_f * n))) * (b2 / n) # Calculate ksi ksi = 1 / ((Ec1 + Ec2) * Ec0) return ksi def __boeing_shear(self, fastener_ID: int, E_1: float, t_1: float, E_2: float, t_2: float) -> float: """ Calculate the shear stiffness of a fastener by using Boeing formula, based on its elastic modulus, diameter and the elastic moduli and thicknesses of the plates it joins. Args: fastener_ID (int): ID of the fastener to be updated. E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] shear_type = info_rivet["shear_type"] E_f = info_rivet["E"] d = info_rivet["D"] if shear_type == "simple": numerator = 2 elif shear_type == "double": numerator = 1.25 # Calculate both flexibilities f1 = (numerator ** ((t_1 / d) ** 0.85) / t_1) * ((1 / E_1) + (3 / (8 * E_f))) f2 = (numerator ** ((t_2 / d) ** 0.85) / t_2) * ((1 / E_2) + (3 / (8 * E_f))) # Add flexibilities f = f1 + f2 # Converts into stiffness ksi = 1 / f return ksi def __swift_shear(self, fastener_ID: int, E_1: float, t_1: float, E_2: float, t_2: float) -> float: """ Calculate the shear stiffness of a fastener by using Swift (Douglas Aircraft) formula, based on its elastic modulus, diameter and the elastic moduli and thicknesses of the plates it joins. Args: fastener_ID (int): ID of the fastener to be updated E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] E_f = info_rivet["E"] d = info_rivet["D"] f = (5 / (d * E_f)) + (0.8 * (1 / (t_1 * E_1)) + (1 / (t_2 * E_2))) ksi = 1 / f return ksi def __tate_rosenfeld_shear(self, fastener_ID: int, E_1: float, t_1: float, E_2: float, t_2: float) -> float: """Calculate the shear stiffness of a fastener by using Tate & Rosenfeld formula, based on its elastic modulus, diameter and the elastic moduli and thicknesses of the plates it joins. Args: fastener_ID (int): ID of the fastener to be updated E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] E_f = info_rivet["E"] nu_f = info_rivet["nu"] d = info_rivet["D"] f = (1 / (E_f * t_1)) + (1 / (E_f * t_2)) + (1 / (E_1 * t_1)) + (1 / (E_2 * t_2)) + ( 32 / (9 * E_f * np.pi * d ** 2)) * (1 + nu_f) * (t_1 + t_2) + \ (8 / (5 * E_f * np.pi * d ** 4)) * (t_1 ** 3 + 5 * (t_1 ** 2) * t_2 + 5 * (t_2 ** 2) * t_1 + t_2 ** 3) ksi = 1 / f return ksi def __grumman_shear(self, fastener_ID: int, E_1: float, t_1: float, E_2: float, t_2: float) -> float: """Calculate the shear stiffness of a fastener by using Grumman formula, based on its elastic modulus, diameter and the elastic moduli and thicknesses of the plates it joins. Args: fastener_ID (int): ID of the fastener to be updated E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] E_f = info_rivet["E"] d = info_rivet["D"] f = (((t_1 + t_2) ** 2) / (E_f * d ** 3)) + 3.72 * ((1 / (E_1 * t_1)) + ((1 / (E_2 * t_2)))) ksi = 1 / f return ksi def __nelson_shear(self, fastener_ID: int, E_1: float, t_1: float, E_2: float, t_2: float, E_L1: float = None, E_LT1: float = None, E_L2: float = None, E_LT2: float = None) -> float: """Calculate the shear stiffness of a fastener by using Grumman formula, based on its elastic modulus, diameter and the elastic moduli and thicknesses of the plates it joins. Args: fastener_ID (int): ID of the fastener to be updated E_1 (float): Elastic modulus of plate 1 oriented in fastener coordinates system. t_1 (float): Thickness of plate 1. E_2 (float): Elastic modulus of plate 2 oriented in fastener coordinates system. t_2 (float): Thickness of plate 2. Returns: ksi (float): Shear stiffness of the fastener. """ info_rivet = self.__fastener_properties[fastener_ID] shear_type = info_rivet["shear_type"] E_f = info_rivet["E"] d = info_rivet["D"] G_f = info_rivet["G"] beta = info_rivet["beta"] if E_L1 is None: E_L1 = E_1 if E_LT1 is None: E_LT1 = E_1 if E_L2 is None: E_L2 = E_2 if E_LT2 is None: E_LT2 = E_2 E_eq1 = np.sqrt(E_L1 * E_LT1) E_eq2 = np.sqrt(E_L2 * E_LT2) A_f = np.pi * (d ** 2) / 4 I_f = np.pi * (d ** 4) / 64 if shear_type == "simple": f = (2 * (t_1 + t_2) / (3 * G_f * A_f)) + (2 * (t_1 + t_2) / (t_1 * t_2 * E_f)) + 1 / (t_1 * E_eq1) + ( 1 + 3 * beta) / (t_2 * E_eq2) elif shear_type == "double": f = (8 * (t_2 ** 3) + 16 * (t_2 ** 2) * t_1 + 8 * (t_1 ** 2) * t_2 + t_1 ** 3) / (192 * E_f * I_f) + ( 2 * (t_1 + t_2) / (3 * G_f * A_f)) + (2 * (t_1 + t_2) / (t_1 * t_2 * E_f)) + 2 / ( t_1 * E_eq1) + 1 / (t_2 * E_eq2) ksi = 1 / f return ksi def __info_extraction(self): """Extracts information from BDF files. This function loads data from BDF files and creates dictionaries containing information related to different types of fasteners and their nodes. Args: "None" Returns: tuple: A tuple containing: crivet (dict): Dictionary of CRIVET and CBUSH cards. privet (dict): Dictionary of PFAST and PBUSH cards. nodos_crivet (dict): Dictionary mapping CRIVET/CBUSH IDs to their corresponding node IDs. model (N2PModelContent): Model loaded from the BDF file. """ # Record the initial time t_init = time.time() # Extract CRIVET and CBUSH cards crivet = {card.EID: card for card in self.__model.ModelInputData.get_cards_by_field(["CFAST", "CBUSH"])} # Extract PFAST and PBUSH cards privet = {pfast.PID: pfast for pfast in self.__model.ModelInputData.get_cards_by_field(["PFAST", "PBUSH"])} # Create a dictionary mapping CRIVET/CBUSH IDs to their corresponding node IDs nodos_crivet = {key: [self.__model.get_nodes((card.GA, 0)), self.__model.get_nodes((card.GB, 0))] for key, card in crivet.items()} # Log information about the extraction time N2PLog.Debug.D500(time.time(), t_init) return crivet, privet, nodos_crivet def __elastic_calculus(self, property_obj: N2PProperty, vector2, theta, axis_mat_X) -> tuple[float, float]: """ Calculate the elastic modulus of a shell or the equivalent elastic modulus of a laminate in the fastener Y and Z directions. Args: property_obj (N2PProperty): Property object. vector2 (ndarray (3,)): Vector Y in the fastener axes. theta (float): Angle between material and G1-G2. axis_mat_X (ndarray (3,)): X vector of the material of the two elements to be joined. Returns: Ez (float): Young's modulus of the element in the Z direction of the fastener. Ey (float): Young's modulus of the element in the Y direction of the fastener. """ t_init = time.time() material_dict = self.__model.MaterialDict # Calculate plate properties if defined as PSHELL if property_obj.PropertyType == "PSHELL": material = material_dict[property_obj.MatMemID] if material.MatType == "MAT1": E1 = material.Young E2 = material.Young nu = material.Poisson G = material.Shear if material.Shear != 0 else E1 / (2 * (1 + nu)) elif material.MatType == "UNDEF": q11 = material.QualitiesDict.get("G11") q22 = material.QualitiesDict.get("G22") q12 = material.QualitiesDict.get("G12") q66 = material.QualitiesDict.get("G33") E1, E2, nu, G = self.__q_to_engineering(q11, q22, q12, q66) elif material.MatType == "MAT8": E1 = material.YoungX E2 = material.YoungY nu = material.PoissonXY G = material.ShearXY if material.ShearXY != 0 else E1 / (2 * (1 + nu)) else: N2PLog.Error.E311(property_obj.ID) # Calculate equivalent properties if the plate is a PCOMP elif property_obj.PropertyType == "PCOMP": equivalent_Q = property_obj.EqQMatrix E1, E2, nu, G = self.__q_to_engineering(equivalent_Q[0][0], equivalent_Q[1][1], equivalent_Q[0][1], equivalent_Q[2][2]) # Orient properties in the fastener axes. # Calculation of the angle between G1-G2 of the element (material_axis_X) and the "Y" axis of the fastener. alpha = self.__angle_between_vectors(vector2, axis_mat_X) # Angle between the material and the fastener x-axis. Theta is the angle between the material and G1-G2. angle = alpha - theta s = np.sin(np.radians(angle)) c = np.cos(np.radians(angle)) # Orient element properties in the fastener axes. Ey = (1 / E1 * c ** 4 + (1 / G - 2 * nu / E1) * s ** 2 * c ** 2 + 1 / E2 * s ** 4) ** -1 Ez = (1 / E1 * s ** 4 + (1 / G - 2 * nu / E1) * s ** 2 * c ** 2 + 1 / E2 * c ** 4) ** -1 N2PLog.Debug.D501(time.time(), t_init) return Ey, Ez def __angle_between_vectors(self, vector1, vector2): dot_product = np.dot(vector1, vector2) norm_product = np.linalg.norm(vector1) * np.linalg.norm(vector2) cos_angle = dot_product / norm_product angle = np.arccos(np.clip(cos_angle, -1.0, 1.0)) # Use clip to avoid numerical errors return np.degrees(angle) def __q_to_engineering(self, q11, q22, q12, q66) -> tuple[float, float, float, float]: """Calculate the engineering constants (E1, E2, nu, G) from the material matrix. Args: q11 (float): Modulus in the 1 direction. q22 (float): Modulus in the 2 direction. q12 (float): Modulus in the 1-2 plane. q66 (float): Shear modulus in the 1-2 plane. Returns: tuple[float, float, float, float]: Engineering constants (E1, E2, nu, G). """ Q = np.array([[q11, q12, 0], [q12, q22, 0], [0, 0, q66]]) S = np.linalg.inv(Q) e1 = 1 / S[0, 0] e2 = 1 / S[1, 1] nu12 = -S[0, 1] * e1 g = 1 / S[2, 2] return e1, e2, nu12, g def __CBUSH_connected_RBE3(self, card: N2PNastranInputData, nodoA: N2PNode, nodoB: N2PNode): """ Returns the input that will be used to compute the elastic modulus of a fastener. This function is developed for the specific case of a connector CBUSH connected to a RBE3 Args: card (N2PModelContent): Card associated to the fastener nodoA (N2PModelContent): First node at which the fastener is connected nodoB (N2PModelContent): Second node at which the fastener is connected Returns: propA, propB (N2PComp): Properties of the components vector2 (ndarray (3,)): Y axis in rivet coordinates thetaA, thetaB (float): Orientation of the material of the two elements to be joined axis_matA_X, axis_matB_X (ndarray (3,)): X axis of the material of the two elements to be joined """ # Define vector 0 based on GA-G0 or its components if card.X2 is None: nodo0 = self.__model.NodesDict[(card.G0, 0)] vector0 = np.array([nodo0.X - nodoA.X, nodo0.Y - nodoA.Y, nodo0.Z - nodoA.Z]) else: vector0 = np.array([card.X1, card.X2, card.X3]) vector0 /= np.linalg.norm(vector0) # Define fastener orientation vectors vector1 = np.array([nodoB.X - nodoA.X, nodoB.Y - nodoA.Y, nodoB.Z - nodoA.Z]) vector1 /= np.linalg.norm(vector1) vector3 = np.cross(vector1, vector0) vector2 = np.cross(vector3, vector1) # Find element connected to RBE3 for both nodes A and B rbe3A = nodoA.Connectivity[0] rbe3B = nodoB.Connectivity[0] ######### METHODOLOGY # We need to find out to which element the RBE3 is connected. To do this, we need to find out to which nodes it # is connected, and then find out to which elements those nodes are connected. The element we are looking for # will be linked to those nodes. However, there is one node that we do not need to consider, and that is the # reference node. To do this: # We go through all the nodes in rbe3A.FreeNodes except rbe3A.RefGrid, and for each of them, we find their # connectivity, obtaining a set of elements for each node of the RBE3. Finally, we apply the intersection of the # sets, that is, we keep a set containing the items repeated in all the sets, which is the element we # are looking for. t_a = time.time() componentA = set.intersection(*[{elem for elem in node.Connectivity if isinstance(elem, N2PElement)} for node in self.__model.get_nodes( [node for node in rbe3A.FreeNodes if node != rbe3A.RefGrid])]).pop() componentB = set.intersection(*[{elem for elem in node.Connectivity if isinstance(elem, N2PElement)} for node in self.__model.get_nodes( [node for node in rbe3B.FreeNodes if node != rbe3B.RefGrid])]).pop() N2PLog.Debug.D502(time.time(), t_a) # Extract axes and orientation angles thetaA = componentA.AngleMat axis_matA_X = (componentA.Nodes[1].X - componentA.Nodes[0].X, componentA.Nodes[1].Y - componentA.Nodes[0].Y, componentA.Nodes[1].Z - componentA.Nodes[0].Z) thetaB = componentB.AngleMat axis_matB_X = (componentB.Nodes[1].X - componentB.Nodes[0].X, componentB.Nodes[1].Y - componentB.Nodes[0].Y, componentB.Nodes[1].Z - componentB.Nodes[0].Z) # Retrieve properties of the components propA = self.__model.PropertyDict.get(componentA.Prop) propB = self.__model.PropertyDict.get(componentB.Prop) return propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X def __CBUSH_connected_nodes(self, card: N2PNastranInputData, nodoA: N2PNode, nodoB: N2PNode): """ Returns the input that will be used to compute the elastic modulus of a fastener. This function is developed for the specific case of a connector CBUSH connected to two nodes Args: card (N2PModelContent): Card associated to the fastener nodoA (N2PModelContent): First node at which the fastener is connected nodoB (N2PModelContent): Second node at which the fastener is connected Returns: propA, propB (N2PComp): Properties of the components vector2 (ndarray (3,)): Y axis in rivet coordinates thetaA, thetaB (float): Orientation of the material of the two elements to be joined axis_matA_X, axis_matB_X (ndarray (3,)): X axis of the material of the two elements to be joined """ # Define vector 0 based on GA-G0 or its components if card.X2 is None: nodo0 = self.__model.NodesDict[(card.G0, 0)] vector0 = np.array([nodo0.X - nodoA.X, nodo0.Y - nodoA.Y, nodo0.Z - nodoA.Z]) else: vector0 = np.array([card.X1, card.X2, card.X3]) vector0 /= np.linalg.norm(vector0) # Define fastener orientation vectors vector1 = np.array([nodoB.X - nodoA.X, nodoB.Y - nodoA.Y, nodoB.Z - nodoA.Z]) vector1 /= np.linalg.norm(vector1) vector3 = np.cross(vector1, vector0) vector2 = np.cross(vector3, vector1) # Extract properties and axes of the connected components componentA = nodoA.Connectivity[0] thetaA = componentA.AngleMat axis_matA_X = (componentA.Nodes[1].X - componentA.Nodes[0].X, componentA.Nodes[1].Y - componentA.Nodes[0].Y, componentA.Nodes[1].Z - componentA.Nodes[0].Z) componentB = nodoB.Connectivity[0] thetaB = componentB.AngleMat axis_matB_X = (componentB.Nodes[1].X - componentB.Nodes[0].X, componentB.Nodes[1].Y - componentB.Nodes[0].Y, componentB.Nodes[1].Z - componentB.Nodes[0].Z) # Retrieve properties of the components propA = self.__model.PropertyDict.get(componentA.Prop) propB = self.__model.PropertyDict.get(componentB.Prop) return propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X def __CFAST_connected_ELEM(self, card: N2PNastranInputData, nodoA: N2PNode, nodoB: N2PNode): """ Returns the input that will be used to compute the elastic modulus of a fastener. This function is developed for the specific case of a connector CFAST connected by ELEM type. Args: card (N2PModelContent): Card associated to the fastener. nodoA (N2PModelContent): First node at which the fastener is connected. nodoB (N2PModelContent): Second node at which the fastener is connected. Returns: propA, propB (N2PComp): Properties of the components. vector2 (ndarray (3,)): Y axis in rivet coordinates. thetaA, thetaB (float): Orientation of the material of the two elements to be joined. axis_matA_X, axis_matB_X (ndarray (3,)): X axis of the material of the two elements to be joined. """ # Para sacar los ejes del remache, necesitamos acceder a la tarjeta PFAST pfast_ID = card.PID pfast_card = self.__p_rivet[pfast_ID] # Procedures followed as detailed in MSC Nastran 2021 Quick Reference Guide --> Bulk Data Entries --> PFAST) if pfast_card.MCID == -1: # Calculate vectors vector1 = np.array([nodoB.X - nodoA.X, nodoB.Y - nodoA.Y, nodoB.Z - nodoA.Z]) vector1 /= np.linalg.norm(vector1) indice_min = np.argmin(np.abs(vector1)) aux_vector = np.zeros(3) aux_vector[indice_min] = 1 vector2 = aux_vector - (np.dot(aux_vector, vector1) / np.dot(vector1, vector1)) * vector1 vector2 /= np.linalg.norm(vector2) #vector3 = np.cross(vector1, vector2) else: raise NotImplementedError("CODE IS ONLY PREPARED FOR CARD PFAST WHOSE MCID = -1") # Element A componentA = self.__model.ElementsDict[(card.IDA, 0)] axisA = componentA.ElemSystemArray elem_Z_A = np.array(axisA[6:]) thetaA = componentA.AngleMat axis_matA_X = (componentA.Nodes[1].X - componentA.Nodes[0].X, componentA.Nodes[1].Y - componentA.Nodes[0].Y, componentA.Nodes[1].Z - componentA.Nodes[0].Z) # Element B componentB = self.__model.ElementsDict[(card.IDB, 0)] axisB = componentB.ElemSystemArray elem_Z_B = np.array(axisB[6:]) thetaB = componentB.AngleMat axis_matB_X = (componentB.Nodes[1].X - componentB.Nodes[0].X, componentB.Nodes[1].Y - componentB.Nodes[0].Y, componentB.Nodes[1].Z - componentB.Nodes[0].Z) # Retrieve properties propA = self.__model.PropertyDict.get(componentA.Prop) propB = self.__model.PropertyDict.get(componentB.Prop) return propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X def __update_fastener(self, fastener_ID: int) -> None: """ Returns the input that will be used to compute the elastic modulus of a fastener. This function is developed for the specific case of a connector CFAST connected by ELEM type. Args: fastener_ID (int): ID of fastener to be updated. Returns: None """ tinit = time.time() # Check that the shear type and the connection type are valid if self.__fastener_properties[fastener_ID]['shear_type'] not in ["simple", "double"]: raise ValueError(f"{self.__fastener_properties['shear_type']} IS NOT A VALID SHEAR TYPE") if self.__fastener_properties[fastener_ID]['connection_type'] not in ["bolt", "rivet"]: raise ValueError(f"{self.__fastener_properties['connection_type']} IS NOT A SUPPORTED CONNECTION TYPE") # Retrieve the card from the dictionary card = self.__c_rivet[fastener_ID] # Joint model (CBUSH/CFAST) kind_union = card.CharName info_rivet = self.__fastener_properties[fastener_ID] # Nodes of the rivet nodoA, nodoB = self.__nodes_crivet[fastener_ID] # Types: if kind_union == "CBUSH": # There are two cases associated to CBUSH: connected to RBE3 and connected to two nodes # CBUSH connected to RBE3 if hasattr(nodoA.Connectivity[0], 'TypeConnector') and nodoA.Connectivity[0].TypeConnector == "RBE3" and \ nodoB.Connectivity[0].TypeConnector == "RBE3": propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X = self.__CBUSH_connected_RBE3(card, nodoA, nodoB) # CBUSH connected to nodes elif hasattr(nodoA.Connectivity[0], 'TypeElement'): propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X = self.__CBUSH_connected_nodes(card, nodoA, nodoB) else: raise NotImplementedError("CBUSH FOR THIS TYPE OF CONNECTOR IS NOT SUPPORTED") elif kind_union == "CFAST": # There are two type of CFAST: connected by elements (ELEM) or connected by properties (PROP) # Connected by ELEM if card.TYPE == 'ELEM': propA, propB, vector2, thetaA, thetaB, axis_matA_X, axis_matB_X = self.__CFAST_connected_ELEM(card, nodoA, nodoB) # Connected by PROP elif card.TYPE == 'PROP': raise NotImplementedError("THE JOINT CFAST CONNECTED BY PROP IS NOT SUPPORTED") # Any other type of joint will raise an error else: raise NotImplementedError("THE TYPE OF JOINT SELECTED IS NOT SUPPORTED") # Compute the thickness of the components thickness = [] for comp in [propA, propB]: if comp.PropertyType == "PSHELL": thickness.append(comp.Thickness) elif comp.PropertyType == "PCOMP": thickness.append(sum(comp.Thickness)) thicknA, thicknB = thickness # Compute the elastic modulus in the direction of the rivet ExA, EyA = self.__elastic_calculus(propA, vector2, thetaA, axis_matA_X) ExB, EyB = self.__elastic_calculus(propB, vector2, thetaB, axis_matB_X) # Compute the axial stiffness axial = self.__axial_stiffness(fastener_ID,thicknA, thicknB) # In order to compute the shear stiffness, we have to consider the method if self.__stiffness_method == "HUTH": shear_Y = self.__huth_shear(fastener_ID, ExA, thicknA, propA.PropertyType, ExB, thicknB, propB.PropertyType) shear_Z = self.__huth_shear(fastener_ID, EyA, thicknA, propA.PropertyType, EyB, thicknB, propB.PropertyType) elif self.__stiffness_method == "TATE_ROSENFELD": nu_f = info_rivet["nu"] shear_Y = self.__tate_rosenfeld_shear(fastener_ID, ExA, thicknA, ExB, thicknB) shear_Z = self.__tate_rosenfeld_shear(fastener_ID, EyA, thicknA, EyB, thicknB) elif self.__stiffness_method == "BOEING": shear_Y = self.__boeing_shear(fastener_ID, ExA, thicknA, ExB, thicknB) shear_Z = self.__boeing_shear(fastener_ID, EyA, thicknA, EyB, thicknB) elif self.__stiffness_method == "GRUMMAN": shear_Y = self.__grumman_shear(fastener_ID, ExA, thicknA, ExB, thicknB) shear_Z = self.__grumman_shear(fastener_ID, EyA, thicknA, EyB, thicknB) elif self.__stiffness_method == "SWIFT": shear_Y = self.__swift_shear(fastener_ID, ExA, thicknA, ExB, thicknB) shear_Z = self.__swift_shear(fastener_ID, EyA, thicknA, EyB, thicknB) elif self.__stiffness_method == "NELSON": shear_Y = self.__nelson_shear(fastener_ID, ExA, thicknA, ExB, thicknB) shear_Z = self.__nelson_shear(fastener_ID, EyA, thicknA, EyB, thicknB) else: raise NotImplementedError( f"{self.__stiffness_method} IS NOT A SUPPORTED CALCULUS METHOD FOR FASTENER SHEAR STIFFNESS") # Update stiffness depending on the type of joint prop_card = self.__p_rivet[card.PID] if prop_card.CharName == "PBUSH": prop_card.K1 = axial prop_card.K2 = shear_Y prop_card.K3 = shear_Z elif prop_card.CharName == "PFAST": prop_card.KT1 = axial prop_card.KT2 = shear_Y prop_card.KT3 = shear_Z N2PLog.Debug.D503(time.time(), tinit) # --------------------------------Main function updating the PFAST and PBUSH cards --------------------------------------
[docs] def calculate(self) -> None: """Update properties of fasteners in a model. Reads a model from a BDF file and updates the properties of PFAST and PBUSH elements. If a list of fastener IDs is provided, it updates the properties of those elements. Args: "None" Returns: "None" """ for fastener_ID in self.__fastener_properties.keys(): self.__update_fastener(fastener_ID) print(f"Fastener updated: {fastener_ID}")
[docs] def write_bdf(self, out_folder: str): self.__model.ModelInputData.rebuild_file(out_folder)
@property def StiffnessMethod(self) -> str: """ Returns the name of the method used to calculate the fastener stiffness """ return self.__stiffness_method @StiffnessMethod.setter def StiffnessMethod(self,stiffness_method: str) -> None: """ Set the method used to calculate the fastener stiffness """ self.__stiffness_method = stiffness_method @property def FastenerInformation(self) -> dict: """ Returns the dictionary mapping each fastener with its properties """ return self.__fastener_properties @FastenerInformation.setter def FastenerInformation(self, fastener_properties: dict) -> None: """ Set the dictionary mapping each fastener with its properties """ self.__fastener_properties = fastener_properties\ @property def IDList(self) -> Union[list,list[list]]: """ Returns the list of fasteners to be updated """ return self.__id_list @IDList.setter def IDList(self, id_list: Union[list,list[list]]) -> None: """ Set the list of fasteners to be updated """ self.__id_list = id_list self.__mapping_fastener_properties() @property def InfoDicts(self) -> Union[dict,list[dict]]: """ Returns the list of dictionaries containing the fastener information """ if len(self.__info_list) == 1: return self.__info_list[0] else: return self.__info_list @InfoDicts.setter def InfoDicts(self, info_dict: Union[list,list[list]]) -> None: """ Set the list of dictionaries containing the fastener information """ if len(info_dict) == 1: self.__info_list = [info_dict] else: self.__info_list = info_dict