Source code for OpenSTRAN.model

import numpy as np

from .Nodes import Nodes
from .Members import Members
from .Solver import Solver


[docs] class Model(): """ 3D space frame structural model. This class represents a three-dimensional space frame for finite element analysis. The model neglects deformations due to transverse shear stresses and torsional warping, operating under the assumption that such effects are negligible in typical civil engineering applications. :param plane: Constrains the model to a two-dimensional plane. Valid values are 'xy', 'yz', or 'xz'. If None, the model is fully three-dimensional. :type plane: str | None :ivar nodes: Collection of nodes in the structure :type nodes: Nodes :ivar members: Collection of members (elements) in the structure :type members: Members :ivar solver: Solver instance for performing structural analysis :type solver: Solver :Example: >>> import OpenSTRAN >>> frame_2d = OpenSTRAN.Model(plane='xy') >>> frame_3d = OpenSTRAN.Model() """ def __init__(self, plane: str | None = None) -> None: """ Initialize a structural model. :param plane: Plane constraint for 2D models ('xy', 'yz', or 'xz'). Defaults to None for 3D models. :type plane: str | None """ self.nodes = Nodes(plane) self.members = Members(self.nodes) self.solver = Solver()
[docs] def solve(self) -> None: """Solve the structural system and compute reactions and member forces. This method performs a complete finite element analysis including: - Assembly and solution of the global stiffness system - Calculation of nodal reactions at restrained DOFs - Determination of member forces and local extrema :returns: None :rtype: None """ self.solver.solve(self.nodes, self.members) self.maxReactions() self.maxMbrForces()
[docs] def maxReactions(self) -> None: """Calculate maximum reaction forces and moments at restrained nodes. Computes the maximum absolute values of reaction forces and moments across all support nodes in the structure. :returns: None :rtype: None Sets the following instance attributes: - Rx_max: Maximum X-direction reaction force - Ry_max: Maximum Y-direction reaction force - Rz_max: Maximum Z-direction reaction force - Rmx_max: Maximum X-direction reaction moment - Rmy_max: Maximum Y-direction reaction moment - Rmz_max: Maximum Z-direction reaction moment """ Rx: list[float] = [] Ry: list[float] = [] Rz: list[float] = [] Rmx: list[float] = [] Rmy: list[float] = [] Rmz: list[float] = [] for node in self.nodes.nodes.values(): if node.mesh_node != False: Rx.append(node.Rx) Ry.append(node.Ry) Rz.append(node.Rz) Rmx.append(node.Rmx) Rmy.append(node.Rmy) Rmz.append(node.Rmz) self.Rx_max = abs(max(Rx, key=lambda x: abs(x))) self.Ry_max = abs(max(Ry, key=lambda x: abs(x))) self.Rz_max = abs(max(Rz, key=lambda x: abs(x))) self.Rmx_max = abs(max(Rmx, key=lambda x: abs(x))) self.Rmy_max = abs(max(Rmy, key=lambda x: abs(x))) self.Rmz_max = abs(max(Rmz, key=lambda x: abs(x)))
[docs] def maxMbrForces(self) -> None: """Calculate maximum member forces and identify local extrema. Computes the maximum values for all member internal forces (axial, shear, torsion, and bending moments) and identifies their local maxima and minima distributions along members. :returns: None :rtype: None Sets the following instance attributes: - axial_max, axial_maxima, axial_minima: Axial force results - torque_max, torque_maxima, torque_minima: Torsional moment results - Vy_max, Vy_maxima, Vy_minima: Y-direction shear force results - Vz_max, Vz_maxima, Vz_minima: Z-direction shear force results - Mzz_max, Mzz_maxima, Mzz_minima: Major axis bending moment results - Myy_max, Myy_maxima, Myy_minima: Minor axis bending moment results """ axial: list[float] = [] torque: list[float] = [] Vy: list[float] = [] Vz: list[float] = [] Mzz: list[float] = [] Myy: list[float] = [] for mbr in self.members.members.values(): for submbr in mbr.submembers.values(): axial.append(submbr.results['axial'][0]) axial.append(submbr.results['axial'][1]*-1) torque.append(submbr.results['torsional moments'][0]) torque.append(submbr.results['torsional moments'][1]*-1) Vy.append(submbr.results['shear'][0]) Vy.append(submbr.results['shear'][1]*-1) Vz.append(submbr.results['transverse shear'][0]) Vz.append(submbr.results['transverse shear'][1]*-1) Mzz.append(submbr.results['major axis moments'][0]) Mzz.append(submbr.results['major axis moments'][1]*-1) Myy.append(submbr.results['minor axis moments'][0]) Myy.append(submbr.results['minor axis moments'][1]*-1) self.axial_max = abs(max(axial, key=lambda x: abs(x))) self.axial_maxima = self.localMaxima(axial) self.axial_minima = self.localMinima(axial) self.torque_max = abs(max(torque, key=lambda x: abs(x))) self.torque_maxima = self.localMaxima(torque) self.torque_minima = self.localMinima(torque) self.Vy_max = abs(max(Vy, key=lambda x: abs(x))) self.Vy_maxima = self.localMaxima(Vy) self.Vy_minima = self.localMinima(Vy) self.Vz_max = abs(max(Vz, key=lambda x: abs(x))) self.Vz_maxima = self.localMaxima(Vz) self.Vz_minima = self.localMinima(Vz) self.Mzz_max = abs(max(Mzz, key=lambda x: abs(x))) self.Mzz_maxima = self.localMaxima(Mzz) self.Mzz_minima = self.localMinima(Mzz) self.Myy_max = abs(max(Myy, key=lambda x: abs(x))) self.Myy_maxima = self.localMaxima(Myy) self.Myy_minima = self.localMinima(Myy)
[docs] def localMaxima(self, forces: list[float]) -> list[bool]: """Identify local maxima in a force distribution. Determines which points in the force distribution represent local maxima by comparing adjacent values, accounting for numerical precision with near-equal comparisons. :param forces: List of force values along a member or structure :type forces: list[float] :returns: Boolean list indicating local maxima positions :rtype: list[bool] """ maxima = [False for _ in forces] length = len(forces)-1 for i, force in enumerate(forces): if i == 0: if force > 0: maxima[i] = True if i % 2 == 0 or i == 1: continue elif i == length: if force > 0: maxima[i] = True else: if forces[i-2] > force: continue if np.isclose(forces[i-2], force) and np.isclose(forces[i+2], force): continue if np.isclose(forces[i-2], force) and forces[i+2] < force: maxima[i] = True continue if forces[i-2] < force and forces[i+2] < force: maxima[i] = True continue if forces[i-2] < force and np.isclose(forces[i+2], force): maxima[i] = True continue return (maxima)
[docs] def localMinima(self, forces: list[float]) -> list[bool]: """Identify local minima in a force distribution. Determines which points in the force distribution represent local minima by comparing adjacent values, accounting for numerical precision with near-equal comparisons. :param forces: List of force values along a member or structure :type forces: list[float] :returns: Boolean list indicating local minima positions :rtype: list[bool] """ minima = [False for _ in forces] length = len(forces)-1 for i, force in enumerate(forces): if i == 0: if force < 0: minima[i] = True if i % 2 == 0 or i == 1: continue elif i == length: if force < 0: minima[i] = True else: if forces[i-2] < force: continue if np.isclose(forces[i-2], force) and np.isclose(forces[i+2], force): continue if np.isclose(forces[i-2], force) and forces[i+2] > force: minima[i] = True continue if forces[i-2] > force and np.isclose(forces[i+2], force): minima[i] = True continue if forces[i-2] > force and forces[i+2] > force: minima[i] = True continue return (minima)
[docs] def reactions(self) -> None: """Print nodal reactions to console. Displays all reaction forces (Rx, Ry, Rz) and reaction moments (Mx, My, Mz) at all restrained support nodes in a formatted table. :returns: None :rtype: None """ print('Nodal Reactions') for node in self.nodes.nodes.values(): if node.mesh_node != True: print(f'\tNode {node.node_ID}:') print(f'\t\tRx = {node.Rx:.2f} kips') print(f'\t\tRy = {node.Ry:.2f} kips') print(f'\t\tRz = {node.Rz:.2f} kips') print(f'\t\tMx = {node.Mx:.2f} kip-ft') print(f'\t\tMy = {node.My:.2f} kip-ft') print(f'\t\tMz = {node.Mz:.2f} kip-ft')