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 @@

Members

{{ member.name }} {{ member.i_node.name }} {{ member.j_node.name }} - {{ "%.4g" | format(member.A) }} - {{ "%.4g" | format(member.Iy) }} - {{ "%.4g" | format(member.Iz) }} - {{ "%.4g" | format(member.J) }} - {{ "%.4g" | format(member.E) }} - {{ "%.4g" | format(member.G) }} + {{ "%.4g" | format(member.section.A) }} + {{ "%.4g" | format(member.section.Iy) }} + {{ "%.4g" | format(member.section.Iz) }} + {{ "%.4g" | format(member.section.J) }} + {{ "%.4g" | format(member.material.E) }} + {{ "%.4g" | format(member.material.G) }} {% endfor %} diff --git a/PyNite/Section.py b/PyNite/Section.py index 38e046cc..8e0c5119 100644 --- a/PyNite/Section.py +++ b/PyNite/Section.py @@ -2,23 +2,45 @@ import numpy as np class Section(): + """ + A class representing a section assigned to a Member3D element in a finite element model. - def __init__(self, model, name, A, Iy, Iz, J, material_name): - + This class stores all properties related to the geometry of the member + """ + def __init__(self, model, name:str, A:float, Iy:float, Iz:float, J:float) -> None: + """ + :param model: The finite element model to which this section belongs + :type model: FEModel3D + :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 + """ self.model = model self.name = name self.A = A self.Iy = Iy self.Iz = Iz self.J = J - self.material_name = material_name - self.fy = model.materials[material_name].fy def Phi(self): pass def G(self, fx, my, mz): - """Returns the gradient to the yield surface at a given point using numerical differentiation. This is a default solution. For a better solution, overwrite this method with a more precies one in the material/shape specific child class that inherits from this class. + """ +<<<<<<< HEAD + Returns the gradient to the yield surface at a given point using numerical differentiation. This is a default solution. For a better solution, overwrite this method with a more precise one in the material/shape specific child class that inherits from this class. +======= + Returns the gradient to the yield surface at a given point using numerical differentiation. + This is a default solution. For a better solution, overwrite this method with a more precies + one in the material/shape specific child class that inherits from this class. +>>>>>>> e49bff2e7890fa6b4069c6c9e2b013a2a90fe7e1 """ # Small increment for numerical differentiation @@ -49,9 +71,13 @@ def __init__(self, model, name, A, Iy, Iz, J, Zy, Zz, material_name): self.rz = (Iz/A)**0.5 self.Zy = Zy self.Zz = Zz + + self.material = model.materials[material_name] def Phi(self, fx, my, mz): - """A method used to determine whether the cross section is elastic or plastic. Values less than 1 indicate the section is elastic. + """ + A method used to determine whether the cross section is elastic or plastic. + Values less than 1 indicate the section is elastic. :param fx: Axial force divided by axial strength. :type fx: float @@ -64,9 +90,9 @@ def Phi(self, fx, my, mz): """ # Plastic strengths for material nonlinearity - Py = self.fy*self.A - Mpy = self.fy*self.Zy - Mpz = self.fy*self.Zz + Py = self.material.fy*self.A + Mpy = self.material.fy*self.Zy + Mpz = self.material.fy*self.Zz # Values for p, my, and mz based on actual loads p = fx/Py @@ -90,9 +116,9 @@ def G(self, fx, my, mz): """ # Plastic strengths for material nonlinearity - Py = self.fy*self.A - Mpy = self.fy*self.Zy - Mpz = self.fy*self.Zz + Py = self.material.fy*self.A + Mpy = self.material.fy*self.Zy + Mpz = self.material.fy*self.Zz # Partial derivatives of Phi dPhi_dfx = 18*fx**5*my**2/(Mpy**2*Py**6) + 2*fx/Py**2 + 7.0*fx*mz**2/(Mpz**2*Py**2) diff --git a/PyNite/Visualization.py b/PyNite/Visualization.py index 7b813cb0..113ea586 100644 --- a/PyNite/Visualization.py +++ b/PyNite/Visualization.py @@ -118,7 +118,7 @@ def render_model(self, interact=True, reset_camera=True): window.Render() # Handle user interaction if requested by the user - if interact == True: + if interact: # Set up an interactor. The interactor style determines how user interactions affect the # view. The trackball camera style behaves much like popular commercial CAD programs. @@ -135,7 +135,6 @@ def render_model(self, interact=True, reset_camera=True): # omitted. I have noticed it will shut down the interactor. window.Finalize() - # Return the window return window def screenshot(self, filepath='console', interact=True, reset_camera=True): @@ -359,246 +358,6 @@ def update(self, reset_camera=True): # Reset the camera if reset_camera: renderer.ResetCamera() -#%% -# The code in this section will be deprecated at some point - -def RenderModel(model, annotation_size=5, deformed_shape=False, deformed_scale=30, - render_loads=True, color_map=None, combo_name='Combo 1', case=None, labels=True, - screenshot=None): - warnings.warn('`RenderModel` will be replaced with `render_model` in a future version of PyNite.', FutureWarning) - render_model(model, annotation_size, deformed_shape, deformed_scale, render_loads, color_map, - True, combo_name, case, labels, screenshot) - -def render_model(model, annotation_size=5, deformed_shape=False, deformed_scale=30, render_loads=True, color_map=None, scalar_bar=True, combo_name='Combo 1', case=None, labels=True, screenshot=None, theme='default'): - ''' - Renders a finite element model using VTK. - - Parameters - ---------- - model : FEModel3D - Finite element model to be rendered. - annotation_size : number, optional - Controls the height of text displayed with the model. The units used for - `annotation_size` are the same as those used for lengths in the model. Sizes - of other objects (such as nodes) are related to this value. The default is - 5. - deformed_shape : boolean, optional - Determines whether the deformed shape will be rendered or not. The model - must be solved to use this feature. Deformed shapes are not available for - load cases, only load combinations. The default is False. - deformed_scale : boolean, optional - Determines what magnification factor will be applied to the deformed - shape. The default is 30. - render_loads : boolean, optional - Determines if loads will be rendered with the model. The default is True. - color_map : string, optional - The type of contour to plot. Acceptable values are: 'dz', 'Mx', 'My', 'Mxy', 'Qx', 'Qy', - 'Sx', 'Sy', 'Txy' - If no value is specified the default is None. - combo_name : string, optional - The load combination used for rendering the deformed shape and the loads. - The default is 'Combo 1'. - case : string, optional - The load case used for rendering loads. The default is None. - labels : boolean, optional - Determines if labels will be rendered. Each label is a single actor in VTK, which slows down - rendering on models with thousands of labels. Set this option to `False` if you want more - speed when rendering and interacting with a large model. This can be very useful on plate - models with large meshes. - screenshot : string, optional - Sends a screenshot to the specified filepath unless set to None. The screenshot will be taken - when the user closes out of the render window. If screenshot is set to 'console' the - screenshot will be returned as an IPython image. Default is None. - - Raises - ------ - Exception - A deformed shape is requested and a load case has been specified. - - Returns - ------- - Ipython Image (if screenshot is set to 'console') - - ''' - - # Input validation - if deformed_shape and case != None: - raise Exception('Deformed shape is only available for load combinations,' - ' not load cases.') - if model.load_combos == {} and render_loads == True and case == None: - raise Exception('Unable to render load combination. No load combinations defined.') - - # Create a visual node for each node in the model - if theme == 'print': color = 'black' - else: color = None - vis_nodes = [] - for node in model.nodes.values(): - vis_nodes.append(VisNode(node, annotation_size, color)) - - # Create a visual auxiliary node for each auxiliary node in the model - vis_aux_nodes = [] - for aux_node in model.aux_nodes.values(): - vis_aux_nodes.append(VisNode(aux_node, annotation_size, color='red')) - - # Create a visual spring for each spring in the model - vis_springs = [] - for spring in model.springs.values(): - vis_springs.append(VisSpring(spring, model.nodes, annotation_size)) - - # Create a visual member for each member in the model - vis_members = [] - for member in model.members.values(): - vis_members.append(VisMember(member, model.nodes, annotation_size, theme)) - - # Create a window - window = vtk.vtkRenderWindow() - - # Set the pixel width and length of the window - window.SetSize(750, 750) - - # Set up the interactor. The interactor style determines how user - # interactions affect the view. The trackball camera style behaves much - # like popular commercial CAD programs. - interactor = vtk.vtkRenderWindowInteractor() - style = vtk.vtkInteractorStyleTrackballCamera() - interactor.SetInteractorStyle(style) - interactor.SetRenderWindow(window) - - # Create a renderer object and add it to the window - renderer = vtk.vtkRenderer() - window.AddRenderer(renderer) - - # Add actors for each spring - for vis_spring in vis_springs: - - # Add the actor for the spring - renderer.AddActor(vis_spring.actor) - - if labels == True: - # Add the actor for the spring label - renderer.AddActor(vis_spring.lblActor) - - # Set the text to follow the camera as the user interacts. This will - # require a reset of the camera (see below) - vis_spring.lblActor.SetCamera(renderer.GetActiveCamera()) - - # Add actors for each member - for vis_member in vis_members: - - # Add the actor for the member - renderer.AddActor(vis_member.actor) - - if labels == True: - # Add the actor for the member label - renderer.AddActor(vis_member.lblActor) - - # Set the text to follow the camera as the user interacts. This will - # require a reset of the camera (see below) - vis_member.lblActor.SetCamera(renderer.GetActiveCamera()) - - # Combine the polydata from each node - - # Create an append filter for combining node polydata - node_polydata = vtk.vtkAppendPolyData() - - for vis_node in vis_nodes: - - # Add the node's polydata - node_polydata.AddInputData(vis_node.polydata.GetOutput()) - - if labels == True: - - # Add the actor for the node label - renderer.AddActor(vis_node.lblActor) - - # Set the text to follow the camera as the user interacts. This will - # require a reset of the camera (see below) - vis_node.lblActor.SetCamera(renderer.GetActiveCamera()) - - # Update the node polydata in the append filter - node_polydata.Update() - - # Create a mapper and actor for the nodes - node_mapper = vtk.vtkPolyDataMapper() - node_mapper.SetInputConnection(node_polydata.GetOutputPort()) - node_actor = vtk.vtkActor() - node_actor.SetMapper(node_mapper) - - # Add the node actor to the renderer - renderer.AddActor(node_actor) - - # Add actors for each auxiliary node - for vis_aux_node in vis_aux_nodes: - - # Add the actor for the auxiliary node - renderer.AddActor(vis_aux_node.actor) - - if labels == True: - - # Add the actor for the auxiliary node label - renderer.AddActor(vis_aux_node.lblActor) - - # Set the text to follow the camera as the user interacts. This will - # require a reset of the camera (see below) - vis_aux_node.lblActor.SetCamera(renderer.GetActiveCamera()) - - # Render the deformed shape if requested - if deformed_shape == True: - _DeformedShape(model, renderer, deformed_scale, annotation_size, combo_name, render_nodes=True, theme=theme) - - # Render the loads if requested - if (combo_name != None or case != None) and render_loads != False: - _RenderLoads(model, renderer, annotation_size, combo_name, case) - - # Render the plates and quads, if present - if model.quads or model.plates: - _RenderContours(model, renderer, deformed_shape, deformed_scale, color_map, scalar_bar, - 12, combo_name) - - # Set the window's background to gray - renderer.SetBackground(0/255, 0/255, 128/255) - - # Reset the camera - renderer.ResetCamera() - - # Render the window - window.Render() - - # Start the interactor. Code execution will pause here until the user closes the window. - interactor.Start() - - # Finalize the render window once the user closes out of it. I don't understand everything - # this does, but I've found screenshots will cause the program to crash if this line is - # omitted. - window.Finalize() - - # Create a screenshot of the last view in the window (if requested) - if screenshot != None: - - # Screenshot code - w2if = vtk.vtkWindowToImageFilter() - w2if.SetInput(window) - w2if.SetInputBufferTypeToRGB() - w2if.ReadFrontBufferOff() - - # These next two lines are in the examples and documentation for VTK, but don't seem to do - # anything. I've left them here in case I find a bug somewhere down the line that needs - # fixing. - # w2if.Update() - # w2if.Modified() - - writer = vtk.vtkPNGWriter() - writer.SetInputConnection(w2if.GetOutputPort()) - - if screenshot == 'console': - writer.SetWriteToMemory(1) - writer.Write() - fig_file = memoryview(writer.GetResult()).tobytes() - return Image(fig_file) - else: - writer.SetFileName(screenshot) - writer.Write() - #%% # Converts a node object into a node for the viewer class VisNode(): diff --git a/README.md b/README.md index 514c4481..d8cf7e60 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ![Build Status](https://github.com/JWock82/PyNite/actions/workflows/build-and-test.yml/badge.svg) +[![codecov](https://codecov.io/gh/JWock82/Pynite/branch/main/graph/badge.svg?token=ZH18US3A7P)](https://codecov.io/gh/JWock82/Pynite) ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyNiteFEA) GitHub code size in bytes ![GitHub last commit](https://img.shields.io/github/last-commit/JWock82/PyNite) @@ -61,7 +62,11 @@ Here's a list of projects that use PyNite: * Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI) # What's New? -v0.0.97 (in progress) +v0.0.98 +* Bug fix for `FEModel3D.add_section`. It was throwing exceptions and had not been updated to match the examples. +* Improvements to spring rendering in `pyvista`. Up until this point spring elements were being rendered as lines. They now render as zigzag lines in `pyvista`. There is still more work for improvement on spring rendering, but this is a good start. + +v0.0.97 * Fixed physical member load and deflection diagrams. Physical members are a newer feature. Member internal results were being reported correctly, but the diagrams for these members had not been revised to plot correctly. The old method for plain members was still being used. Physical members were not considering that a physical member was made from multiple submembers, and results for each span needed to be combined to get the whole plot. * Switched some commonly used python libraries to be installed by default with `Pynite`. Most `Pynite` users will want these libraries installed for full-featured use of `Pynite`. These libraries help with `Pynite` visualizations, plotting, the sparse solver, and `Jupyter Lab` functionality. This is just easier for new python users. I was getting a lot of questions about how to set up libraries, and this takes the guesswork away. This is part of `Pynite's` objective to stay easy to use. diff --git a/Testing/T Matrix Test.py b/Testing/T Matrix Test.py index 5327ae6a..6e3a140a 100644 --- a/Testing/T Matrix Test.py +++ b/Testing/T Matrix Test.py @@ -26,15 +26,24 @@ # Define properties common to all members E = 200000000 # kN/m^2 G = 0.4*E +truss.add_material('Steel',E, G, 0.3, 78.5) + +#Define some section properties J = 100 Iy = 100 Iz = 100 # Define members -truss.add_member('ab', 'a', 'b', E, G, Iy, Iz, J, 20*10**3/1000**2) -truss.add_member('ac', 'a', 'c', E, G, Iy, Iz, J, 30*10**3/1000**2) -truss.add_member('ad', 'a', 'd', E, G, Iy, Iz, J, 40*10**3/1000**2) -truss.add_member('ae', 'a', 'e', E, G, Iy, Iz, J, 30*10**3/1000**2) +truss.add_section('TrussSection1', 20*10**3/1000**2, Iy, Iz, J) +truss.add_member('ab', 'a', 'b', 'Steel', 'TrussSection1') + +truss.add_section('TrussSection2', 30*10**3/1000**2, Iy, Iz, J) +truss.add_member('ac', 'a', 'c', 'Steel', 'TrussSection2') + +truss.add_section('TrussSection3', 40*10**3/1000**2, Iy, Iz, J) +truss.add_member('ad', 'a', 'd', 'Steel', 'TrussSection3') + +truss.add_member('ae', 'a', 'e', 'Steel', 'TrussSection2') # Define member end releases truss.def_releases('ab', False, False, False, False, True, True, diff --git a/Testing/__init__.py b/Testing/__init__.py index 45acb8d9..2ef48592 100644 --- a/Testing/__init__.py +++ b/Testing/__init__.py @@ -6,6 +6,7 @@ """ import unittest +import os # Run the tests in this module # Use warnings flag to suppress the PendingDeprecationWarning @@ -16,9 +17,3 @@ # `TextTestRunner` does not exit the module. CI will get confused unless we save the result # and send the proper exit code. result = unittest.TextTestRunner().run(test_suite) - -# Send the proper exit code for GitHub Actions CI to read -if result.wasSuccessful(): - exit(0) -else: - exit(1) diff --git a/Testing/test_2D_frames.py b/Testing/test_2D_frames.py index dfaf632c..5ea318d7 100644 --- a/Testing/test_2D_frames.py +++ b/Testing/test_2D_frames.py @@ -50,11 +50,13 @@ def test_XY_gravity_load(self): Iy = 250 Iz = 200 A = 12 - frame.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) - frame.add_member('M2', 'N2', 'N3', 'Steel', Iy, Iz, J, A) - frame.add_member('M3', 'N3', 'N4', 'Steel', Iy, Iz, J, A) - frame.add_member('M4', 'N4', 'N5', 'Steel', Iy, Iz, J, A) - frame.add_member('M5', 'N5', 'N6', 'Steel', Iy, Iz, J, A) + frame.add_section('FrameSection', A, Iy, Iz, J) + + frame.add_member('M1', 'N1', 'N2', 'Steel', 'FrameSection') + frame.add_member('M2', 'N2', 'N3', 'Steel', 'FrameSection') + frame.add_member('M3', 'N3', 'N4', 'Steel', 'FrameSection') + frame.add_member('M4', 'N4', 'N5', 'Steel', 'FrameSection') + frame.add_member('M5', 'N5', 'N6', 'Steel', 'FrameSection') # Add nodal loads frame.add_node_load('N3', 'FY', -30) @@ -99,16 +101,21 @@ def test_XY_member_ptload(self): Iz = 82.7/12**4 # ft^4 J = 0.346/12**4 # ft^4 A = 5.26/12**2 # in^2 + frame.add_section('FrameSection', A, Iy, Iz, J) + # Define members - frame.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) - frame.add_member('M2', 'N2', 'N3', 'Steel', Iy, Iz, J, A) - frame.add_member('M3', 'N4', 'N3', 'Steel', Iy, Iz, J, A) + frame.add_member('M1', 'N1', 'N2', 'Steel', 'FrameSection') + frame.add_member('M2', 'N2', 'N3', 'Steel', 'FrameSection') + frame.add_member('M3', 'N4', 'N3', 'Steel', 'FrameSection') + # Add loads to the frame frame.add_member_pt_load('M2', 'Fy', -5, 7.75/2) # 5 kips @ midspan frame.add_member_dist_load('M2', 'Fy', -0.024, -0.024) # W8x24 self-weight + # Analyze the frame frame.analyze() calculated_RZ = frame.nodes['N1'].RZ['Combo 1'] + # Update the expected value to an appropriate precision expected_RZ = 0.00022794540510395617 self.assertAlmostEqual(calculated_RZ/expected_RZ, 1.0, 2) @@ -138,11 +145,14 @@ def test_YZ_gravity_load(self): Iy = 250 Iz = 200 A = 12 - frame.add_member('M1', 'N1', 'N2', 'Steel', Iz, Iy, J, A) - frame.add_member('M2', 'N2', 'N3', 'Steel', Iy, Iz, J, A) - frame.add_member('M3', 'N3', 'N4', 'Steel', Iy, Iz, J, A) - frame.add_member('M4', 'N4', 'N5', 'Steel', Iy, Iz, J, A) - frame.add_member('M5', 'N5', 'N6', 'Steel', Iz, Iy, J, A) + frame.add_section('FrameSection1', A, Iy, Iz, J) + frame.add_section('FrameSection2', A, Iz, Iy, J) + + frame.add_member('M1', 'N1', 'N2', 'Steel', 'FrameSection2') + frame.add_member('M2', 'N2', 'N3', 'Steel', 'FrameSection1') + frame.add_member('M3', 'N3', 'N4', 'Steel', 'FrameSection1') + frame.add_member('M4', 'N4', 'N5', 'Steel', 'FrameSection1') + frame.add_member('M5', 'N5', 'N6', 'Steel', 'FrameSection2') # Add nodal loads frame.add_node_load('N3', 'FY', -30) @@ -197,7 +207,9 @@ def test_XZ_ptload(self): Iy = 100 Iz = 150 J = 250 - SimpleBeam.add_member("M1", "N1", "N2", "Steel", Iy, Iz, J, A) + SimpleBeam.add_section('BeamSection', A, Iy, Iz, J) + + SimpleBeam.add_member("M1", "N1", "N2", "Steel", 'BeamSection') # Provide simple supports SimpleBeam.def_support("N1", True, True, True, False, False, True) SimpleBeam.def_support("N2", True, True, True, False, False, False) @@ -241,11 +253,12 @@ def test_Kassimali_3_35(self): Iz = 204/12**4 J = 0.3/12**4 A = 7.65/12**2 + frame.add_section('FrameSection', A, Iy, Iz, J) - frame.add_member('AC', 'A', 'C', 'Steel', Iy, Iz, J, A) - frame.add_member('BD', 'B', 'D', 'Steel', Iy, Iz, J, A) - frame.add_member('CE', 'C', 'E', 'Steel', Iy, Iz, J, A) - frame.add_member('ED', 'E', 'D', 'Steel', Iy, Iz, J, A) + frame.add_member('AC', 'A', 'C', 'Steel', 'FrameSection') + frame.add_member('BD', 'B', 'D', 'Steel', 'FrameSection') + frame.add_member('CE', 'C', 'E', 'Steel', 'FrameSection') + frame.add_member('ED', 'E', 'D', 'Steel', 'FrameSection') frame.def_support('A', support_DX=True, support_DY=True, support_DZ=True) frame.def_support('B', support_DX=True, support_DY=True, support_DZ=True) diff --git a/Testing/test_AISC_PDelta_benchmarks.py b/Testing/test_AISC_PDelta_benchmarks.py index fcbd2ab8..f874e438 100644 --- a/Testing/test_AISC_PDelta_benchmarks.py +++ b/Testing/test_AISC_PDelta_benchmarks.py @@ -47,6 +47,7 @@ def test_AISC_benchmark_old(self): nu = 0.3 rho = 0.490 # kcf cantilever.add_material('Steel', E, G, nu, rho) + cantilever.add_section('BeamSection', 10/12**2, I, I, 200/12**4) # Add nodes along the length of the column to capture P-little-delta effects num_nodes = 6 @@ -55,7 +56,7 @@ def test_AISC_benchmark_old(self): 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) + cantilever.add_member('M1', 'N1', 'N6', 'Steel', 'BeamSection') # Add a fixed support at the base of the column cantilever.def_support('N1', True, True, True, True, True, True) @@ -118,8 +119,9 @@ def test_AISC_benchmark_case1(self): Iz = 484/12**4 # ft^4 A = 14.1/12**2 # ft^2 J = 1.45/12**4 # ft^4 + column.add_section('ColumnSection', A, Iy, Iz, J) - column.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) + column.add_member('M1', 'N1', 'N2', 'Steel', 'ColumnSection') column.add_member_dist_load('M1', 'Fy', -0.200, -0.200, case='P1') column.add_member_dist_load('M1', 'Fy', -0.200, -0.200, case='P2') @@ -183,8 +185,9 @@ def test_AISC_benchmark_case2(self): Iz = 484/12**4 # ft^4 A = 14.1/12**2 # ft^2 J = 1.45/12**4 # ft^4 + column.add_section('ColumnSection', A, Iy, Iz, J) - column.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) + column.add_member('M1', 'N1', 'N2', 'Steel', 'ColumnSection') column.add_node_load('N2', 'FX', 1, case='P1') column.add_node_load('N2', 'FX', 1, case='P2') diff --git a/Testing/test_TC_analysis.py b/Testing/test_TC_analysis.py index 6ace6c8f..a5865d7a 100644 --- a/Testing/test_TC_analysis.py +++ b/Testing/test_TC_analysis.py @@ -41,9 +41,10 @@ def test_TC_members(self): Iz = 3 # in^4 J = 0.0438 # in^4 A = 1.94 # in^2 + tc_model.add_section('Section', A, Iy, Iz, J) - tc_model.add_member('both-ways', 'N1', 'N2', 'Steel', Iy, Iz, J, A) - tc_model.add_member('t-only top', 'N3', 'N2', 'Steel', Iy, Iz, J, A, tension_only=True) + tc_model.add_member('both-ways', 'N1', 'N2', 'Steel', 'Section') + tc_model.add_member('t-only top', 'N3', 'N2', 'Steel', 'Section', tension_only=True) tc_model.def_releases('t-only top', Ryi=True, Rzi=True, Ryj=True, Rzj=True) tc_model.def_releases('both-ways', Ryi=True, Rzi=True, Ryj=True, Rzj=True) @@ -53,7 +54,7 @@ def test_TC_members(self): tc_model.def_support('N4', *[True]*6) tc_model.add_node_load('N2', 'FY', -10) - tc_model.add_member('t-only bott', 'N4', 'N2', 'Steel', Iy, Iz, J, A, tension_only=True) + tc_model.add_member('t-only bott', 'N4', 'N2', 'Steel', 'Section', tension_only=True) tc_model.def_releases('t-only bott', Ryi=True, Rzi=True, Ryj=True, Rzj=True) tc_model.analyze() diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py new file mode 100644 index 00000000..bd39e6ad --- /dev/null +++ b/Testing/test_Visualization.py @@ -0,0 +1,143 @@ +from io import BytesIO +import unittest +from unittest.mock import MagicMock +import vtk +from PyNite import FEModel3D +from PyNite.Visualization import Renderer +from IPython.display import Image + +class TestRenderer(unittest.TestCase): + + def setUp(self): + + # Create a simple model to visualize + self.beam_model = FEModel3D() + + self.beam_model.add_node('N1', 0, 0, 0) + self.beam_model.add_node('N2', 20, 0, 0) + + self.beam_model.def_support('N1', True, True, True, True, True, True) + self.beam_model.def_support('N2', False, True, True, False, False, False) + + self.beam_model.add_material('Steel', 29000/144, 11200/144, 0.3, 0.49, 36/144) + + self.beam_model.add_section('Custom', 20, 100, 200, 150) + + self.beam_model.add_member('M1', 'N1', 'N2', 'Steel', 'Custom') + + self.beam_model.add_member_dist_load('M1', 'Fy', -1, -1, case='D') + + self.beam_model.add_load_combo('1.4D', {'D': 1.4}) + + self.beam_model.analyze_linear() + + # Create the Renderer instance + self.renderer = Renderer(self.beam_model) + + # Set the renderer window to render offscreen + self.renderer.window.SetOffScreenRendering(1) + + # Set up the load combo to be visualized + self.renderer.combo_name = '1.4D' + self.renderer.render_loads = True + + def test_set_annotation_size(self): + self.renderer.set_annotation_size(10) + self.assertEqual(self.renderer.annotation_size, 10) + + def test_set_deformed_shape(self): + self.renderer.set_deformed_shape(True) + self.assertTrue(self.renderer.deformed_shape) + + def test_set_deformed_scale(self): + self.renderer.set_deformed_scale(50) + self.assertEqual(self.renderer.deformed_scale, 50) + + def test_set_render_nodes(self): + self.renderer.set_render_nodes(False) + self.assertFalse(self.renderer.render_nodes) + + def test_set_render_loads(self): + self.renderer.set_render_loads(False) + self.assertFalse(self.renderer.render_loads) + + def test_set_color_map(self): + self.renderer.set_color_map('Mx') + self.assertEqual(self.renderer.color_map, 'Mx') + + def test_set_combo_name(self): + self.renderer.set_combo_name('Combo 2') + self.assertEqual(self.renderer.combo_name, 'Combo 2') + self.assertIsNone(self.renderer.case) + + def test_set_case(self): + self.renderer.set_case('Case 2') + self.assertEqual(self.renderer.case, 'Case 2') + self.assertIsNone(self.renderer.combo_name) + + def test_set_show_labels(self): + self.renderer.set_show_labels(False) + self.assertFalse(self.renderer.labels) + + def test_set_scalar_bar(self): + self.renderer.set_scalar_bar(True) + self.assertTrue(self.renderer.scalar_bar) + + def test_set_scalar_bar_text_size(self): + self.renderer.set_scalar_bar_text_size(30) + self.assertEqual(self.renderer.scalar_bar_text_size, 30) + + def test_window_size_properties(self): + self.renderer.window.SetSize(800, 600) + self.assertEqual(self.renderer.window_width, 800) + self.assertEqual(self.renderer.window_height, 600) + + self.renderer.window_width = 1024 + self.assertEqual(self.renderer.window.GetSize()[0], 1024) + + self.renderer.window_height = 768 + self.assertEqual(self.renderer.window.GetSize()[1], 768) + + def test_render_model(self): + + # Mock the update and render methods + # self.renderer.update = MagicMock() + # self.renderer.window.Render = MagicMock() + + # Call the render_model method + self.renderer.render_model(interact=False) + + # Assert that the update and render methods were called + # self.renderer.update.assert_called_once_with(True) + # self.renderer.window.Render.assert_called_once_with() + + def test_screenshot_console(self): + + # Mock the render_model method + # self.renderer.render_model = MagicMock(return_value=self.renderer.window) + + # Mock the vtkWindowToImageFilter and vtkPNGWriter + # vtk.vtkWindowToImageFilter = MagicMock() + # vtk.vtkPNGWriter = MagicMock() + + # Call the screenshot method + result = self.renderer.screenshot(filepath='console', interact=False, reset_camera=True) + + # Assert that the render_model method was called + # self.renderer.render_model.assert_called_once_with(False, True) + + # Assert that the result is an instance of IPython.display.Image + self.assertIsInstance(result, Image) + + def test_screenshot_bytesio(self): + + # Call the screenshot method + result = self.renderer.screenshot(filepath='BytesIO', interact=False, reset_camera=True) + + # Assert that the result is an instance of BytesIO + self.assertIsInstance(result, BytesIO) + + def test_screenshot_file(self): + + # Call the screenshot method + self.renderer.screenshot(filepath='test.png', interact=False, reset_camera=True) diff --git a/Testing/test_end_releases.py b/Testing/test_end_releases.py index 7d731b32..c3b717ec 100644 --- a/Testing/test_end_releases.py +++ b/Testing/test_end_releases.py @@ -33,13 +33,14 @@ def test_end_release_Rz(self): nu = 0.3 rho = 0.490/12**3 # kci myModel.add_material('Steel', E, G, nu, rho) + myModel.add_section('ColumnSection', 10, 100, 150, 250) # Add two supported nodes and one member myModel.add_node('N1', 0, 0, 0) myModel.add_node('N2', 10*12, 0, 0) myModel.def_support('N1', True, True, True, True, True, True) myModel.def_support('N2', True, True, True, True, True, True) - myModel.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 10) + myModel.add_member('M1', 'N1', 'N2', 'Steel', 'ColumnSection') # Release Rzi and Rzj on member M1 myModel.def_releases('M1', False, False, False, False, False, True, \ diff --git a/Testing/test_loads.py b/Testing/test_loads.py index bda3c59d..9d98ed34 100644 --- a/Testing/test_loads.py +++ b/Testing/test_loads.py @@ -28,7 +28,8 @@ def test_member_self_weight(self): cont_beam.add_node('N2', 15, 0, 0) cont_beam.add_node('N3', 30, 0, 0) cont_beam.add_material('Steel', 29000*144, 11200*144, 0.3, 0.490) - cont_beam.add_member('M1', 'N1', 'N3', 'Steel', 23.3/12**4, 340/12**4, 0.569/12**4, 10/12**2) # W14x34 + cont_beam.add_section('Section', 10/12**2, 23.3/12**4, 340/12**4, 0.569/12**4) + cont_beam.add_member('M1', 'N1', 'N3', 'Steel', 'Section') # W14x34 cont_beam.def_support('N1', True, True, True, True, False, False) cont_beam.def_support('N2', False, True, True, False, False, False) cont_beam.def_support('N3', False, True, True, False, False, False) @@ -61,7 +62,8 @@ def test_axial_distributed_load(self): Iz = 0.003125 # m^4 J = 1 A = 0.15 # m^2 - Beam.add_member("M1", "N1", "N2", 'Mat1', Iy, Iz, J, A) + Beam.add_section('Section', A, Iy, Iz, J) + Beam.add_member("M1", "N1", "N2", 'Mat1', 'Section') # Supports Beam.def_support("N1", True, True, True, True, True, True) diff --git a/Testing/test_member_internal_results.py b/Testing/test_member_internal_results.py index 6c06301a..91075357 100644 --- a/Testing/test_member_internal_results.py +++ b/Testing/test_member_internal_results.py @@ -45,6 +45,7 @@ def test_beam_internal_forces(self): Iy = 200/12**4 Iz = 200/12**4 A = 12/12**2 + beam.add_section('Section', A, Iy, Iz, J) # Define a material E = 29000*144 # ksf @@ -54,7 +55,7 @@ def test_beam_internal_forces(self): beam.add_material('Steel', E, G, nu, rho) # Create the beam - beam.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) + beam.add_member('M1', 'N1', 'N2', 'Steel', 'Section') # Add a mid-span node beam.add_node('N3', 5, 0, 0) diff --git a/Testing/test_reanalysis.py b/Testing/test_reanalysis.py index 992a1f85..ab91c1d7 100644 --- a/Testing/test_reanalysis.py +++ b/Testing/test_reanalysis.py @@ -41,13 +41,14 @@ def test_reanalysis(self): Iy = 30*24**3/12 J = Iz + Iy A = 24*30 + beam.add_section('Section', A, Iy, Iz, J) # Define a material E = 57*(4500)**0.5 G = 0.4*E beam.add_material('Concrete', E, G, 0.17, 0.150/12**2) - beam.add_member('M1', 'N1', 'N3', 'Concrete', Iy, Iz, J, A) + beam.add_member('M1', 'N1', 'N3', 'Concrete', 'Section') # Add a member load w = -0.100*(10*12) @@ -60,7 +61,7 @@ def test_reanalysis(self): # Change the moment of inertia to account for cracking Iz = 0.35*Iz - beam.members['M1'].Iz = Iz + beam.members['M1'].section.Iz = Iz beam.analyze() diff --git a/Testing/test_shear_wall.py b/Testing/test_shear_wall.py index 91ffde52..667dcfcc 100644 --- a/Testing/test_shear_wall.py +++ b/Testing/test_shear_wall.py @@ -8,6 +8,7 @@ import unittest from PyNite import FEModel3D from PyNite.Mesh import CylinderMesh, RectangleMesh +from PyNite.Rendering import Renderer import sys from io import StringIO from math import isclose @@ -61,6 +62,13 @@ def test_quad_shear_wall(self): # Check that the solution matches the theoretical solution within 0.1% self.assertLess(abs(1 - delta1/delta2), 0.001, 'Failed quad shear wall test.') + rndr = Renderer(sw) + rndr.annotation_size = 5 + rndr.window_width = 700 + rndr.window_height = 700 + rndr.plotter.off_screen = True + rndr.render_model() + def test_rect_shear_wall(self): sw = FEModel3D() diff --git a/Testing/test_sloped_beam.py b/Testing/test_sloped_beam.py index bc48b3b4..cc4911bb 100644 --- a/Testing/test_sloped_beam.py +++ b/Testing/test_sloped_beam.py @@ -46,9 +46,10 @@ def test_sloped_beam(self): Iy = 200/12**4 Iz = 200/12**4 A = 12/12**2 + beam.add_section('Section', A, Iy, Iz, J) # Create the beam - beam.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) + beam.add_member('M1', 'N1', 'N2', 'Steel', 'Section') # Hinge the ends of the beam beam.def_releases('M1', False, False, False, False, True, True, False, False, False, False, True, True) diff --git a/Testing/test_spring_support.py b/Testing/test_spring_support.py index 2080a569..c3ccacf6 100644 --- a/Testing/test_spring_support.py +++ b/Testing/test_spring_support.py @@ -52,12 +52,13 @@ def test_beam_on_elastic_foundation(self): Iz = 128.5 # in^4 (strong axis) Iy = 42.6 # in^4 (weak axis) J = 0.769 # in^4 + boef.add_section('Section', A, Iy, Iz, J) # Define members for i in range(16): # Add the members - boef.add_member('M' + str(i + 1), 'N' + str(i + 1), 'N' + str(i + 2), 'Steel', Iy, Iz, J, A) + boef.add_member('M' + str(i + 1), 'N' + str(i + 1), 'N' + str(i + 2), 'Steel', 'Section') # Add a point load at midspan boef.add_node_load('N9', 'FY', -40) diff --git a/Testing/test_support_settlement.py b/Testing/test_support_settlement.py index 7a07d7e9..ecc5655c 100644 --- a/Testing/test_support_settlement.py +++ b/Testing/test_support_settlement.py @@ -46,10 +46,11 @@ def test_support_settlement(self): Iy = 1000 Iz = 7800 J = 8800 + beam.add_section('Section', A, Iy, Iz, J) - beam.add_member('AB', 'A', 'B', 'Steel', Iy, Iz, J, A) - beam.add_member('BC', 'B', 'C', 'Steel', Iy, Iz, J, A) - beam.add_member('CD', 'C', 'D', 'Steel', Iy, Iz, J, A) + beam.add_member('AB', 'A', 'B', 'Steel', 'Section') + beam.add_member('BC', 'B', 'C', 'Steel', 'Section') + beam.add_member('CD', 'C', 'D', 'Steel', 'Section') # Provide supports beam.def_support('A', True, True, True, True, False, False) diff --git a/Testing/test_torsion.py b/Testing/test_torsion.py index f0eb5cf7..bce33092 100644 --- a/Testing/test_torsion.py +++ b/Testing/test_torsion.py @@ -29,8 +29,10 @@ def test_member_torque_load(self): TorqueBeam.add_node('N2', 168, 0, 0) # Add a material TorqueBeam.add_material('Steel', 29000, 11400, 0.5, 490/1000/12**3) + #Add section + TorqueBeam.add_section('Section', 20, 100, 150, 250) # Add a beam with the following properties: - TorqueBeam.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 20) + TorqueBeam.add_member('M1', 'N1', 'N2', 'Steel', 'Section') # Provide fixed supports TorqueBeam.def_support('N1', False, True, True, True, True, True) TorqueBeam.def_support('N2', True, True, True, True, True, True) diff --git a/Testing/test_unstable_structure.py b/Testing/test_unstable_structure.py index e929eac0..2faa93db 100644 --- a/Testing/test_unstable_structure.py +++ b/Testing/test_unstable_structure.py @@ -40,12 +40,14 @@ def test_unstable_supports(self): # Add columns with the following properties: # Iy = 100 in^4, Iz = 150 in^4, J = 250 in^4, A = 10 in^2 - MomentFrame.add_member("M1", "N1", "N2", 'Steel', 100, 150, 250, 10) - MomentFrame.add_member("M2", "N4", "N3", 'Steel', 100, 150, 250, 10) + MomentFrame.add_section('Section', 10, 100, 150, 250) + MomentFrame.add_member("M1", "N1", "N2", 'Steel', 'Section') + MomentFrame.add_member("M2", "N4", "N3", 'Steel', 'Section') # Add a beam with the following properties: # Iy = 100 in^4, Iz = 250 in^4, J = 250 in^4, A = 15 in^2 - MomentFrame.add_member("M3", "N2", "N3", 'Steel', 100, 250, 250, 15) + MomentFrame.add_section('Section2', 15, 100, 250, 250) + MomentFrame.add_member("M3", "N2", "N3", 'Steel', 'Section2') # Pin the ends of the columns MomentFrame.def_releases('M1', Dzi=True) diff --git a/Testing/x_test_nonlinear.py b/Testing/x_test_nonlinear.py index 9acaa1bd..d6919d09 100644 --- a/Testing/x_test_nonlinear.py +++ b/Testing/x_test_nonlinear.py @@ -33,8 +33,7 @@ def test_plastic_beam(self): plastic_beam.add_material('Stl_A992', E, G, nu, rho, fy) # Define a cross-section - W12 = SteelSection('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) @@ -46,7 +45,7 @@ def test_plastic_beam(self): plastic_beam.def_support('N2', False, True, True, False, False, False) # Add a member - plastic_beam.add_member('M1', 'N1', 'N2', 'A992', section='W12x65') + plastic_beam.add_member('M1', 'N1', 'N2', 'A992', 'W12x65') # Add a load plastic_beam.add_node_load('N2', 'FY', 0.3, 'Push') diff --git a/docs/source/member.rst b/docs/source/member.rst index bb1cc1ce..a478a326 100644 --- a/docs/source/member.rst +++ b/docs/source/member.rst @@ -14,11 +14,12 @@ material and section properties: .. code-block:: python - # Define a few section properties (W12x26) + # Define a section property (W12x26) to assign to the member Iy = 17.3 # (in**4) Weak axis moment of inertia Iz = 204 # (in**4) Strong axis moment of inertia J = 0.300 # (in**4) Torsional constant - A = 7.65 # (in**2) Cross-sectional area + A = 7.65 # (in**2) Cross-sectional area' + my_model.add_section('W12x26', A, Iy, Iz, J) # Define a new material (steel) E = 29000 # (ksi) Modulus of elasticity @@ -29,7 +30,7 @@ material and section properties: # Add a member name 'M1' starting at node 'N1' and ending at node 'N2' # made from a previously defined material named 'Steel' - my_model.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A) + my_model.add_member('M1', 'N1', 'N2', 'Steel', 'W12x26') Local Coordinate System ======================= @@ -80,9 +81,9 @@ Members can be changed to tension or compression only by passing ``tension_only= ``comp_only=True`` to the ``FEModel3D.add_member()`` method. Here's an example: .. code-block:: python - - my_model.add_member('M1', 'N1', 'N2', 'Steel', Iy, Iz, J, A, tension-only=True) - my_model.add_member('M2', 'N1', 'N2', 'Steel', Iy, Iz, J, A, comp-only=True) + my_model.add_section('Section', A, Iy, Iz, J) + my_model.add_member('M1', 'N1', 'N2', 'Steel', 'Section', tension-only=True) + my_model.add_member('M2', 'N1', 'N2', 'Steel', 'Section', comp-only=True) Tension-only and compression-only analysis is an iterative process. When using these types of members be sure to perform a non-linear analysis. Do not use the ``FEModel3D.analyze_linear()`` diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 639e7b06..91e50767 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -29,9 +29,12 @@ Here's a simple example of how to analyze a simple beam. Many more examples are rho = 2.836e-4 # Density (kci) 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 - beam.add_member('M1', 'N1', 'N2', 'Steel', 100, 150, 250, 20) + my_model.add_section('MySection', 20, 100, 150, 250) + + #Add a member + beam.add_member('M1', 'N1', 'N2', 'Steel', 'MySection') # Provide simple supports beam.def_support('N1', True, True, True, False, False, False) diff --git a/requirements.txt b/requirements.txt index a514cc5a..3b784ba0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,10 @@ # Minimum requirements numpy -prettytable \ No newline at end of file +scipy +prettytable +pyvista[all,trame] +matplotlib + +# Required for jupyter interaction +trame_jupyter_extension +ipywidgets diff --git a/setup.py b/setup.py index f8789699..e1fe56f1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="PyNiteFEA", - version="0.0.97", + version="0.0.98", author="D. Craig Brinck, PE, SE", author_email="Building.Code@outlook.com", description="A simple elastic 3D structural finite element library for Python.",