diff --git a/.coverage b/.coverage new file mode 100644 index 00000000..8e895862 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 746e9131..633b57a4 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,7 +11,6 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: fail-fast: false @@ -20,22 +19,40 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 pytest - python -m pip install scipy + python -m pip install vtk + python -m pip install coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with unittest run: | - python -m unittest Testing + xvfb-run -a coverage run --source=PyNite -m unittest discover + + - name: Convert coverage to XML + run: | + coverage xml -o .coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + file: .coverage.xml + flags: unittests + name: codecov-coverage + token: ${{ secrets.CODECOV_TOKEN }} + diff --git a/PyNite/MITC4.py b/Archived/MITC4.py similarity index 100% rename from PyNite/MITC4.py rename to Archived/MITC4.py diff --git a/Examples/Beam on Elastic Foundation.py b/Examples/Beam on Elastic Foundation.py index c96d9624..f26cd127 100644 --- a/Examples/Beam on Elastic Foundation.py +++ b/Examples/Beam on Elastic Foundation.py @@ -38,14 +38,16 @@ G = 11200 # ksi boef.add_material('Steel', E, G, 0.3, 490/1000/12**3) -# Define section properties (W8x35) +# Define section properties (W8x35) and add a section to the model A = 10.3 # in^2 Iz = 127 # in^4 (strong axis) Iy = 42.6 # in^4 (weak axis) J = 0.769 # in^4 +boef.add_section('W8x35', A, Iy, Iz, J) + # Define the member -boef.add_member('M1', 'N1', 'N' + str(num_nodes), 'Steel', Iy, Iz, J, A) +boef.add_member('M1', 'N1', 'N' + str(num_nodes), 'Steel', 'W8x35') # In the next few lines no load case or load combination is being specified. When this is the case, # PyNite internally creates a default load case ('Case 1') and a default load combination diff --git a/Examples/Braced Frame - Spring Supported.py b/Examples/Braced Frame - Spring Supported.py index bfe15148..0f3027fc 100644 --- a/Examples/Braced Frame - Spring Supported.py +++ b/Examples/Braced Frame - Spring Supported.py @@ -13,11 +13,12 @@ braced_frame.add_node('N3', 15*12, 12*12, 0) braced_frame.add_node('N4', 15*12, 0*12, 0) -# Define column properties (use W10x33 from the AISC Manual): +# Define column properties (use W10x33 from the AISC Manual) and add section to the model: Iy = 36.6 # in^4 Iz = 171 # in^4 J = 0.58 # in^4 A = 9.71 # in^2 +braced_frame.add_section('W10x33', A, Iy, Iz, J) # Define a material E = 29000 # Young's modulus (ksi) @@ -27,29 +28,31 @@ braced_frame.add_material('Steel', E, G, nu, rho) # Define the columns -braced_frame.add_member('Col1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) -braced_frame.add_member('Col2', 'N4', 'N3', 'Steel', Iy, Iz, J, A) +braced_frame.add_member('Col1', 'N1', 'N2', 'Steel', 'W10x33') +braced_frame.add_member('Col2', 'N4', 'N3', 'Steel', 'W10x33') -# Define beam properties (Use W8x24) +# Define beam properties (Use W8x24) and add section to the model Iy = 18.3 # in^4 Iz = 82.7 # in^4 J = 0.346 # in^4 A = 7.08 # in^2 +braced_frame.add_section('W8x24', A, Iy, Iz, J) # Define the beams -braced_frame.add_member('Beam', 'N2', 'N3', 'Steel', Iy, Iz, J, A) +braced_frame.add_member('Beam', 'N2', 'N3', 'Steel', 'W8x24') braced_frame.def_releases('Beam', Ryi=True, Rzi=True, Ryj=True, Rzj=True) # Define the brace properties # We'll use a section with L/r <= 300 which is a common rule of thumb for -# tension members. We'll use L4x4x1/4. +# tension members. We'll use L4x4x1/4. Iy = 3 # in^4 Iz = 3 # in^4 J = 0.0438 # in^4 A = 1.94 # in^2 +braced_frame.add_section('L4x4x1/4', A, Iy, Iz, J) # Define a brace (tension and compression - both ways) -braced_frame.add_member('Brace1', 'N1', 'N3', 'Steel', Iy, Iz, J, A) +braced_frame.add_member('Brace1', 'N1', 'N3', 'Steel', 'L4x4x1/4') # Let's add spring supports to the base of the structure. We'll add a couple of # extra nodes at the base of the structure that will receive the springs. The @@ -97,18 +100,18 @@ # to the full member length. Note also that the direction uses lowercase # notations to indicate member local coordinate systems. Brace loads have been # neglected. -braced_frame.add_member_dist_load('Beam', Direction='Fy', w1=-0.024/12, +braced_frame.add_member_dist_load('Beam', direction='Fy', w1=-0.024/12, w2=-0.024/12, x1=0, x2=15*12, case='D') -braced_frame.add_member_dist_load('Col1', Direction='Fx', w1=-0.033/12, +braced_frame.add_member_dist_load('Col1', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') -braced_frame.add_member_dist_load('Col2', Direction='Fx', w1=-0.033/12, +braced_frame.add_member_dist_load('Col2', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') # Add nodal wind loads of 25 kips to each side of the frame. Note that the # direction uses uppercase notation to indicate model global coordinate # system. -braced_frame.add_node_load('N2', Direction='FX', P=25, case='W') -braced_frame.add_node_load('N3', Direction='FX', P=25, case='W') +braced_frame.add_node_load('N2', direction='FX', P=25, case='W') +braced_frame.add_node_load('N3', direction='FX', P=25, case='W') # Create load combinations # Note that the load combination '1.4D' has no lateral load, but does have diff --git a/Examples/Braced Frame - Tension Only.py b/Examples/Braced Frame - Tension Only.py index 37ab53ee..b0ad87b5 100644 --- a/Examples/Braced Frame - Tension Only.py +++ b/Examples/Braced Frame - Tension Only.py @@ -18,6 +18,7 @@ Iz = 171 # in^4 J = 0.58 # in^4 A = 9.71 # in^2 +braced_frame.add_section('W10x33', A, Iy, Iz, J) # Define a material E = 29000 # ksi @@ -27,17 +28,18 @@ braced_frame.add_material('Steel', E, G, nu, rho) # Define the columns -braced_frame.add_member('Col1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) -braced_frame.add_member('Col2', 'N4', 'N3', 'Steel', Iy, Iz, J, A) +braced_frame.add_member('Col1', 'N1', 'N2', 'Steel', 'W10x33') +braced_frame.add_member('Col2', 'N4', 'N3', 'Steel', 'W10x33') # Define beam properties (Use W8x24) Iy = 18.3 # in^4 Iz = 82.7 # in^4 J = 0.346 # in^4 A = 7.08 # in^2 +braced_frame.add_section('W8x24', A, Iy, Iz, J) # Define the beams -braced_frame.add_member('Beam', 'N2', 'N3', 'Steel', Iy, Iz, J, A) +braced_frame.add_member('Beam', 'N2', 'N3', 'Steel', 'W8x24') braced_frame.def_releases('Beam', Ryi=True, Rzi=True, Ryj=True, Rzj=True) # Define the brace properties @@ -47,12 +49,11 @@ Iz = 3 # in^4 J = 0.0438 # in^4 A = 1.94 # in^2 +braced_frame.add_section('L4x4x1/4', A, Iy, Iz, J) # Define the braces -braced_frame.add_member('Brace1', 'N1', 'N3', 'Steel', Iy, Iz, J, A, - tension_only=True) -braced_frame.add_member('Brace2', 'N4', 'N2', 'Steel', Iy, Iz, J, A, - tension_only=True) +braced_frame.add_member('Brace1', 'N1', 'N3', 'Steel', 'L4x4x1/4', tension_only=True) +braced_frame.add_member('Brace2', 'N4', 'N2', 'Steel', 'L4x4x1/4', tension_only=True) # Release the brace ends to form an axial member braced_frame.def_releases('Brace1', Ryi=True, Rzi=True, Ryj=True, Rzj=True) @@ -75,18 +76,18 @@ # to the full member length. Note also that the direction uses lowercase # notations to indicate member local coordinate systems. Brace loads have been # neglected. -braced_frame.add_member_dist_load('Beam', Direction='Fy', w1=-0.024/12, +braced_frame.add_member_dist_load('Beam', direction='Fy', w1=-0.024/12, w2=-0.024/12, x1=0, x2=15*12, case='D') -braced_frame.add_member_dist_load('Col1', Direction='Fx', w1=-0.033/12, +braced_frame.add_member_dist_load('Col1', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') -braced_frame.add_member_dist_load('Col2', Direction='Fx', w1=-0.033/12, +braced_frame.add_member_dist_load('Col2', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') # Add nodal wind loads of 25 kips to each side of the frame. Note that the # direction uses uppercase notation to indicate model global coordinate # system. -braced_frame.add_node_load('N2', Direction='FX', P=25, case='W') -braced_frame.add_node_load('N3', Direction='FX', P=25, case='W') +braced_frame.add_node_load('N2', direction='FX', P=25, case='W') +braced_frame.add_node_load('N3', direction='FX', P=25, case='W') # Create load combinations # Note that the load combination '1.4D' has no lateral load, but does have diff --git a/Examples/Circular Bin with Conical Hopper.py b/Examples/Circular Bin with Conical Hopper.py index 3d150085..bc723f54 100644 --- a/Examples/Circular Bin with Conical Hopper.py +++ b/Examples/Circular Bin with Conical Hopper.py @@ -69,5 +69,11 @@ model.analyze() # Render the model. Labels and loads will be turned off to speed up interaction. -from PyNite.Visualization import Renderer, render_model -render_model(model, 0.1, render_loads=True, color_map='dz', combo_name='1.4F', labels=False) \ No newline at end of file +from PyNite.Visualization import Renderer +rndr = Renderer(model) +rndr.annotation_size = 0.1 +rndr.render_loads = False +rndr.color_map = 'dz' +rndr.combo_name = '1.4F' +rndr.labels = False +rndr.render_model() diff --git a/Examples/Moment Frame - Lateral Load.py b/Examples/Moment Frame - Lateral Load.py index 977148fa..10b58da4 100644 --- a/Examples/Moment Frame - Lateral Load.py +++ b/Examples/Moment Frame - Lateral Load.py @@ -18,6 +18,7 @@ Iz = 171 # in^4 J = 0.58 # in^4 A = 9.71 # in^2 +MomentFrame.add_section('W10x33', A, Iy, Iz, J) # Define a material E = 29000 # ksi @@ -27,17 +28,18 @@ MomentFrame.add_material('Steel', E, G, nu, rho) # Define the columns -MomentFrame.add_member('Col1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) -MomentFrame.add_member('Col2', 'N4', 'N3', 'Steel', Iy, Iz, J, A) +MomentFrame.add_member('Col1', 'N1', 'N2', 'Steel', 'W10x33') +MomentFrame.add_member('Col2', 'N4', 'N3', 'Steel', 'W10x33') # Define beam properties (Use W8x24) Iy = 18.3 # in^4 Iz = 82.7 # in^4 J = 0.346 # in^4 A = 7.08 # in^2 +MomentFrame.add_section('W8x24', A, Iy, Iz, J) # Define the beams -MomentFrame.add_member('Beam', 'N2', 'N3', 'Steel', Iy, Iz, J, A) +MomentFrame.add_member('Beam', 'N2', 'N3', 'Steel', 'W8x24') # Provide fixed supports at the bases of the columns MomentFrame.def_support('N1', support_DX=True, support_DY=True, support_DZ=True, support_RX=True, support_RY=True, support_RZ=True) @@ -46,13 +48,13 @@ # Add self weight dead loads to the frame # Note that we could leave 'x1' and 'x2' undefined below and it would default to the full member length # Note also that the direction uses lowercase notations to indicate member local coordinate systems -MomentFrame.add_member_dist_load('Beam', Direction='Fy', w1=-0.024/12, w2=-0.024/12, x1=0, x2=15*12, case='D') -MomentFrame.add_member_dist_load('Col1', Direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') -MomentFrame.add_member_dist_load('Col2', Direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') +MomentFrame.add_member_dist_load('Beam', direction='Fy', w1=-0.024/12, w2=-0.024/12, x1=0, x2=15*12, case='D') +MomentFrame.add_member_dist_load('Col1', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') +MomentFrame.add_member_dist_load('Col2', direction='Fx', w1=-0.033/12, w2=-0.033/12, x1=0, x2=12*12, case='D') # Add a nodal wind load of 10 kips at the left side of the frame # Note that the direction uses uppercase notation to indicate model global coordinate system -MomentFrame.add_node_load('N2', Direction='FX', P=10, case='W') +MomentFrame.add_node_load('N2', direction='FX', P=10, case='W') # Create two load combinations MomentFrame.add_load_combo('1.2D+1.0W', factors={'D':1.2, 'W':1.0}) @@ -69,8 +71,13 @@ # MomentFrame.analyze_linear(log=True) # Display the deformed shape of the structure magnified 50 times with the text height 5 model units (inches) high -from PyNite import Visualization -Visualization.render_model(MomentFrame, annotation_size=5, deformed_shape=True, deformed_scale=50, combo_name='1.2D+1.0W') +from PyNite.Visualization import Renderer +rndr = Renderer(MomentFrame) +rndr.annotation_size = 5 +rndr.deformed_shape = True +rndr.deformed_scale = 50 +rndr.combo_name = '1.2D+1.0W' +rndr.render_model(MomentFrame) # Plot the moment diagram for the beam MomentFrame.members['Beam'].plot_moment('Mz', combo_name='1.2D+1.0W') diff --git a/Examples/P-Delta Analysis.py b/Examples/P-Delta Analysis.py index ed624b3c..1faae80e 100644 --- a/Examples/P-Delta Analysis.py +++ b/Examples/P-Delta Analysis.py @@ -23,8 +23,9 @@ # Add nodes cantilever.add_node('N' + str(i+1), 0, i*L/(num_nodes - 1), 0) -# Add the member -cantilever.add_member('M1', 'N1', 'N6', 'Steel', I, I, 200/12**4, 10/12**2) +# Add the section and member +cantilever.add_section('MySection', 10/12**2, I, I, 200/12**4) +cantilever.add_member('M1', 'N1', 'N6', 'Steel', 'MySection') # Add a fixed support at the base of the column cantilever.def_support('N1', True, True, True, True, True, True) diff --git a/Examples/Rectangular Tank Wall - Hydrostatic Loads.py b/Examples/Rectangular Tank Wall - Hydrostatic Loads.py index ca026640..09c47128 100644 --- a/Examples/Rectangular Tank Wall - Hydrostatic Loads.py +++ b/Examples/Rectangular Tank Wall - Hydrostatic Loads.py @@ -74,7 +74,7 @@ renderer.deformed_scale = 1000 renderer.color_map = 'Qy' renderer.combo_name = '1.4F' -renderer.labels = True +renderer.show_labels = True renderer.scalar_bar = True renderer.scalar_bar_text_size = 12 renderer.render_model() @@ -89,10 +89,10 @@ My = -0.0242*qo*a**2 # Pynite solution -Qx_pn = model.Quads['Q176'].shear(-1, 0, True, '1.4F')[0, 0] -Qy_pn = model.Meshes['MSH1'].max_shear('Qy', '1.4F') -Mx_pn = model.Quads['Q176'].moment(-1, 0, True, '1.4F')[0, 0] -My_pn = model.Meshes['MSH1'].min_moment('My', '1.4F') +Qx_pn = model.quads['Q176'].shear(-1, 0, True, '1.4F')[0, 0] +Qy_pn = model.meshes['MSH1'].max_shear('Qy', '1.4F') +Mx_pn = model.quads['Q176'].moment(-1, 0, True, '1.4F')[0, 0] +My_pn = model.meshes['MSH1'].min_moment('My', '1.4F') # Comparison of solutions print('Max Moment at Side Mid-Height of Wall, Mx | Pynite: ', Mx_pn, '| Timoshenko: ', Mx) diff --git a/Examples/Simple Beam - Factored Envelope.py b/Examples/Simple Beam - Factored Envelope.py index aa2d3a93..e93bf6da 100644 --- a/Examples/Simple Beam - Factored Envelope.py +++ b/Examples/Simple Beam - Factored Envelope.py @@ -22,9 +22,12 @@ rho = 2.836e-4 # Density (kci) simple_beam.add_material('Steel', E, G, nu, rho) -# Add a beam with the following properties: +# Add a section with the following properties: # Iy = 100 in^4, Iz = 150 in^4, J = 250 in^4, A = 20 in^2 -simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 20) +simple_beam.add_section('MySection', 20, 100, 150, 250) + +#Add member +simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 'MySection') # Provide simple supports simple_beam.def_support('N1', True, True, True, True, False, False) # Constrained for torsion at 'N1' diff --git a/Examples/Simple Beam - Point Load.py b/Examples/Simple Beam - Point Load.py index 11321662..cf2b6b76 100644 --- a/Examples/Simple Beam - Point Load.py +++ b/Examples/Simple Beam - Point Load.py @@ -21,9 +21,12 @@ rho = 2.836e-4 # Density (kci) simple_beam.add_material('Steel', E, G, nu, rho) -# Add a beam with the following properties: +# Add a section with the following properties: # Iy = 100 in^4, Iz = 150 in^4, J = 250 in^4, A = 20 in^2 -simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 20) +simple_beam.add_section('MySection', 20, 100, 150, 250) + +#Add member +simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 'MySection') # Provide simple supports simple_beam.def_support('N1', True, True, True, True, False, False) # Constrained for torsion at 'N1' diff --git a/Examples/Simple Beam - Uniform Load.py b/Examples/Simple Beam - Uniform Load.py index d2fb0492..87722958 100644 --- a/Examples/Simple Beam - Uniform Load.py +++ b/Examples/Simple Beam - Uniform Load.py @@ -19,9 +19,12 @@ rho = 2.836e-4 # Density (kci) simple_beam.add_material('Steel', E, G, nu, rho) -# Add a beam with the following properties: +# Add a section with the following properties: # Iy = 100 in^4, Iz = 150 in^4, J = 250 in^4, A = 20 in^2 -simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 20) +simple_beam.add_section('MySection', 20, 100, 150, 250) + +#Add member +simple_beam.add_member('M1', 'N1', 'N2', 'Steel', 'MySection') # Provide simple supports simple_beam.def_support('N1', True, True, True, False, False, False) diff --git a/Examples/Space Frame - Nodal Loads 1.py b/Examples/Space Frame - Nodal Loads 1.py index 877cdb14..3f9b9ff0 100644 --- a/Examples/Space Frame - Nodal Loads 1.py +++ b/Examples/Space Frame - Nodal Loads 1.py @@ -26,6 +26,7 @@ Iy = 100 Iz = 100 A = 10 +frame.add_section('MySection', A, Iy, Iz, J) # Define a material E = 30000 @@ -34,9 +35,9 @@ rho = 2.836e-4 frame.add_material('Steel', E, G, nu, rho) -frame.add_member('M1', 'N2', 'N1', 'Steel', Iy, Iz, J, A) -frame.add_member('M2', 'N3', 'N1', 'Steel', Iy, Iz, J, A) -frame.add_member('M3', 'N4', 'N1', 'Steel', Iy, Iz, J, A) +frame.add_member('M1', 'N2', 'N1', 'Steel', 'MySection') +frame.add_member('M2', 'N3', 'N1', 'Steel', 'MySection') +frame.add_member('M3', 'N4', 'N1', 'Steel', 'MySection') # Add nodal loads frame.add_node_load('N1', 'FY', -50) diff --git a/Examples/Space Frame - Nodal Loads 2.py b/Examples/Space Frame - Nodal Loads 2.py index 115543d7..fc0dddd7 100644 --- a/Examples/Space Frame - Nodal Loads 2.py +++ b/Examples/Space Frame - Nodal Loads 2.py @@ -5,7 +5,7 @@ # Import 'FEModel3D' and 'Visualization' from 'PyNite' from PyNite import FEModel3D -from PyNite import Visualization +from PyNite.Visualization import Renderer # Create a new model frame = FEModel3D() @@ -25,6 +25,7 @@ Iy = 200 Iz = 1000 A = 100 +frame.add_section('MySection', A, Iy, Iz, J) # Define a material E = 30000 @@ -33,9 +34,9 @@ rho = 2.836e-4 frame.add_material('Steel', E, G, nu, rho) -frame.add_member('M12', 'N1', 'N2', 'Steel', Iy, Iz, J, A) -frame.add_member('M23', 'N2', 'N3', 'Steel', Iy, Iz, J, A) -frame.add_member('M34', 'N3', 'N4', 'Steel', Iy, Iz, J, A) +frame.add_member('M12', 'N1', 'N2', 'Steel', 'MySection') +frame.add_member('M23', 'N2', 'N3', 'Steel', 'MySection') +frame.add_member('M34', 'N3', 'N4', 'Steel', 'MySection') # Add nodal loads frame.add_node_load('N2', 'FY', -5) @@ -48,5 +49,11 @@ print('Calculated results: ', frame.nodes['N2'].DY, frame.nodes['N3'].DZ) print('Expected results: ', -0.063, 1.825) -# Render the model for viewing -Visualization.render_model(frame, annotation_size=5, deformed_shape=True, deformed_scale=40, render_loads=True) \ No newline at end of file +# Render the deformed shape +rndr = Renderer(frame) +rndr.annotation_size = 5 +rndr.render_loads = True +rndr.deformed_shape = True +rndr.deformed_scale = 40 +rndr.render_loads = True +rndr.render_model() \ No newline at end of file diff --git a/Examples/Space Truss - Nodal Load.py b/Examples/Space Truss - Nodal Load.py index 71dd3346..73ae06ad 100644 --- a/Examples/Space Truss - Nodal Load.py +++ b/Examples/Space Truss - Nodal Load.py @@ -30,14 +30,16 @@ rho = 1 truss.add_material('Rigid', E, G, nu, rho) +#Add a section to use for all members in this model +truss.add_section('TrussSection', 100, 100, 100, 100) # Create members -truss.add_member('AB', 'A', 'B', 'Rigid', 100, 100, 100, 100) -truss.add_member('AC', 'A', 'C', 'Rigid', 100, 100, 100, 100) -truss.add_member('AD', 'A', 'D', 'Rigid', 100, 100, 100, 100) -truss.add_member('BC', 'B', 'C', 'Rigid', 100, 100, 100, 100) -truss.add_member('BD', 'B', 'D', 'Rigid', 100, 100, 100, 100) -truss.add_member('BE', 'B', 'E', 'Rigid', 100, 100, 100, 100) +truss.add_member('AB', 'A', 'B', 'Rigid', 'TrussSection') +truss.add_member('AC', 'A', 'C', 'Rigid', 'TrussSection') +truss.add_member('AD', 'A', 'D', 'Rigid', 'TrussSection') +truss.add_member('BC', 'B', 'C', 'Rigid', 'TrussSection') +truss.add_member('BD', 'B', 'D', 'Rigid', 'TrussSection') +truss.add_member('BE', 'B', 'E', 'Rigid', 'TrussSection') # Release the moments at the ends of the members to make truss members truss.def_releases('AC', False, False, False, False, True, True, \ diff --git a/PyNite/FEModel3D.py b/PyNite/FEModel3D.py index 43e6d2f3..8effdd49 100644 --- a/PyNite/FEModel3D.py +++ b/PyNite/FEModel3D.py @@ -8,7 +8,7 @@ from PyNite.Node3D import Node3D from PyNite.Material import Material -from PyNite.Section import Section +from PyNite.Section import Section, SteelSection from PyNite.PhysMember import PhysMember from PyNite.Spring3D import Spring3D from PyNite.Member3D import Member3D @@ -207,7 +207,7 @@ def add_material(self, name, E, G, nu, rho, fy=None): count += 1 # Create a new material - new_material = Material(name, E, G, nu, rho, fy) + new_material = Material(self, name, E, G, nu, rho, fy) # Add the new material to the list self.materials[name] = new_material @@ -215,23 +215,63 @@ def add_material(self, name, E, G, nu, rho, fy=None): # Flag the model as unsolved self.solution = None - def add_section(self, name, section): + def add_section(self, name:str, A:float, Iy:float, Iz:float, J:float): """Adds a cross-section to the model. :param name: A unique name for the cross-section. :type name: string - :param section: A 'PyNite' `Section` object. - :type section: Section + :param name: Name of the section + :type name: str + :param A: Cross-sectional area of the section + :type A: float + :param Iy: The second moment of area the section about the Y (minor) axis + :type Iy: float + :param Iz: The second moment of area the section about the Z (major) axis + :type Iz: float + :param J: The torsion constant of the section + :type J: float """ # Check if the section name has already been used - if name not in self.sections.keys(): + if name not in self.sections: # Store the section in the `Sections` dictionary - self.sections[name] = section + self.sections[name] = Section(self, name, A, Iy, Iz, J) else: # Stop execution and notify the user that the section name is already being used raise Exception('Cross-section name ' + name + ' already exists in the model.') + def add_steel_section(self, name:str, A:float, Iy:float, Iz:float, J:float, + Zy:float, Zz:float, material_name:str): + """Adds a cross-section to the model. + + :param name: A unique name for the cross-section. + :type name: string + :param name: Name of the section + :type name: str + :param A: Cross-sectional area of the section + :type A: float + :param Iy: The second moment of area the section about the Y (minor) axis + :type Iy: float + :param Iz: The second moment of area the section about the Z (major) axis + :type Iz: float + :param J: The torsion constant of the section + :type J: float + :param Zy: The section modulus about the Y (minor) axis + :type Zy: float + :param Zz: The section modulus about the Z (major) axis + :type Zz: float + :param material_name: The name of the steel material + :type material_name: str + """ + + # Check if the section name has already been used + if name not in self.sections: + # Store the section in the `Sections` dictionary + self.sections[name] = SteelSection(self, name, A, Iy, Iz, J, Zy, Zz, material_name) + else: + # Stop execution and notify the user that the section name is already being used + raise Exception('Cross-section name ' + name + ' already exists in the model.') + def add_spring(self, name, i_node, j_node, ks, tension_only=False, comp_only=False): """Adds a new spring to the model. @@ -279,7 +319,7 @@ def add_spring(self, name, i_node, j_node, ks, tension_only=False, comp_only=Fal # Return the spring name return name - def add_member(self, name, i_node, j_node, material_name, Iy=None, Iz=None, J=None, A=None, aux_node=None, tension_only=False, comp_only=False, section_name=None): + def add_member(self, name, i_node, j_node, material_name, section_name, aux_node=None, tension_only=False, comp_only=False): """Adds a new physical member to the model. :param name: A unique user-defined name for the member. If ``None`` or ``""``, a name will be automatically assigned @@ -289,24 +329,16 @@ def add_member(self, name, i_node, j_node, material_name, Iy=None, Iz=None, J=No :param j_node: The name of the j-node (end node). :type j_node: str :param material_name: The name of the material of the member. - :type material: str - :param Iy: The moment of inertia of the member about its local y-axis. - :type Iy: number - :param Iz: The moment of inertia of the member about its local z-axis. - :type Iz: number - :param J: The polar moment of inertia of the member. - :type J: number - :param A: The cross-sectional area of the member. - :type A: number - :param auxNode: The name of the auxiliary node used to define the local z-axis. The default is ``None``, in which case the program defines the axis instead of using an auxiliary node. + :type material_name: str + :param section_name: The name of the cross section to use for section properties. + :type section_name: string + :param aux_node: The name of the auxiliary node used to define the local z-axis. The default is ``None``, in which case the program defines the axis instead of using an auxiliary node. :type aux_node: str, optional :param tension_only: Indicates if the member is tension-only, defaults to False :type tension_only: bool, optional :param comp_only: Indicates if the member is compression-only, defaults to False :type comp_only: bool, optional :raises NameError: Occurs if the specified name already exists. - :param section_name: The name of the cross section to use for section properties. If section is `None` the user must provide `Iy`, `Iz`, `A`, and `J`. If section is not `None` any values of `Iy`, `Iz`, `A`, and `J` will be ignored and the cross-section's values will be used instead. - :type section: string :return: The name of the member added to the model. :rtype: str """ @@ -323,10 +355,8 @@ def add_member(self, name, i_node, j_node, material_name, Iy=None, Iz=None, J=No count += 1 # Create a new member - if aux_node == None: - new_member = PhysMember(name, self.nodes[i_node], self.nodes[j_node], material_name, self, Iy, Iz, J, A, tension_only=tension_only, comp_only=comp_only, section_name=section_name) - else: - new_member = PhysMember(name, self.nodes[i_node], self.nodes[j_node], material_name, self, Iy, Iz, J, A, aux_node=self.aux_nodes[aux_node], tension_only=tension_only, comp_only=comp_only, section_name=section_name) + if aux_node is not None: aux_node = self.aux_nodes[aux_node] + new_member = PhysMember(self, name, self.nodes[i_node], self.nodes[j_node], material_name, section_name, aux_node=aux_node, tension_only=tension_only, comp_only=comp_only) # Add the new member to the list self.members[name] = new_member @@ -1180,7 +1210,7 @@ def add_member_self_weight(self, global_direction, factor, case='Case 1'): for member in self.members.values(): # Calculate the self weight of the member - self_weight = factor*self.materials[member.material_name].rho*member.A + self_weight = factor*member.material.rho*member.section.A # Add the self-weight load to the member self.add_member_dist_load(member.name, global_direction, self_weight, self_weight, case=case) @@ -1623,8 +1653,8 @@ def Kg(self, combo_name='Combo 1', log=False, sparse=True, first_step=True): for member in phys_member.sub_members.values(): # Calculate the axial force in the member - E = member.E - A = member.A + E = member.material.E + A = member.section.A L = member.L() # Calculate the axial force acting on the member diff --git a/PyNite/Material.py b/PyNite/Material.py index a79870d3..ec1885d9 100644 --- a/PyNite/Material.py +++ b/PyNite/Material.py @@ -1,7 +1,14 @@ +from typing import Optional + class Material(): + """ + A class representing a material assigned to a Member3D, Plate or Quad in a finite element model. - def __init__(self, name, E, G, nu, rho, fy=None): + This class stores all properties related to the physical material of the element + """ + def __init__(self, model, name:str, E:float, G:float, nu:float, rho:float, fy:Optional[float] = None): + self.model = model self.name = name self.E = E self.G = G diff --git a/PyNite/Member3D.py b/PyNite/Member3D.py index cf4fc050..a7c39074 100644 --- a/PyNite/Member3D.py +++ b/PyNite/Member3D.py @@ -21,8 +21,8 @@ class Member3D(): __plt = None #%% - def __init__(self, name, i_node, j_node, material_name, model, Iy, Iz, J, A, auxNode=None, - tension_only=False, comp_only=False, section_name=None): + def __init__(self, model, name, i_node, j_node, material_name, section_name, auxNode=None, + tension_only=False, comp_only=False): """ Initializes a new member. """ @@ -30,23 +30,16 @@ def __init__(self, name, i_node, j_node, material_name, model, Iy, Iz, J, A, aux self.ID = None # Unique index number for the member assigned by the program self.i_node = i_node # The element's i-node self.j_node = j_node # The element's j-node - self.material_name = material_name # The element's material - self.E = model.materials[material_name].E # The modulus of elasticity of the element - self.G = model.materials[material_name].G # The shear modulus of the element - - # Section properties - if section_name is None: - self.section = None - self.A = A # The cross-sectional area - self.Iy = Iy # The y-axis moment of inertia - self.Iz = Iz # The z-axis moment of inertia - self.J = J # The torsional constant - else: - self.section = model.sections[section_name] - self.A = model.sections[section_name].A - self.Iy = model.sections[section_name].Iy - self.Iz = model.sections[section_name].Iz - self.J = model.sections[section_name].J + + try: + self.material = model.materials[material_name] # The element's material + except KeyError: + raise NameError(f"No material named '{material_name}'") + + try: + self.section = model.sections[section_name] # The element's section + except KeyError: + raise NameError(f"No section names '{section_name}'") # Variables used to track nonlinear material member end forces self._fxi = 0 @@ -144,12 +137,12 @@ def _k_unc(self): """ # Get the properties needed to form the local stiffness matrix - E = self.E - G = self.G - Iy = self.Iy - Iz = self.Iz - J = self.J - A = self.A + E = self.material.E + G = self.material.G + Iy = self.section.Iy + Iz = self.section.Iz + J = self.section.J + A = self.section.A L = self.L() # Create the uncondensed local stiffness matrix @@ -181,8 +174,8 @@ def kg(self, P=0): """ # Get the properties needed to form the local geometric stiffness matrix - Ip = self.Iy + self.Iz - A = self.A + Ip = self.section.Iy + self.section.Iz + A = self.section.A L = self.L() # Create the uncondensed local geometric stiffness matrix @@ -464,7 +457,7 @@ def f(self, combo_name='Combo 1', push_combo='Push', step_num=1): # Calculate and return the member's local end force vector if self.model.solution == 'P-Delta': # Back-calculate the axial force on the member from the axial strain - P = (self.d(combo_name)[6, 0] - self.d(combo_name)[0, 0])*self.A*self.E/self.L() + P = (self.d(combo_name)[6, 0] - self.d(combo_name)[0, 0])*self.section.A*self.material.E/self.L() return add(matmul(add(self.k(), self.kg(P)), self.d(combo_name)), self.fer(combo_name)) elif self.model.solution == 'Pushover': P = self._fxj - self._fxi @@ -1907,10 +1900,10 @@ def _segment_member(self, combo_name='Combo 1'): # Get the member's length and stiffness properties L = self.L() - E = self.E - A = self.A - Iz = self.Iz - Iy = self.Iy + E = self.material.E + A = self.section.A + Iz = self.section.Iz + Iy = self.section.Iy SegmentsZ = self.SegmentsZ SegmentsY = self.SegmentsY SegmentsX = self.SegmentsX diff --git a/PyNite/PhysMember.py b/PyNite/PhysMember.py index 280cae9c..1705284e 100644 --- a/PyNite/PhysMember.py +++ b/PyNite/PhysMember.py @@ -11,15 +11,14 @@ class PhysMember(Member3D): Physical members can detect internal nodes and subdivide themselves into sub-members at those nodes. """ - + # '__plt' is used to store the 'pyplot' from matplotlib once it gets imported. Setting it to 'None' for now allows us to defer importing it until it's actually needed. - __plt = None - - def __init__(self, name, i_node, j_node, material_name, model, Iy, Iz, J, A, aux_node=None, - tension_only=False, comp_only=False, section_name=None): - - super().__init__(name, i_node, j_node, material_name, model, Iy, Iz, J, A, aux_node, tension_only, comp_only, section_name) + __plt = None + + def __init__(self, model, name, i_node, j_node, material_name, section_name, aux_node=None, + tension_only=False, comp_only=False): + super().__init__(model, name, i_node, j_node, material_name, section_name, aux_node, tension_only, comp_only) self.sub_members = {} def descritize(self): @@ -81,9 +80,7 @@ def descritize(self): xj = int_nodes[i+1][1] # Create a new sub-member - if self.section is None: section_name = None - else: section_name = self.section.name - new_sub_member = Member3D(name, i_node, j_node, self.material_name, self.model, self.Iy, self.Iz, self.J, self.A, self.auxNode, self.tension_only, self.comp_only, section_name) + new_sub_member = Member3D(self.model, name, i_node, j_node, self.material.name, self.section.name, self.auxNode, self.tension_only, self.comp_only) # Flag the sub-member as active for combo_name in self.model.load_combos.keys(): diff --git a/PyNite/Plastic Beam.py b/PyNite/Plastic Beam.py index 291ef25d..a3addeb4 100644 --- a/PyNite/Plastic Beam.py +++ b/PyNite/Plastic Beam.py @@ -15,8 +15,7 @@ plastic_beam.add_material('Stl_A992', E, G, nu, rho, fy) # Define a cross-section -W12 = SteelSection(plastic_beam, 'W12x65', 19.1, 20, 533, 1, 15, 96.8, 'Stl_A992') -plastic_beam.add_section('W12x65', W12) +plastic_beam.add_steel_section('W12x65', 19.1, 20, 533, 1, 15, 96.8, 'Stl_A992') # Add nodes plastic_beam.add_node('N1', 0, 0, 0) @@ -28,7 +27,7 @@ plastic_beam.def_support('N3', False, True, True, False, False, False) # Add a member -plastic_beam.add_member('M1', 'N1', 'N3', 'Stl_A992', section_name='W12x65') +plastic_beam.add_member('M1', 'N1', 'N3', 'Stl_A992', 'W12x65') # Add a load plastic_beam.add_node_load('N3', 'FY', -0.0001, 'D') diff --git a/PyNite/Rendering.py b/PyNite/Rendering.py index a769ade4..8e97ace6 100644 --- a/PyNite/Rendering.py +++ b/PyNite/Rendering.py @@ -179,10 +179,10 @@ def render_model(self, reset_camera=True): self.update(reset_camera) # Render the model (code execution will pause here until the user closes the window) - self.plotter.show(title='Pynite - Simple Finite Element Analysis for Python') + self.plotter.show(title='Pynite - Simple Finite Element Analysis for Python', auto_close=False) def screenshot(self, filepath='./Pynite_Image.png', interact=True, reset_camera=True): - """Saves a screenshot of the rendered model. Press `q` to capture the screenshot after positioning the view. Pressing the close button in the corner of the window will ignore the positioning. + """Saves a screenshot of the rendered model. Important: Press `q` to capture the screenshot after positioning the view. Pressing the `X` button in the corner of the window will ignore the positioning and shut down the entire renderer against further use once the screenshot is taken. :param filepath: The filepath to write the image to. When set to 'jupyter', the resulting plot is placed inline in a jupyter notebook. Defaults to 'jupyter'. :type filepath: str, optional @@ -195,17 +195,14 @@ def screenshot(self, filepath='./Pynite_Image.png', interact=True, reset_camera= # Update the plotter with the latest geometry self.update(reset_camera) - # Show the plotter for interaction - if interact == True: - # Use `q` for `quit` to take the screenshot. The window will not close until the `X` in - # the corner of the window is hit. - self.plotter.show(title='Pynite - Simple Finite Element Anlaysis for Python', screenshot=filepath) - else: + # Determine if the user should interact with the window before capturing the screenshot + if interact == False: + # Don't bother showing the image before capturing the screenshot self.plotter.off_screen = True - - # Save the screenshot - self.plotter.screenshot(filename=filepath) + + # Save the screenshot to the specified filepath. Note that `auto_close` shuts down the entire plotter after the screenshot is taken, rather than just closing the window. We'll set `auto_close=False` to allow the plotter to remain active. Note that the window must be closed by pressing `q`. Closing it with the 'X' button in the window's corner will close the whole plotter down. + self.plotter.show(title='Pynite - Simple Finite Element Anlaysis for Python', screenshot=filepath, auto_close=False) def update(self, reset_camera=True): """ @@ -262,7 +259,7 @@ def update(self, reset_camera=True): # Render the springs for spring in self.model.springs.values(): - self.plot_spring(spring, self.annotation_size, 'grey') + self.plot_spring(spring, 'grey') # Render the spring labels self.plotter.add_point_labels(self._spring_label_points, self._spring_labels, text_color='black', bold=False, shape=None, render_points_as_spheres=False) @@ -289,7 +286,7 @@ def update(self, reset_camera=True): # Render deformed springs for spring in self.model.springs.values(): - self.plot_deformed_spring(spring, self.deformed_scale, self.combo_name) + self.plot_spring(spring, 'red', deformed=True) # _DeformedShape(self.model, self.deformed_scale, self.annotation_size, self.combo_name, self.render_nodes, self.theme) @@ -483,6 +480,16 @@ def plot_node(self, node, color='grey'): color=color) def plot_member(self, member, theme='default'): + """ + Adds a member to the plotter. This method generates a line representing a structural member between two nodes, and adds it to the plotter with specified theme settings. + + Parameters + ========== + :param member: The structural member to be plotted, containing information about its end nodes. + :type member: Member + :param theme: The theme for plotting the member. Default is 'default'. + :type theme: str + """ # Generate a line for the member line = pv.Line() @@ -499,29 +506,72 @@ def plot_member(self, member, theme='default'): self.plotter.add_mesh(line, color='black', line_width=2) - def plot_spring(self, spring, size, color='grey'): + def plot_spring(self, spring, color='grey', deformed=False): + """ + Adds a spring to the plotter. This method generates a zig-zag line representing a spring between two nodes, and adds it to the plotter with specified theme settings.""" - # Find the position of the i-node and j-node + # Scale the spring's zigzags + size = self.annotation_size + + # Find the spring's i-node and j-node i_node = spring.i_node j_node = spring.j_node + + # Find the spring's node coordinates Xi, Yi, Zi = i_node.X, i_node.Y, i_node.Z Xj, Yj, Zj = j_node.X, j_node.Y, j_node.Z - # Create the line - line = pv.Line((Xi, Yi, Zi), (Xj, Yj, Zj)) + # Determine if the spring should be plotted in its deformed shape + if deformed: + Xi = Xi + i_node.DX[self.combo_name]*self.deformed_scale + Yi = Yi + i_node.DY[self.combo_name]*self.deformed_scale + Zi = Zi + i_node.DZ[self.combo_name]*self.deformed_scale + Xj = Xj + j_node.DX[self.combo_name]*self.deformed_scale + Yj = Yj + j_node.DY[self.combo_name]*self.deformed_scale + Zj = Zj + j_node.DZ[self.combo_name]*self.deformed_scale + + # Calculate the spring direction vector and length + direction = np.array([Xj, Yj, Zj]) - np.array([Xi, Yi, Zi]) + length = ((Xj-Xi)**2 + (Yj-Yi)**2 - (Zj-Zi)**2)**0.5 + + # Normalize the direction vector + direction = direction/length + + # Calculate perpendicular vectors for zig-zag plane + arbitrary_vector = np.array([1, 0, 0]) + if np.allclose(direction, arbitrary_vector) or np.allclose(direction, -arbitrary_vector): + arbitrary_vector = np.array([0, 1, 0]) + perp_vector1 = np.cross(direction, arbitrary_vector) + perp_vector1 /= np.linalg.norm(perp_vector1) + perp_vector2 = np.cross(direction, perp_vector1) + perp_vector2 /= np.linalg.norm(perp_vector2) - # Change the color - if color is None: - line.plot(color='magenta') - elif color == 'black': - line.plot(color='black') + # Generate points for the zig-zag line + num_zigs = 4 + num_points = num_zigs * 2 + amplitude = size + t = np.linspace(0, length, num_points) + zigzag_pattern = amplitude * np.tile([1, -1], num_zigs) + zigzag_points = np.outer(t, direction) + np.outer(zigzag_pattern, perp_vector1) + + # Adjust the zigzag points to start position + zigzag_points += np.array([Xi, Yi, Zi]) + + # Add lines connecting the points + lines = np.zeros((num_points - 1, 3), dtype=int) + lines[:, 0] = np.full((num_points - 1), 2, dtype=int) + lines[:, 1] = np.arange(num_points - 1, dtype=int) + lines[:, 2] = np.arange(1, num_points, dtype=int) + + # Create a PolyData object for the zig-zag line + zigzag_line = pv.PolyData(zigzag_points, lines=lines) + + # Create a plotter and add the zig-zag line + self.plotter.add_mesh(zigzag_line, color=color, line_width=2) # Add the spring label to the list of labels self._spring_labels.append(spring.name) self._spring_label_points.append([(Xi+Xj)/2, (Yi+Yj)/2, (Zi+Zj)/2]) - - # Add the line to the plotter - self.plotter.add_mesh(line) def plot_plates(self, deformed_shape, deformed_scale, color_map, combo_name): @@ -677,27 +727,6 @@ def plot_deformed_member(self, member, scale_factor): for i in range(len(D_plot)-1): line = pv.Line(D_plot[i], D_plot[i+1]) self.plotter.add_mesh(line, color='red', line_width=2) - - def plot_deformed_spring(self, spring, scale_factor, combo_name='Combo 1'): - - # Determine if the spring is active for the load combination - if spring.active[combo_name]: - - # Get the spring's i-node and j-node - i_node = spring.i_node - j_node = spring.j_node - - # Calculate the deformed positions of the spring's end points - Xi = i_node.X + i_node.DX[combo_name]*scale_factor - Yi = i_node.Y + i_node.DY[combo_name]*scale_factor - Zi = i_node.Z + i_node.DZ[combo_name]*scale_factor - - Xj = j_node.X + j_node.DX[combo_name]*scale_factor - Yj = j_node.Y + j_node.DY[combo_name]*scale_factor - Zj = j_node.Z + j_node.DZ[combo_name]*scale_factor - - # Plot a line for the deformed spring - self.plotter.add_mesh(pv.Line((Xi, Yi, Zi), (Xj, Yj, Zj))) def plot_pt_load(self, position, direction, length, label_text=None, color='green'): diff --git a/PyNite/Report_Template.html b/PyNite/Report_Template.html index 402b6813..0637101d 100644 --- a/PyNite/Report_Template.html +++ b/PyNite/Report_Template.html @@ -67,12 +67,12 @@