Source code for planesections.analysis.pynite

import numpy as np
import planesections.builder as bb 

from Pynite.FEModel3D import FEModel3D

from .recorder import OutputRecorder

class OutputRecorderPyNite2D(OutputRecorder):
    """
    An interface that can be used to get beam internal forces for each node
    in the model. 
    When called on a beam, it will get all internal forces for that beam.
    Information at each node in the model is stored in the beam.
    The recorder is only is not instantiated at the time of recording. 

    Parameters
    ----------
    beam : planesections Beam2D
        The beam whose data is being recorded.

    """
    lcName = 'Combo 1'
    def __init__(self, beam:bb.Beam, analysisBeam):       
        self.Nnodes = beam.Nnodes
        self.nodeID0 = 1
        self.nodeIDEnd = self.Nnodes
        self.ndf = beam._ndf        
        self.analysisBeam = analysisBeam
        for ii, ID in enumerate(analysisBeam.nodes.keys()):
            analysisNode = analysisBeam.nodes[ID]
            disps = [analysisNode.DX[self.lcName], analysisNode.DY[self.lcName], analysisNode.RZ[self.lcName]]
            rFrc  = [analysisNode.RxnFX[self.lcName], analysisNode.RxnFY[self.lcName], analysisNode.RxnMZ[self.lcName]]
            
            # assign values
            node = beam.nodes[ii]
            node.disps = np.array(disps)
            node.rFrc  = np.array(rFrc)
            node.Fint  = self.getEleInteralForce(ii)

    def _getFint(self, ele):
        PyL, PyR = ele.axial_array(2)[1]
        VyL, VyR = ele.shear_array('Fy', 2)[1]
        MyL, MyR = ele.moment_array('Mz', 2)[1]
        
        return np.array([PyL, VyL, MyL]), np.array([PyR, VyR, MyR])

    def getEleInteralForce(self, nodeID:int):
        """
        Gets the internal force at the left and right side of a node.
        The left and right side forces represent internal force at either side
        of a section cut.       
        
        """
        ndf = self.ndf
        Fint = np.zeros(ndf*2)
        
        nodeID += 1
        
        if nodeID == self.nodeID0: # Left most node
            eleRID = 'M' + str(nodeID)   
            eleR = self.analysisBeam.members[eleRID]
            # 0 is used to so that the plot "closes", i.e. starts at zero the goes up
            Fint[:ndf] =  0    

            FeleR_L, _ = self._getFint(eleR)
            Fint[ndf:] =  FeleR_L    # Left side forces for right side element
                                    
        elif nodeID == self.nodeIDEnd: # right side node
            eleLID = 'M' + str(int(nodeID - 1))   
            eleL = self.analysisBeam.members[eleLID]

            _, FeleL_R = self._getFint(eleL)
            Fint[:ndf] = FeleL_R # Right side forces
            Fint[ndf:] = 0   
                            # Left side forces
        else: # center nodes
        
            eleLID = 'M' + str(int(nodeID - 1))   
            eleRID = 'M' + str(int(nodeID))
            eleL = self.analysisBeam.members[eleLID]
            eleR = self.analysisBeam.members[eleRID]
            _, FeleL_R = self._getFint(eleL)
            FeleR_L, _ = self._getFint(eleR)


            Fint[:ndf] = FeleL_R # left entries
            Fint[ndf:] = FeleR_L # right entries
        
        return Fint


[docs] class PyNiteAnalyzer2D: """ This class is used to can be used to create and run an analysis of an input 2D beam using OpenSeesPy. The nodes, elements, sections, and forces for the beam are defined in the analysis model The PyNite solver makes use of a beam object, which is constructed and stored as a analysisBeam attribute Note, nodes and elements will both start at 0 instead of 1. For the PyNite beam, The 2D directions are X/Y Parameters ---------- beam : planesections Beam2D The beam whose data is being recorded. recorder : planesections Recorder The recorder to use for the output beam. geomTransform: str, optional The OpenSees Geometry transform to use. Can be "Linear" or "PDelta" clearOld : bool, optional A flag that can be used to turn on or off clearing the old analysis when the beam is created. There are some very niche cases where users may want to have mutiple beams at once in the OpenSees model. However, this should remain true for nearly all analyses. Do not turn on unless you know what you're doing. """ def __init__(self, beam2D:bb.Beam, recorder = OutputRecorderPyNite2D): self.beam = beam2D self._checkBeam(beam2D) self.Nnodes = beam2D.Nnodes self.Nele = self.Nnodes - 1 self.recorder = recorder self.nodeAnalysisNames = [] self.memberNames = [] self.matName = 'baseMat' self.sectionName = 'baseSec' def _checkBeam(self, beam2D): if isinstance(beam2D, bb.TimoshenkoBeam): raise Exception("The pynite solver does not support timoshenko beams.") if not beam2D._dimension: raise Exception("The beam has no dimension, something terrible has happened.") if beam2D._dimension != '2D': raise Exception(f"The beam has dimension of type {beam2D._dimension}, it should have type '2D'")
[docs] def initModel(self): """ Initializes the model. Parameters ---------- clearOld : bool, optional A flag that can be used to turn on or off clearing the old analysis when the beam is created. There are some very niche cases where users may want to have mutiple beams at once in the OpenSees model. However, this should remain true for nearly all analyses. Do not turn on unless you know what you're doing. """ self.analysisBeam = FEModel3D()
[docs] def buildNodes(self): """ Adds each node in the beam to the OpenSeesPy model, and assigns that node a fixity. """ analysisBeam = self.analysisBeam for ii, node in enumerate(self.beam.nodes): name = 'N' + str(node.ID) self.nodeAnalysisNames.append(name) analysisBeam.add_node(name, node.x, 0, 0) if node.hasReaction: f1, f2, f3 = node.fixity.fixityValues analysisBeam.def_support(name, f1, f2, True, True, False, f3)
[docs] def buildBeams(self): """ Creates an elastic Euler beam between each node in the model. """ nodeAnalysisNames = self.nodeAnalysisNames E, G, A, Iz, _ = self.beam.getMaterialPropreties() self.analysisBeam.add_material(self.matName, E, G, 0.3, 8000) # The section has it's weak axis propreties set to 1, these are not used. self.analysisBeam.add_section(self.sectionName, A, 1., Iz, 1.) memberNames = [] for ii in range(self.Nele): memberName = 'M' + str(int(ii+1)) N1 = nodeAnalysisNames[ii] N2 = nodeAnalysisNames[ii+1] self.analysisBeam.add_member(memberName, N1, N2, self.matName, self.sectionName) memberNames.append(memberName) self.memberNames = memberNames
[docs] def buildEulerBeams(self): """ Creates an elastic Euler beam between each node in the model. """ print("""DEPRECATION WARNING: buildEulerBeams is being deprecated and will return an error in a future release. Use buildBeams instead.""") self.buildBeams()
def _buildPointLoads(self, pointLoads): for load in pointLoads: node = 'N' + str(load.nodeID) Fx, Fy, M = load.P self.analysisBeam.add_node_load(node, 'FY', Fy) self.analysisBeam.add_node_load(node, 'FX', Fx) self.analysisBeam.add_node_load(node, 'MZ', M)
[docs] def buildPointLoads(self): """ Applies point loads to the appropriate nodes in the model. """ self._buildPointLoads(self.beam.pointLoads)
[docs] def analyze(self): """ Analyzes the model once and records outputs. """ self.analysisBeam.analyze(check_statics=False)
[docs] def buildEleLoads(self): """ Applies element loads to the appropriate elements in the model. """ for eleload in self.beam.eleLoads: N1 = self.beam._findNode(eleload.x1) + 1 N2 = self.beam._findNode(eleload.x2) + 1 build = self._selectLoad(eleload) build([N1, N2], eleload)
def _selectLoad(self, eleload): if isinstance(eleload, bb.EleLoadDist): return self._buildDistLoad if isinstance(eleload, bb.EleLoadLinear): return self._buildLinLoad def _buildDistLoad(self, Nodes:list[int], eleload:bb.EleLoadDist): N1, N2 = Nodes memberNames = self.memberNames q = eleload.P # We subtract one because node names are the index +1 for ii in range(N1-1, N2-1): memberName = memberNames[ii] self.analysisBeam.add_member_dist_load(memberName, 'Fy', q[1], q[1]) self.analysisBeam.add_member_dist_load(memberName, 'Fx', q[0], q[0]) def _buildLinLoad(self, Nodes:list[int], eleload:bb.EleLoadLinear): N1, N2 = Nodes memberNames = self.memberNames q = eleload.P for ii in range(N1-1, N2-1): # shift one back because indicies are one less memberName = memberNames[ii] Node1 = self.beam.nodes[ii] Node2 = self.beam.nodes[ii + 1] qx1, qx2 = eleload.getLoadComponents(Node1.x, Node2.x, q[0]) qy1, qy2 = eleload.getLoadComponents(Node1.x, Node2.x, q[1]) self.analysisBeam.add_member_dist_load(memberName, 'Fy', qy1, qy2) self.analysisBeam.add_member_dist_load(memberName, 'Fx', qx1, qx2) def _getBeam(self): return self.analysisBeam
[docs] def runAnalysis(self, recordOutput = True): """ Makes and analyzes the beam with PyNite. Returns ------- None. """ self.initModel() self.buildNodes() self.buildBeams() self.buildPointLoads() self.buildEleLoads() self.analyze() if recordOutput == True: self.recorder(self.beam, self.analysisBeam)