diff --git a/doc/img/Beam Image 2 BMD.png b/doc/img/Beam Image 2 BMD.png index 2f78aab..0b0173c 100644 Binary files a/doc/img/Beam Image 2 BMD.png and b/doc/img/Beam Image 2 BMD.png differ diff --git a/doc/img/Beam Image 2 SFD.png b/doc/img/Beam Image 2 SFD.png index fbd0103..9d70309 100644 Binary files a/doc/img/Beam Image 2 SFD.png and b/doc/img/Beam Image 2 SFD.png differ diff --git a/doc/img/Beam Image 2.png b/doc/img/Beam Image 2.png index 7458d6f..8adedf3 100644 Binary files a/doc/img/Beam Image 2.png and b/doc/img/Beam Image 2.png differ diff --git a/doc/readthedocs/planesections/analysis.py b/doc/readthedocs/planesections/analysis.py index 9d93905..2ae0ef6 100644 --- a/doc/readthedocs/planesections/analysis.py +++ b/doc/readthedocs/planesections/analysis.py @@ -33,15 +33,9 @@ def __init__(self, beam2D:Beam2D): def getEleInteralForce(self, nodID): """ - 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. - - N-1 L R N - .------.------. - N-1 N N+1 - + of a section cut. """ @@ -51,8 +45,6 @@ def getEleInteralForce(self, nodID): # 0 is used to so that the plot "closes", i.e. starts at zero the goes up Fint[:3] = 0 # Left side forces Fint[3:] = op.eleForce(eleR)[:3] # Right side forces - # Fint[3:] = op.eleForce(nodeID)[:3] # Right side forces - # Fint[:3] = Fint[3:] # Left side forces #Direct Check, this is scary. elif nodID == self.nodeIDEnd: # right side node diff --git a/doc/readthedocs/planesections/archetypes.py b/doc/readthedocs/planesections/archetypes.py deleted file mode 100644 index a0cd3ff..0000000 --- a/doc/readthedocs/planesections/archetypes.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Mar 5 20:57:19 2022 - -@author: Christian - -These are the abstract classes a user needs in order to "hook" into. - -They define the promise we make - if people match these classes, they -can hook into the beam classes. -""" - -from abc import ABC, abstractmethod - -# ============================================================================= -# Archetype classes -# ============================================================================= - - -class LabelArchetype: - - pass - - """ - Methods to be determined closer to implementation. - The methods used by the BeamPlotter will go here. - """ - - - -class NodeArchetype(ABC): - """ - Represents a node/point of interst on the diagram. - """ - @abstractmethod - def getFixityType(): - """ - Gets the fixity type used. - Currently supported - free, roller, pin, fixed. - """ - pass - - @abstractmethod - def getPosition(): - """ - Gets the x position of the node. - Currently supported - roller, pin, fixed. - """ - - pass - - @abstractmethod - def getLabel(): - """ - Returns the label that has been assigned to the node/ - ???: - what values are supported?? - """ - pass - -class BeamArchetype(ABC): - """ - Archetype class of data beams need in order to use make use of the beam - diagram class - - ???: - Should nodes be given a required internal naming convention? - """ - nodes = [] - NodeArchetype = None - forces = [] - distForces = [] - - @abstractmethod - def getNodes(): - """ - Returns all noed objects in the beam. - """ - pass - - @abstractmethod - def getForces(): - """ - Gets all applied point Loads. - """ - pass - - @abstractmethod - def getDistForses(): - """ - Gets all applied distrubuted Loads. - """ - pass - - - @abstractmethod - def setNodeArchetype(): - """ - Sets the archetype method used to create new nodes. - """ - pass - - @abstractmethod - def addNode(self, xCoord, fixity, label=None): - """ - Adds a new node to the model. - """ - pass - - -class ForceArchetype(ABC): - """ - Represents a force distributed across multiple nodes - """ - FixityTypes = ['Fixed', 'Pinned', 'Roller'] - - @abstractmethod - def getConnectedNode(): - """ - - """ - pass - - @abstractmethod - def getMagnitudes(): - """ - Returns the magnitude of force at each of the connected nodes - """ - pass - - - @abstractmethod - def getLabelNodes(): - """ - Returns the nodes that are labeled - """ - pass - -class DistrubtedForceArchetype(ABC): - """ - Represents a force distributed across multiple nodes, i.e. uniform, etc - """ - - @abstractmethod - def getConnectedNode(): - """ - - """ - pass - - @abstractmethod - def getMagnitudes(): - """ - Returns the magnitude of force at each of the connected nodes - """ - pass - - - @abstractmethod - def getLabelNodes(): - """ - Returns the nodes that are labeled - """ - pass - - -# ============================================================================= -# -# ============================================================================= - -class BeamDiagramArchetype(ABC): - """ - The beam class will manage the beam diagram and holds it's current state. - Currently this only really makes sense using Matplotlib. - """ - - @abstractmethod - def plotPinned(): - """ - Makes a plot of the pin elements. - """ - pass - - @abstractmethod - def plotRoller(): - """ - Makes a plot of the roller elements. - """ - pass - - @abstractmethod - def plotFixed(): - """ - Makes a plot of the roller elements. - """ - pass - - - - diff --git a/doc/readthedocs/planesections/builder.py b/doc/readthedocs/planesections/builder.py index 4156e10..49be6e1 100644 --- a/doc/readthedocs/planesections/builder.py +++ b/doc/readthedocs/planesections/builder.py @@ -17,14 +17,6 @@ - -# ============================================================================= -# Problems: -# ============================================================================= -# Summarize Nodes? -# Summarize Loads? - - class Section2D(): E:None A:None @@ -88,7 +80,7 @@ class Node2D(NodeArchetype): A name for the node. This can be displayed in the plots. The default is ''. """ - + _dimension = '2D' def __init__(self, x, fixity, label = ''): @@ -125,9 +117,9 @@ def __repr__(self): def getFixityType(self): """ - ??? - This isn't great, but we can't hash lists so I'm not sure how to - improve it. + Returns the type of beam fixity for supported 2D fixities. + Currently only free, roller, pinned, and fixed are supported. + """ if list(self.fixity) == [0,0,0]: return 'free' diff --git a/doc/readthedocs/planesections/diagram.py b/doc/readthedocs/planesections/diagram.py index 9de7223..24794aa 100644 --- a/doc/readthedocs/planesections/diagram.py +++ b/doc/readthedocs/planesections/diagram.py @@ -279,15 +279,6 @@ def setNodeArchetype(self, nodeArchetype:NodeArchetype = Node): - - - - - - - - - # ============================================================================= # # ============================================================================= @@ -313,9 +304,6 @@ def plotRoller(): def plotFixed(): pass - - - class BasicBeamDiagram(BeamDiagram): """ @@ -603,21 +591,26 @@ def plotLabel(self, xy0, label): -class BeamPlotter: - - """ - The interface between the high level beam abstraction and plotting lower - level plotting. - - Note, the diagram has been rescaled so the beam has lenght scaled to the - maximum beam size of 8. - This is to make consistent plotting easier across a number of beam sizes, - however, the matplotlib objects in the plot have different value than - the actual beam. - - """ - +class BeamPlotter2D: def __init__(self, beam, figsize = 8): + """ + Used to make a diagram of the beam. Only certain fixities are supported + for plotting, including free, roller (support only in y), pinned (support in x and y), + and fixed (support in x/y/rotation). + Only certain forces are supported for plotting - for distrubuted + forces only the y component of the beam can be plotted. + + Note, the diagram has been rescaled so the beam has lenght scaled to the + maximum beam size of 8. + This is to make consistent plotting easier across a number of beam sizes, + however, the matplotlib objects in the plot have different value than + the actual beam. + + The class used as a interface between the high level beam abstraction + and lower level rules plotting. + + + """ self.beam = beam self.figsize = figsize @@ -632,7 +625,6 @@ def __init__(self, beam, figsize = 8): self.xmax = xlims[0] xlimsPlot = [(xlims[0] - L/20) / xscale, (xlims[1] + L/20) / xscale] - # The ylimit is ylimsPlot = [-L/10 / xscale, L/10 / xscale] self.plotter._initPlot(self.figsize, xlimsPlot, ylimsPlot) @@ -644,7 +636,6 @@ def plotBeam(self): self.plotter.plotBeam(xy0, xy1) def plotSupports(self): - for node in self.beam.nodes: fixityType = node.getFixityType() x = node.getPosition() @@ -661,10 +652,24 @@ def plotSupports(self): kwargs = {'isLeft':False} xy = [x / self.xscale, 0] + # plot the appropriate option without an if statement! self.plotter.supportPlotOptions[fixityType](xy, **kwargs) def plot(self, **kwargs): + """ + Plots the beam, supports, point forces, element forces, and labels. + + Note that forces have a "static" and "adaptive" portion of their length + . This means that arrows can't have values length less than a certain + length, preventing very small arrows from being plotted that look silly. + However, this also means that the ratio between different arrows won't + be exactly the ratio between their force magnitude. + + Only the vertical components of distributed forces are plotted. + + """ + self.plotBeam() self.plotSupports() if self.beam.pointLoads: @@ -674,6 +679,10 @@ def plot(self, **kwargs): self.plotLabels() def plotLabels(self): + """ + Adds all labels to the plot. Labels are offset from the point in the + x and y. + """ for node in self.beam.nodes: label = node.label if label: @@ -688,12 +697,9 @@ def _getForceVectorLength(self, forces, vectScale = 1): and a dynamic component that adapts to the magnitude of forces. The output plotting forces are in the direction they act. - - - + """ # Normalize forces - print(forces) forces = np.array(forces) signs = np.sign(forces) Fmax = np.max(np.abs(forces), 0) @@ -717,6 +723,8 @@ def _getForceVectorLength(self, forces, vectScale = 1): def plotPointForces(self): """ + Plots all point forces. + Forces have a static portion to their length and dynamic portion. This means that arrows can't have length less than a certain value. This prevents small from being plotted that look silly. @@ -745,8 +753,12 @@ def plotPointForces(self): else: self.plotter.plotPointForce(x - Px, -Py, Px, Py) - def plotEleForces(self): + """ + Plots all distributed forces. Only vertical forces can be plotted. + If a horizontal component is supplied to the force, it is not included + in the plot. + """ # The spacing between force lines spacing = self.beam.getLength() / 25 / self.xscale @@ -757,21 +769,19 @@ def plotEleForces(self): forces.append(force.P) xcoords.append([force.x1 / self.xscale, force.x2 / self.xscale]) - print(forces) fplot = self._getForceVectorLength(forces, vectScale = 0.4) ycoords = self._getStackedPositions(xcoords, fplot) - + NLoads = len(forces) for ii in range(NLoads): Px, Py = fplot[ii] x1, x2 = xcoords[ii] y1 = ycoords[ii] # y1 is the start point of the arrow - if (Px != 0 ): - print("Waring: Px is supploed to distributed load, but plotting isn't supported for these force types.") + print("WARNING: A force with an X component is being used, but plotting isn't supported for this force type.") if (Py == 0): - print("Waring: distributed load has no vertical component.") + print("WARNING: Distributed load has no vertical component.") else: # This is a little akward, but Py is added to account for the offset of -Py in the base funciton. self.plotter.plotVerticalLineLoad(x1, x2, Py, y1 + Py, spacing=spacing) @@ -793,7 +803,6 @@ def _getStackedPositions(self, xcoords: list[list[float]], fplot): fplotOut = np.zeros_like(fplot) fplotOut[:] = fplot - print(fplot) # the current x and y points being plotted. plottedxCoords = [] @@ -802,87 +811,59 @@ def _getStackedPositions(self, xcoords: list[list[float]], fplot): # start at the widest items and plot them first for ind in sortedInds: - dy = fplotOut[ind][1] - y0 = self.getStackedDatum(xcoords[ind], plottedxCoords, plottedyCoords) - - # y = - # if positive + y0 = self._getStackedDatum(xcoords[ind], plottedxCoords, plottedyCoords) + ycoords[ind] = y0 - dy plottedxCoords.append(xcoords[ind]) plottedyCoords.append(ycoords[ind]) - # plottedyCoords.append([y, y - dy]) - # plottedDyCoords.append(dy) - - - print(y0) - print(dy) - print(plottedyCoords) - # print(plottedDyCoords) - - # lengths[ii] = xcoords[ii][1] - xcoords[ii][0] - # print(ycoords) + return ycoords - def checkIfInRange(self, xtest, x1,x2): - # print((x1 <= xtest)) - # print((xtest <= x2)) - + def _checkIfInRange(self, xtest, x1,x2): if (x1 < xtest) and (xtest < x2): return True - return False - - - - def getStackedDatum(self, xCurrent, currentRanges, plottedY): + def _getStackedDatum(self, xCurrent, currentRanges, plottedY): """ Starting at the top of the force stack, check each force to see if it intersects with any other forces. """ Npoint = len(currentRanges) - for ii in range(Npoint): localInd = Npoint - 1 - ii x1, x2 = currentRanges[localInd] - if self.checkIfInRange(xCurrent[0], x1, x2): + if self._checkIfInRange(xCurrent[0], x1, x2): return plottedY[localInd] - if self.checkIfInRange(xCurrent[0], x1, x2): + if self._checkIfInRange(xCurrent[0], x1, x2): return plottedY[localInd] - return 0 - - - def normalizeForces(self): - """ - Reads in all the forces, then normalizes them to fit in the pot space - """ - pass - - def getForceIntersection(self, x1:list, x2:list): - """ - Finds which forces overlap - """ - - + def plotBeamDiagram(beam): """ - Creates a diagram of the created beam. Only certain load types are supported, - including - - Distributed loads do not stack on top of eachother - - The resulting diagram is a matplotlib - figure that can be furthe manipulated. + Creates a diagram of the created beam. + Only certain fixities are supported for plotting, including free, roller + (support only in y), pinned (support in x and y), and fixed + (support in x/y/rotation). + Only certain forces are supported for plotting - for distrubuted + forces only the y component of the beam can be plotted. + + Note, the diagram has been rescaled so the beam has lenght scaled to the + maximum beam size of 8. + This is to make consistent plotting easier across a number of beam sizes, + however, the matplotlib objects in the plot have different value than + the actual beam. + + The resulting diagram is a matplotlib figure that can be further manipulated. Parameters ---------- - beam : TYPE - DESCRIPTION. + beam : PlaneSections beam + The Beam Object to be plotted. Returns ------- @@ -891,7 +872,7 @@ def plotBeamDiagram(beam): ax : matplotlib ax """ - diagram = BeamPlotter(beam) + diagram = BeamPlotter2D(beam) diagram.plot() return diagram.plotter.fig, diagram.plotter.ax diff --git a/doc/readthedocs/planesections/postprocess.py b/doc/readthedocs/planesections/postprocess.py index 02f4dc3..51b0d21 100644 --- a/doc/readthedocs/planesections/postprocess.py +++ b/doc/readthedocs/planesections/postprocess.py @@ -1,11 +1,9 @@ import openseespy.opensees as op import numpy as np import matplotlib.pyplot as plt - from planesections.builder import Node2D, Beam2D - def getInternalForces2D(node:Node2D, ind): """ 0 = axial force @@ -45,7 +43,6 @@ def _plotAxis(ax, xcoords, xunit, yunit, baseY = 'Internal Force'): ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) - def _plotLabels(ax, xcoords, ycoords, labels): offsetx = (max(xcoords) - min(xcoords)) / 100 diff --git a/doc/readthedocs/planesections/units/metric.py b/doc/readthedocs/planesections/units/metric.py new file mode 100644 index 0000000..4fab2be --- /dev/null +++ b/doc/readthedocs/planesections/units/metric.py @@ -0,0 +1,11 @@ +m = 1 +cm = 0.01 +mm = 0.001 +N = 1 +kN = 10**3 +MN = 10**6 +Pa = 1 +kPa = 1000 +MPa = 10**6 +GPa = 10**9 +kg = 1 \ No newline at end of file diff --git a/doc/readthedocs/source/rst/diagram-beamPlotter.rst b/doc/readthedocs/source/rst/diagram-beamPlotter.rst index f818e1d..e1803e7 100644 --- a/doc/readthedocs/source/rst/diagram-beamPlotter.rst +++ b/doc/readthedocs/source/rst/diagram-beamPlotter.rst @@ -1,6 +1,10 @@ Beam Plotter ============ +The class used to plot beam diagrams. +While the beam can analyze all support types, currently not all support types can be plotted. +Only free, roller, pin, and fixed supports can be plotted. + .. autoclass:: planesections.diagram.BeamPlotter :members: :undoc-members: diff --git a/doc/readthedocs/source/rst/diagram-plotBeamDiagram.rst b/doc/readthedocs/source/rst/diagram-plotBeamDiagram.rst index b1fa177..67d8829 100644 --- a/doc/readthedocs/source/rst/diagram-plotBeamDiagram.rst +++ b/doc/readthedocs/source/rst/diagram-plotBeamDiagram.rst @@ -1,4 +1,8 @@ Plot Beam Diagram ================= +The class used to plot beam diagrams. +While the beam can analyze all support types, currently not all support types can be plotted. +Only free, roller, pin, and fixed supports can be plotted. + .. autofunction:: planesections.diagram.plotBeamDiagram diff --git a/doc/readthedocs/source/rst/diagram.rst b/doc/readthedocs/source/rst/diagram.rst index 32e3e00..ec6b678 100644 --- a/doc/readthedocs/source/rst/diagram.rst +++ b/doc/readthedocs/source/rst/diagram.rst @@ -3,6 +3,8 @@ Diagram The diagram class is used to plot representations of the beam. These output plots will include labels, loads, and support fixities. +While the beam can analyze all support types, currently not all support types can be plotted. +Only free, roller, pin, and fixed supports can be plotted. #. :doc:`diagram-plotBeamDiagram` #. :doc:`diagram-beamPlotter` diff --git a/example/Ex1.py b/example/Ex1.py index 39b6d63..37c0c8a 100644 --- a/example/Ex1.py +++ b/example/Ex1.py @@ -1,14 +1,8 @@ -# -*- coding: utf-8 -*- -R""" -Created on Sun May 23 01:00:41 2021 - -@author: Christian - -The following example shows a basic implementatation of the beam class. +""" +The following is a basic demo of how planesections works A simply supported beam is created, a diagram of it is made, then the beam is -and analyzed. +and analyzed and results are output """ - import planesections as ps # Define node locations, and support conditions @@ -17,14 +11,14 @@ # Define beam and fixities pinned = [1,1,0] -beam.setFixity(L*0.1, pinned, label = '1') -beam.setFixity(L*0.9, pinned) +beam.setFixity(L*0.1, pinned, label = 'A') +beam.setFixity(L*0.9, pinned, label = 'B') # Define point Loads -Pz = -1 -beam.addVerticalLoad(0, Pz, label = 'A') -beam.addVerticalLoad(L*0.5, 2*Pz, label = 'B') -beam.addVerticalLoad(L, Pz, label = 'C') +Pz = -1000 +beam.addVerticalLoad(0, Pz, label = 'C') +beam.addVerticalLoad(L*0.5, 2*Pz, label = 'D') +beam.addVerticalLoad(L, Pz, label = 'E') # Define distributed Loads beam.addDistLoadVertical(0, L, Pz) @@ -38,7 +32,5 @@ analysis.runAnalysis() # Plot the SFD and BMD -ps.plotShear2D(beam) -ps.plotMoment2D(beam) - - +ps.plotShear2D(beam, scale = 0.001, yunit = 'kN') +ps.plotMoment2D(beam, scale = 0.001, yunit = 'kNm') \ No newline at end of file diff --git a/example/Ex2.py b/example/Ex2.py index 265c0d4..d71571a 100644 --- a/example/Ex2.py +++ b/example/Ex2.py @@ -1,51 +1,66 @@ -# -*- coding: utf-8 -*- -R""" -Created on Sun May 23 01:00:41 2021 +""" +The following is a more realist example that uses user defined section +propreties for the beam. -@author: Christian """ import planesections as ps -from planesections.diagram import plotBeamDiagram - import numpy as np """ -Start by defining the units for the problem, then the section dimensions and -material propreties +Start by defining the units for the problem, the outputs need to be in a consistent +unit base for FEM. """ +from planesections.units.metric import m, mm, kN, GPa -GPa = 10**9 -m = 1 -mm = 0.001 -kN = 1000 - +""" +Define the material properties then make the rectangular section. +""" E = 9*GPa d = 300*mm w = 265*mm -L = 5*m -Loffset = 0.5*m +section = ps.SectionRectangle(E, d, w) +""" +Define the beam. In this example we will define the node coordinants directly, +then add those to the beam. By manually defining the node coordinants, it +is possible to place them anywhere in the beam desired. +""" +L = 10*m +Loffset = 0.5*m +beam = ps.EulerBeam2D(section = section) x = np.linspace(0, L, 80) -fixed = np.array([1, 1, 0]) -q = np.array([0.,-1*kN]) +beam.addNodes(x) -section = ps.SectionRectangle(E, d, w) -beam = ps.EulerBeam2D(section = section) +""" +Define the node fixities. The fixity is a list for each DOF, where 1 represents +fixed, and 0 represents free in this case two pin constraints are applied to the +beam. Lists or Numpy arrays can be used for fixities. +""" +pin = np.array([1, 1, 0]) +beam.setFixity(Loffset, pin) +beam.setFixity(L - Loffset, pin) -beam.addNodes(x) -beam.setFixity(Loffset, fixed) -beam.setFixity(L - Loffset, fixed) +""" +Define the beam nodes loads +""" +q = np.array([0.,-1*kN]) beam.addVerticalLoad(0, -5*kN) beam.addVerticalLoad(L *0.7, -5*kN) beam.addVerticalLoad(L, -5*kN) beam.addDistLoad(0, L, q) -plotBeamDiagram(beam) +ps.plotBeamDiagram(beam) +""" +Run the analysis +""" analysis = ps.OpenSeesAnalyzer2D(beam) analysis.runAnalysis() -ps.OutputRecorder2D(beam) -ps.plotDisp2D(beam, scale=1000) -ps.plotRotation2D(beam, scale=1000) + +""" +Plot results +""" +ps.plotDisp2D(beam, scale=1000, yunit = 'mm') +ps.plotRotation2D(beam, scale=1000, yunit = 'mrad') diff --git a/example/Ex2b.py b/example/Ex2b.py new file mode 100644 index 0000000..2f12a84 --- /dev/null +++ b/example/Ex2b.py @@ -0,0 +1,70 @@ +""" +The following is a more realist example that uses user defined section +propreties for the beam. Units are in imperial. + +""" + +import planesections as ps +import numpy as np + +""" +Start by defining the units for the problem, the outputs need to be in a consistent +unit base for FEM. In this case, lb and ft are used. +""" +psf = 1 +psi = 144*psf +ksi = 1000*psi +ft = 1 +inch = ft / 12 +kip = 1000 + +""" +Define the material properties then make the rectangular section. +""" +E = 1305*ksi +d = 12*inch +w = 10*inch +section = ps.SectionRectangle(E, d, w) + +""" +Define the beam. In this example we will define the node coordinants directly, +then add those to the beam. By manually defining the node coordinants, it +is possible to place them anywhere in the beam desired. +""" +L = 30*ft +Loffset = 1.5*ft +x = np.linspace(0, L, 80) +beam = ps.EulerBeam2D(section = section) +beam.addNodes(x) + +""" +Define the node fixities. The fixity is a list for each DOF, where 1 represents +fixed, and 0 represents free in this case two pin constraints are applied to the +beam. Lists or Numpy arrays can be used for fixities. +""" +fixed = np.array([1, 1, 0]) +beam.setFixity(Loffset, fixed) +beam.setFixity(L - Loffset, fixed) + +""" +Define the beam nodes loads +""" +q = np.array([0.,-1*kip/ft]) +beam.addVerticalLoad(0, -5*kip) +beam.addVerticalLoad(L *0.7, -5*kip) +beam.addVerticalLoad(L, -5*kip) +beam.addDistLoad(0, L, q) +ps.plotBeamDiagram(beam) + +""" +Run the analysis +""" +analysis = ps.OpenSeesAnalyzer2D(beam) +analysis.runAnalysis() + +""" +Plot results +""" +ps.plotDisp2D(beam, scale=1/inch, yunit = 'in', xunit = 'ft') + + diff --git a/example/Ex3.py b/example/Ex3.py index de25ad3..9f21174 100644 --- a/example/Ex3.py +++ b/example/Ex3.py @@ -1,44 +1,39 @@ -# -*- coding: utf-8 -*- -R""" -Created on Sun May 23 01:00:41 2021 - -@author: Christian +""" +An example with lots of forces. """ -from planesections import EulerBeam2D, OpenSeesAnalyzer2D, plotMoment2D, plotShear2D -from planesections.diagram import plotBeamDiagram +import planesections as ps +from planesections.units.metric import m, kN import numpy as np -kN = 1000 - -# Initialize beam variables -x1 = 0 -x2 = 1 -offset = x2/20 -x = np.linspace(0, x2, 80) -fixed = np.array([1, 1, 0]) -roller = np.array([0, 1, 0]) - -q = np.array([0.,-1*kN]) # element Distributed load - -# Set up the beam -beam = EulerBeam2D(x) -beam.setFixity(x1, fixed) -beam.setFixity(x2, fixed) -beam.setFixity(x2/3, roller) - -# apply forces -beam.addVerticalLoad(offset, -2*kN, 'A') -beam.addVerticalLoad(x2/2, -2*kN, 'B') -beam.addVerticalLoad(x2 - offset, -2*kN, 'C') -beam.addDistLoad(0, x2, q) -plotBeamDiagram(beam) - -# run analysis -analysis = OpenSeesAnalyzer2D(beam) -analysis.runAnalysis() +L = 5*m +x = np.linspace(0,L,80) +beam = ps.EulerBeam2D(x) + +pinned = [1,1,0] +fixed = [1,1,1] +beam.setFixity(0.4, pinned, label = 'A') +beam.setFixity(L, fixed, label = 'B') -plotShear2D(beam) -plotMoment2D(beam) +P = -2*kN +M = 5*kN*m +q = np.array([0.,-1000.]) +beam.addVerticalLoad(0, P,label = 'C') +beam.addVerticalLoad(2.5, P,label = 'D') +beam.addVerticalLoad(4, -P,label = 'E') +beam.addMoment(3, M) + +beam.addDistLoad(0, 3, q) +beam.addDistLoad(0.2,0.5, q*2) +beam.addDistLoad(0.5,3, q*4) +beam.addDistLoad(4, 4.8, -q*4) +beam.addDistLoad(3.2, 4.8, q*2) + +ps.plotBeamDiagram(beam) + +analysis = ps.OpenSeesAnalyzer2D(beam) +analysis.runAnalysis() +ps.plotShear2D(beam,scale = 1/kN, yunit = 'kN') +ps.plotMoment2D(beam,scale = -1/kN, yunit = 'kNm') diff --git a/example/Ex4.py b/example/Ex4.py deleted file mode 100644 index 4b57598..0000000 --- a/example/Ex4.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -R""" -Created on Sun May 23 01:00:41 2021 - -@author: Christian -""" - -from planesections import EulerBeam2D, OpenSeesAnalyzer2D, OutputRecorder2D, plotMoment2D,plotShear2D -from planesections.diagram import plotBeamDiagram -import numpy as np - -x = np.linspace(0,5,80) -fixed = np.array([1,1,0.]) - -P = -1000 -q = np.array([0.,-1000.]) - - -beam = EulerBeam2D(x) - -beam.setFixity(0.4, fixed, label = '1') -beam.setFixity(4.6, fixed) -beam.addVerticalLoad(0, P,label = 'A') -beam.addVerticalLoad(2.5, P,label = 'B') -beam.addVerticalLoad(5, -P,label = 'C') -beam.addDistLoad(0,3, q) -beam.addDistLoad(0.2,0.5, q*2) -beam.addDistLoad(0.5,3, q*4) - -beam.addDistLoad(4, 4.8, -q*4) -beam.addDistLoad(3.2, 4.8, q*2) -plotBeamDiagram(beam) - - -analysis = OpenSeesAnalyzer2D(beam) -analysis.runAnalysis() -OutputRecorder2D(beam) -# fig, ax, line = plotMoment2D(beam) - -# print(beam.Mmax) -plotShear2D(beam) - diff --git a/src/analysis.py b/src/analysis.py index 9d93905..2ae0ef6 100644 --- a/src/analysis.py +++ b/src/analysis.py @@ -33,15 +33,9 @@ def __init__(self, beam2D:Beam2D): def getEleInteralForce(self, nodID): """ - 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. - - N-1 L R N - .------.------. - N-1 N N+1 - + of a section cut. """ @@ -51,8 +45,6 @@ def getEleInteralForce(self, nodID): # 0 is used to so that the plot "closes", i.e. starts at zero the goes up Fint[:3] = 0 # Left side forces Fint[3:] = op.eleForce(eleR)[:3] # Right side forces - # Fint[3:] = op.eleForce(nodeID)[:3] # Right side forces - # Fint[:3] = Fint[3:] # Left side forces #Direct Check, this is scary. elif nodID == self.nodeIDEnd: # right side node diff --git a/src/archetypes.py b/src/archetypes.py deleted file mode 100644 index a0cd3ff..0000000 --- a/src/archetypes.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Mar 5 20:57:19 2022 - -@author: Christian - -These are the abstract classes a user needs in order to "hook" into. - -They define the promise we make - if people match these classes, they -can hook into the beam classes. -""" - -from abc import ABC, abstractmethod - -# ============================================================================= -# Archetype classes -# ============================================================================= - - -class LabelArchetype: - - pass - - """ - Methods to be determined closer to implementation. - The methods used by the BeamPlotter will go here. - """ - - - -class NodeArchetype(ABC): - """ - Represents a node/point of interst on the diagram. - """ - @abstractmethod - def getFixityType(): - """ - Gets the fixity type used. - Currently supported - free, roller, pin, fixed. - """ - pass - - @abstractmethod - def getPosition(): - """ - Gets the x position of the node. - Currently supported - roller, pin, fixed. - """ - - pass - - @abstractmethod - def getLabel(): - """ - Returns the label that has been assigned to the node/ - ???: - what values are supported?? - """ - pass - -class BeamArchetype(ABC): - """ - Archetype class of data beams need in order to use make use of the beam - diagram class - - ???: - Should nodes be given a required internal naming convention? - """ - nodes = [] - NodeArchetype = None - forces = [] - distForces = [] - - @abstractmethod - def getNodes(): - """ - Returns all noed objects in the beam. - """ - pass - - @abstractmethod - def getForces(): - """ - Gets all applied point Loads. - """ - pass - - @abstractmethod - def getDistForses(): - """ - Gets all applied distrubuted Loads. - """ - pass - - - @abstractmethod - def setNodeArchetype(): - """ - Sets the archetype method used to create new nodes. - """ - pass - - @abstractmethod - def addNode(self, xCoord, fixity, label=None): - """ - Adds a new node to the model. - """ - pass - - -class ForceArchetype(ABC): - """ - Represents a force distributed across multiple nodes - """ - FixityTypes = ['Fixed', 'Pinned', 'Roller'] - - @abstractmethod - def getConnectedNode(): - """ - - """ - pass - - @abstractmethod - def getMagnitudes(): - """ - Returns the magnitude of force at each of the connected nodes - """ - pass - - - @abstractmethod - def getLabelNodes(): - """ - Returns the nodes that are labeled - """ - pass - -class DistrubtedForceArchetype(ABC): - """ - Represents a force distributed across multiple nodes, i.e. uniform, etc - """ - - @abstractmethod - def getConnectedNode(): - """ - - """ - pass - - @abstractmethod - def getMagnitudes(): - """ - Returns the magnitude of force at each of the connected nodes - """ - pass - - - @abstractmethod - def getLabelNodes(): - """ - Returns the nodes that are labeled - """ - pass - - -# ============================================================================= -# -# ============================================================================= - -class BeamDiagramArchetype(ABC): - """ - The beam class will manage the beam diagram and holds it's current state. - Currently this only really makes sense using Matplotlib. - """ - - @abstractmethod - def plotPinned(): - """ - Makes a plot of the pin elements. - """ - pass - - @abstractmethod - def plotRoller(): - """ - Makes a plot of the roller elements. - """ - pass - - @abstractmethod - def plotFixed(): - """ - Makes a plot of the roller elements. - """ - pass - - - - diff --git a/src/builder.py b/src/builder.py index fd062bb..49be6e1 100644 --- a/src/builder.py +++ b/src/builder.py @@ -17,18 +17,11 @@ - -# ============================================================================= -# Problems: -# ============================================================================= -# Summarize Nodes? -# Summarize Loads? - - class Section2D(): E:None A:None Ixx:None + Iyy:None @@ -36,17 +29,19 @@ class Section2D(): class SectionBasic2D(Section2D): """ A basic section that contains the global propreties of the beam section, - without any geometry. + without any geometry. It's assume the section is elastic. """ E:float = 1 A:float = 1 Ixx:float = 1 + Iyy:float = 1 @dataclass class SectionRectangle(Section2D): """ - Represents a Rectangular section. + Represents a elastic Rectangular section. Ixx and A are calcualted using + the beam width and height. """ E:float = 200*10**9 @@ -57,6 +52,7 @@ class SectionRectangle(Section2D): def __post_init__(self): self.A = self.d*self.w self.Ixx = self.d**3*self.w / 12 + self.Ixx = self.w**3*self.d / 12 class Node2D(NodeArchetype): @@ -84,7 +80,7 @@ class Node2D(NodeArchetype): A name for the node. This can be displayed in the plots. The default is ''. """ - + _dimension = '2D' def __init__(self, x, fixity, label = ''): @@ -121,9 +117,9 @@ def __repr__(self): def getFixityType(self): """ - ??? - This isn't great, but we can't hash lists so I'm not sure how to - improve it. + Returns the type of beam fixity for supported 2D fixities. + Currently only free, roller, pinned, and fixed are supported. + """ if list(self.fixity) == [0,0,0]: return 'free' @@ -717,10 +713,9 @@ def newEulerBeam2D(x2, x1 = 0, meshSize = 101): if x2 <= x1: raise Exception('x2 must be greater than x1') - x = np.linspace(x1,x2,meshSize) - EulerBeam2D(x) + x = np.linspace(x1, x2, meshSize) - return EulerBeam2D() + return EulerBeam2D(x) diff --git a/src/diagram.py b/src/diagram.py index 25f2820..24794aa 100644 --- a/src/diagram.py +++ b/src/diagram.py @@ -279,15 +279,6 @@ def setNodeArchetype(self, nodeArchetype:NodeArchetype = Node): - - - - - - - - - # ============================================================================= # # ============================================================================= @@ -313,9 +304,6 @@ def plotRoller(): def plotFixed(): pass - - - class BasicBeamDiagram(BeamDiagram): """ @@ -603,21 +591,26 @@ def plotLabel(self, xy0, label): -class BeamPlotter: - - """ - The interface between the high level beam abstraction and plotting lower - level plotting. - - Note, the diagram has been rescaled so the beam has lenght scaled to the - maximum beam size of 8. - This is to make consistent plotting easier across a number of beam sizes, - however, the matplotlib objects in the plot have different value than - the actual beam. - - """ - +class BeamPlotter2D: def __init__(self, beam, figsize = 8): + """ + Used to make a diagram of the beam. Only certain fixities are supported + for plotting, including free, roller (support only in y), pinned (support in x and y), + and fixed (support in x/y/rotation). + Only certain forces are supported for plotting - for distrubuted + forces only the y component of the beam can be plotted. + + Note, the diagram has been rescaled so the beam has lenght scaled to the + maximum beam size of 8. + This is to make consistent plotting easier across a number of beam sizes, + however, the matplotlib objects in the plot have different value than + the actual beam. + + The class used as a interface between the high level beam abstraction + and lower level rules plotting. + + + """ self.beam = beam self.figsize = figsize @@ -632,7 +625,6 @@ def __init__(self, beam, figsize = 8): self.xmax = xlims[0] xlimsPlot = [(xlims[0] - L/20) / xscale, (xlims[1] + L/20) / xscale] - # The ylimit is ylimsPlot = [-L/10 / xscale, L/10 / xscale] self.plotter._initPlot(self.figsize, xlimsPlot, ylimsPlot) @@ -644,7 +636,6 @@ def plotBeam(self): self.plotter.plotBeam(xy0, xy1) def plotSupports(self): - for node in self.beam.nodes: fixityType = node.getFixityType() x = node.getPosition() @@ -661,10 +652,24 @@ def plotSupports(self): kwargs = {'isLeft':False} xy = [x / self.xscale, 0] + # plot the appropriate option without an if statement! self.plotter.supportPlotOptions[fixityType](xy, **kwargs) def plot(self, **kwargs): + """ + Plots the beam, supports, point forces, element forces, and labels. + + Note that forces have a "static" and "adaptive" portion of their length + . This means that arrows can't have values length less than a certain + length, preventing very small arrows from being plotted that look silly. + However, this also means that the ratio between different arrows won't + be exactly the ratio between their force magnitude. + + Only the vertical components of distributed forces are plotted. + + """ + self.plotBeam() self.plotSupports() if self.beam.pointLoads: @@ -674,6 +679,10 @@ def plot(self, **kwargs): self.plotLabels() def plotLabels(self): + """ + Adds all labels to the plot. Labels are offset from the point in the + x and y. + """ for node in self.beam.nodes: label = node.label if label: @@ -688,12 +697,9 @@ def _getForceVectorLength(self, forces, vectScale = 1): and a dynamic component that adapts to the magnitude of forces. The output plotting forces are in the direction they act. - - - + """ # Normalize forces - print(forces) forces = np.array(forces) signs = np.sign(forces) Fmax = np.max(np.abs(forces), 0) @@ -717,6 +723,8 @@ def _getForceVectorLength(self, forces, vectScale = 1): def plotPointForces(self): """ + Plots all point forces. + Forces have a static portion to their length and dynamic portion. This means that arrows can't have length less than a certain value. This prevents small from being plotted that look silly. @@ -745,8 +753,12 @@ def plotPointForces(self): else: self.plotter.plotPointForce(x - Px, -Py, Px, Py) - def plotEleForces(self): + """ + Plots all distributed forces. Only vertical forces can be plotted. + If a horizontal component is supplied to the force, it is not included + in the plot. + """ # The spacing between force lines spacing = self.beam.getLength() / 25 / self.xscale @@ -757,21 +769,19 @@ def plotEleForces(self): forces.append(force.P) xcoords.append([force.x1 / self.xscale, force.x2 / self.xscale]) - print(forces) fplot = self._getForceVectorLength(forces, vectScale = 0.4) ycoords = self._getStackedPositions(xcoords, fplot) - + NLoads = len(forces) for ii in range(NLoads): Px, Py = fplot[ii] x1, x2 = xcoords[ii] y1 = ycoords[ii] # y1 is the start point of the arrow - if (Px != 0 ): - print("Waring: Px is supploed to distributed load, but plotting isn't supported for these force types.") + print("WARNING: A force with an X component is being used, but plotting isn't supported for this force type.") if (Py == 0): - print("Waring: distributed load has no vertical component.") + print("WARNING: Distributed load has no vertical component.") else: # This is a little akward, but Py is added to account for the offset of -Py in the base funciton. self.plotter.plotVerticalLineLoad(x1, x2, Py, y1 + Py, spacing=spacing) @@ -793,7 +803,6 @@ def _getStackedPositions(self, xcoords: list[list[float]], fplot): fplotOut = np.zeros_like(fplot) fplotOut[:] = fplot - print(fplot) # the current x and y points being plotted. plottedxCoords = [] @@ -802,94 +811,68 @@ def _getStackedPositions(self, xcoords: list[list[float]], fplot): # start at the widest items and plot them first for ind in sortedInds: - dy = fplotOut[ind][1] - y0 = self.getStackedDatum(xcoords[ind], plottedxCoords, plottedyCoords) - - # y = - # if positive + y0 = self._getStackedDatum(xcoords[ind], plottedxCoords, plottedyCoords) + ycoords[ind] = y0 - dy plottedxCoords.append(xcoords[ind]) plottedyCoords.append(ycoords[ind]) - # plottedyCoords.append([y, y - dy]) - # plottedDyCoords.append(dy) - - - print(y0) - print(dy) - print(plottedyCoords) - # print(plottedDyCoords) - - # lengths[ii] = xcoords[ii][1] - xcoords[ii][0] - # print(ycoords) + return ycoords - def checkIfInRange(self, xtest, x1,x2): - # print((x1 <= xtest)) - # print((xtest <= x2)) - + def _checkIfInRange(self, xtest, x1,x2): if (x1 < xtest) and (xtest < x2): return True - return False - - - - def getStackedDatum(self, xCurrent, currentRanges, plottedY): + def _getStackedDatum(self, xCurrent, currentRanges, plottedY): """ Starting at the top of the force stack, check each force to see if it intersects with any other forces. """ Npoint = len(currentRanges) - for ii in range(Npoint): localInd = Npoint - 1 - ii x1, x2 = currentRanges[localInd] - if self.checkIfInRange(xCurrent[0], x1, x2): + if self._checkIfInRange(xCurrent[0], x1, x2): return plottedY[localInd] - if self.checkIfInRange(xCurrent[0], x1, x2): + if self._checkIfInRange(xCurrent[0], x1, x2): return plottedY[localInd] - return 0 - - - def normalizeForces(self): - """ - Reads in all the forces, then normalizes them to fit in the pot space - """ - pass - - def getForceIntersection(self, x1:list, x2:list): - """ - Finds which forces overlap - """ - - + def plotBeamDiagram(beam): """ - Creates a diagram of the created beam. Only certain load types are supported, - including - - Distributed loads do not stack on top of eachother - - The resulting diagram is a matplotlib - figure that can be furthe manipulated. + Creates a diagram of the created beam. + Only certain fixities are supported for plotting, including free, roller + (support only in y), pinned (support in x and y), and fixed + (support in x/y/rotation). + Only certain forces are supported for plotting - for distrubuted + forces only the y component of the beam can be plotted. + + Note, the diagram has been rescaled so the beam has lenght scaled to the + maximum beam size of 8. + This is to make consistent plotting easier across a number of beam sizes, + however, the matplotlib objects in the plot have different value than + the actual beam. + + The resulting diagram is a matplotlib figure that can be further manipulated. Parameters ---------- - beam : TYPE - DESCRIPTION. + beam : PlaneSections beam + The Beam Object to be plotted. Returns ------- - None. + fig : matplotlib fig + + ax : matplotlib ax """ - diagram = BeamPlotter(beam) + diagram = BeamPlotter2D(beam) diagram.plot() return diagram.plotter.fig, diagram.plotter.ax diff --git a/src/postprocess.py b/src/postprocess.py index a0041fe..51b0d21 100644 --- a/src/postprocess.py +++ b/src/postprocess.py @@ -1,11 +1,9 @@ import openseespy.opensees as op import numpy as np import matplotlib.pyplot as plt - from planesections.builder import Node2D, Beam2D - def getInternalForces2D(node:Node2D, ind): """ 0 = axial force @@ -17,7 +15,6 @@ def getInternalForces2D(node:Node2D, ind): def _initOutputFig(showAxis, showGrid): fig, ax = plt.subplots(dpi=300) - if not showAxis: ax.axis("off") @@ -46,7 +43,6 @@ def _plotAxis(ax, xcoords, xunit, yunit, baseY = 'Internal Force'): ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) - def _plotLabels(ax, xcoords, ycoords, labels): offsetx = (max(xcoords) - min(xcoords)) / 100 @@ -95,7 +91,9 @@ def plotMoment2D(beam:Beam2D, scale:float=-1, yunit = 'Nm', **kwargs): Returns ------- fig : matplotlib fig + ax : matplotlib ax + line : matplotlib line the plotted line. diff --git a/src/units/metric.py b/src/units/metric.py new file mode 100644 index 0000000..4fab2be --- /dev/null +++ b/src/units/metric.py @@ -0,0 +1,11 @@ +m = 1 +cm = 0.01 +mm = 0.001 +N = 1 +kN = 10**3 +MN = 10**6 +Pa = 1 +kPa = 1000 +MPa = 10**6 +GPa = 10**9 +kg = 1 \ No newline at end of file