From d6fb9cdcc6d1a9f73a778dfe0bf80ee20b492e43 Mon Sep 17 00:00:00 2001 From: bjhowie <45848617+bjhowie@users.noreply.github.com> Date: Sat, 14 Sep 2024 09:00:31 +0800 Subject: [PATCH 01/35] member section use enforced --- Examples/Beam on Elastic Foundation.py | 6 +- Examples/Braced Frame - Spring Supported.py | 17 +++-- Examples/Braced Frame - Tension Only.py | 15 ++-- Examples/Moment Frame - Lateral Load.py | 8 +- Examples/P-Delta Analysis.py | 5 +- Examples/Simple Beam - Factored Envelope.py | 7 +- Examples/Simple Beam - Point Load.py | 7 +- Examples/Simple Beam - Uniform Load.py | 7 +- Examples/Space Frame - Nodal Loads 1.py | 7 +- Examples/Space Frame - Nodal Loads 2.py | 7 +- Examples/Space Truss - Nodal Load.py | 14 ++-- PyNite/FEModel3D.py | 82 ++++++++++++++------- PyNite/Member3D.py | 57 +++++++------- PyNite/PhysMember.py | 10 +-- PyNite/Plastic Beam.py | 5 +- PyNite/Section.py | 46 +++++++++--- Testing/T Matrix Test.py | 17 ++++- Testing/test_2D_frames.py | 49 +++++++----- Testing/test_AISC_PDelta_benchmarks.py | 9 ++- Testing/test_end_releases.py | 3 +- Testing/test_loads.py | 6 +- Testing/test_member_internal_results.py | 3 +- Testing/test_reanalysis.py | 5 +- Testing/test_sloped_beam.py | 3 +- Testing/test_spring_support.py | 3 +- Testing/test_support_settlement.py | 7 +- Testing/test_torsion.py | 4 +- Testing/test_unstable_structure.py | 8 +- Testing/x_test_nonlinear.py | 5 +- docs/source/member.rst | 9 ++- docs/source/quickstart.rst | 7 +- 31 files changed, 271 insertions(+), 167 deletions(-) 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..7dcab12d 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 diff --git a/Examples/Braced Frame - Tension Only.py b/Examples/Braced Frame - Tension Only.py index 37ab53ee..31a47a2a 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) diff --git a/Examples/Moment Frame - Lateral Load.py b/Examples/Moment Frame - Lateral Load.py index 977148fa..f9b32e54 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) 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/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..c1fd9fa4 100644 --- a/Examples/Space Frame - Nodal Loads 2.py +++ b/Examples/Space Frame - Nodal Loads 2.py @@ -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) 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 4918d5bc..5f0388de 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 @@ -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 @@ -1177,7 +1207,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) @@ -1620,8 +1650,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/Member3D.py b/PyNite/Member3D.py index 1a6b2488..4df5c059 100644 --- a/PyNite/Member3D.py +++ b/PyNite/Member3D.py @@ -22,8 +22,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. """ @@ -31,23 +31,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 @@ -145,12 +138,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 @@ -182,8 +175,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 @@ -465,7 +458,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 @@ -1792,10 +1785,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 ebd22177..b3e8a052 100644 --- a/PyNite/PhysMember.py +++ b/PyNite/PhysMember.py @@ -12,10 +12,10 @@ class PhysMember(Member3D): nodes. """ - 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): + def __init__(self, model, name, i_node, j_node, material_name, section_name, aux_node=None, + tension_only=False, comp_only=False): - super().__init__(name, i_node, j_node, material_name, model, Iy, Iz, J, A, aux_node, tension_only, comp_only, section_name) + super().__init__(model, name, i_node, j_node, material_name, section_name, aux_node, tension_only, comp_only) self.sub_members = {} def descritize(self): @@ -77,9 +77,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/Section.py b/PyNite/Section.py index 38e046cc..ad7f1eee 100644 --- a/PyNite/Section.py +++ b/PyNite/Section.py @@ -2,23 +2,41 @@ import numpy as np class Section(): + """ + A class representing a section assigned to a 3D frame 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. + """ + 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. """ # Small increment for numerical differentiation @@ -49,9 +67,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 +86,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 +112,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/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/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_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_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..ffd74119 100644 --- a/docs/source/member.rst +++ b/docs/source/member.rst @@ -29,7 +29,8 @@ 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_section('Section', A, Iy, Iz, J) + my_model.add_member('M1', 'N1', 'N2', 'Steel', 'Section') 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) From 3708c035b3028fade45de03b1ecb900ab647d31e Mon Sep 17 00:00:00 2001 From: bjhowie <45848617+bjhowie@users.noreply.github.com> Date: Sat, 14 Sep 2024 09:29:13 +0800 Subject: [PATCH 02/35] report + material updates --- PyNite/FEModel3D.py | 2 +- PyNite/Material.py | 7 ++++++- PyNite/Report_Template.html | 12 ++++++------ PyNite/Section.py | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/PyNite/FEModel3D.py b/PyNite/FEModel3D.py index 5f0388de..2c2dd32d 100644 --- a/PyNite/FEModel3D.py +++ b/PyNite/FEModel3D.py @@ -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 diff --git a/PyNite/Material.py b/PyNite/Material.py index a79870d3..d3aef61d 100644 --- a/PyNite/Material.py +++ b/PyNite/Material.py @@ -1,7 +1,12 @@ 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:float | None = None): + self.model = model self.name = name self.E = E self.G = G 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 ad7f1eee..4c02b8ab 100644 --- a/PyNite/Section.py +++ b/PyNite/Section.py @@ -3,7 +3,7 @@ class Section(): """ - A class representing a section assigned to a 3D frame element in a finite element model. + A class representing a section assigned to a Member3D element in a finite element model. This class stores all properties related to the geometry of the member """ From 566546c3bc9e29d54c9f096f92279208e4a93808 Mon Sep 17 00:00:00 2001 From: bjhowie <45848617+bjhowie@users.noreply.github.com> Date: Tue, 24 Sep 2024 21:47:43 +0800 Subject: [PATCH 03/35] Update member.rst --- docs/source/member.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/member.rst b/docs/source/member.rst index ffd74119..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,8 +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_section('Section', A, Iy, Iz, J) - my_model.add_member('M1', 'N1', 'N2', 'Steel', 'Section') + my_model.add_member('M1', 'N1', 'N2', 'Steel', 'W12x26') Local Coordinate System ======================= From ed7456153869610b37662fb556d9abf34e810bb1 Mon Sep 17 00:00:00 2001 From: bjhowie <45848617+bjhowie@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:25:12 +0800 Subject: [PATCH 04/35] Fix TC test + remove 3.10 style type hints --- PyNite/Material.py | 4 +++- Testing/test_TC_analysis.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/PyNite/Material.py b/PyNite/Material.py index d3aef61d..ec1885d9 100644 --- a/PyNite/Material.py +++ b/PyNite/Material.py @@ -1,10 +1,12 @@ +from typing import Optional + class Material(): """ A class representing a material assigned to a Member3D, Plate or Quad in a finite element model. 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:float | None = None): + def __init__(self, model, name:str, E:float, G:float, nu:float, rho:float, fy:Optional[float] = None): self.model = model self.name = name 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() From a6dc2552862ce2363c4d18d75d447fe153ca2dd3 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Sat, 9 Nov 2024 14:14:32 -0700 Subject: [PATCH 05/35] v0.0.97 is no longer "in progress" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 514c4481..188adb3d 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ 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.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. From eefd8e3834d81b597439f48b8f6acabe2138dcaf Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:05:53 -0700 Subject: [PATCH 06/35] Added code coverage --- .coverage | Bin 0 -> 53248 bytes .github/workflows/coverage.yml | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .coverage create mode 100644 .github/workflows/coverage.yml diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..8e895862753c5b271830df0286f005e0a20ec22a GIT binary patch literal 53248 zcmeI5dvH@#9>>p3Zky!haYJcIyMVnwOX<=V4`o1>ra+lyDfF#_6vJ(E+eVw*k_R-q zT9S$~xTBWV2kbH}t6g!JU3bRSKXzG{Dh?z5v5d~_h%>t4$_UD?u%IJgq5C`cCQXXe zaW-)j{hdj2?mdt1{hiP6JaUrewxO=t9gu`J+2;`h!XjcN3`6XKKnO9y(+tm8(7^#4 zJAs%zuR2H7n}B(=NjKMc?=I52X&&M6*4d&0VIF~kid_d zK%~y3v*qM4z4r&imQE?)6I&&p`f6TPUs=&uDKu8BtgaN)G+`bmKqxL2Dg>XrP3V$* zLYuo&65L*wyHyOhz3oDvLyDCS`Xv`#=pYT2I$E$qsZWobZWq)FNbMle<#T&PUr4xD z3Kb~@qE`z@-2pWLL?m~+mzEIbMXMJIKB-OeN#0h;uUawB?JDF7AK1?6Y*VK)J0+z} zqOTPm#tE%~gm_aNZ6MSt`&`ue7N6*C?U4LMf;is7RvGN<4ydj5$)3`HEOffPN;AEF zcfc)s1*uzV4F)9F4I5ZQp~V8gOKc5jZKe&{tk#TPD}heD@(pT6i$^QBfpK0rhBaQ1 zUK%PZHhY7f7C?Sa(9=bo12xnO2jt0WPF6P{M;rT%eRBg}#@jo=oiXIpqiU_7*S#eu zjXS?ch}SGqn&Fj;1lkDb0r|#s+tR5FfnF(f`?qw$`z@y!49Lp46P)i{057H;Q}jAp zZZ5NPi_%dj9%qX?;2%FQOz1S@$&+*)nm>8Jp`&DCC!wRLvr~}Ulp#ZF3#xOVHz$sn z(ijZUM+cf9M+<_b-)V27#)1t(OQ+m2v9BpT#xKDDfw>@7tS#tmr9;I}+t4a+m3(5m zR1(cqr-kpjBSF_$U`V%B=1$gi#=C%X!T2E;$w}AQrcY;j4N5nPdMg^~CUl{gCvPx~ z#nwh@kTl zeWp!l1Wkz#f$J86Z*G2<80ZkbYCBB^!wb^VZMSDlW;iv$xq!Z=Kd@1wn7lJbq3?l7 zpFXt{Or|Mulh;b2;;mvQ%qCr;+Xq7y=3sRN5NJV{2!+KKIT%p-FFjSdCtZ8u_NoH$ z^B8+5Rj3q);aw~H{cc!R>9XZt7E2WhIz&I6h5Q9Vi!66aqE}rTpoYT_*L3>@#fGo0 zO6744e~4JeS)aHmd5P~6rbeML@PjaG{RDd6@<0xhB#qjaooqF-qB zNstL^X<>o(>$(B;mU97prMqN3eJdBqWpuXr^O;C6IvCY?#_5Me-6O`#j-F~Jbdu{b z`y$(ct$(j7X2fDB_+3&Easq98vYF7&+wn$oDD>)kpL1v0!RP}AOR$R z1dsp{Kmter2_S(O0h4wn6Z;fE!)d1*q8|d#@Bedj7YP46zmYdu{%q;6WSUc7sL9QtS z__gnfk;ClF9p|0q)Y` zZC8xkz=7hDq#M1)-7UE)y{>vmRPPKyjWwjiSULh5r1lL|QJ+f1yQrcrC5r1LdM6eI zXni^;7A6g}wj<eAyoGNzy=KZaoaL5T{$f62 zEaOda=W1l1|)z4kN^@u0!ZNIA<)D|ikYO}-Cei; z&$)w*lqRow{r*3DJq!0xO4``i{@+&5MhcRbOWglYOO<4N|DTl_4_&wa&rA)<*#3WN zY8-z3{y(FdjZ9D8L+bv2N*x=?PhKox|8HH#MrJ0j7~TK#wQOWg@@nz@zomwa%t~Hx zeE)AwP3KPB|C>_dlZ5@h@h B>6_)X#dZp#@K}YzagcHbpNkUjbg(7U)R7!3X=yq zVgH|=nywz-|EHy>+4229o0`EhzW>*zX7D8L|204{-;nfjn6UrH`~TfMZ`{yQB!C2v z01`j~NB{{S0VIF~kN^@u0{;gAEu$r7`u#t{j}mxbKmter2_OL^fCP{L57n4-?YgLsR>MkBnqk_w1==NdplaJ0I|Iv4?cM8Y8#t!5_m>sO zS69qEwrIoP-ph;TTr8YXOUNY`OP05iaXoSrzBr4CLp;(&5;9{M;RR4(Gd&BRYUq36 zH?L&8^h))LLoS-qb+zF{NzOyZc(Q`@@070%ZQ?dspe2#pEYE`S$jeX8sLMRLyWzZ_ zdts!xZu7rFyHAGd#qw*pmK|()`R)VzNuJ{^jeW;ZV{L8U`{%xBIGJ11f9i0rX8*~d zL${}`XZkb*EQj!V)!fjh5v!ScPB4d;kP|PjwJ15(?tP2KE6-n@@tRY6v-Wh~o!33s*w?G6 z>d*Ul*ZBdCvopfs_s<;=4B=NF6K<(smyQ{fix6Uu#$iIL`W=I3NjPs{`P+=a#4?6V z(_CWH&oRPbmGQncN6Yi7`o?NBeI##m7uf*{_P?=)46@}~0h7%^p@kf4Ai`xQb)Wsh z;i_rJII|oiBQm(WmW#Wym~D{ zBWt+1Ng%r(w$rPF5RJp+)K8yQVt4_qFd@FRr0O3Be?n{6;i7Oy5Va^ow1g6<88n@Q zPY%5}FfcT9{DZNffzgYnYYw^~^^F7I!lOmRzWu^&?Hf0wQ??7!pJ)Eid#ZY&qyM#r zk9K|Rc+yehV2F{LB2Xz1EFzcE$YJHgO3v6uO6j4U?01HhG=@wgVOqqloa*~mThqX! zv(p+0P~&JSqhle(%6_8bwQwM4*=IKO(7;)H%QVzMn>FuVJFdSpHg+o&VTj4Dj&XsE zki0Yk(9%yrOi1=fA9dFVnOjIpLZGb<_YMs#R?YxuG@hZvnWOuLqX}xnLc79H2qf)2 z9C?@686lWOrH4rsM;~N20JYxhVER^*vP|-jf>#evT^ixAMj-GgTKV(B(4N&*h54m( zhKqk!9Lk>MDfAqhf8U^nm&%r7L4{woXzW)C{vT54GP z!aWrShub;Q>zGkrwMMjaPZN8Yqknk+cy0B79r>+#0K8a(!NSt-|E+vK;lJcR69ovU5V1tl!#@ONUK#MjfRB} z0F>|l$3N^yVJlDy2_OL^fCP{L5 Date: Tue, 12 Nov 2024 08:06:30 -0700 Subject: [PATCH 07/35] Cleaning the working tree --- PyNite/Rendering.py | 10 ++++++++++ PyNite/Section.py | 3 ++- Testing/__init__.py | 10 ++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/PyNite/Rendering.py b/PyNite/Rendering.py index a769ade4..3737669e 100644 --- a/PyNite/Rendering.py +++ b/PyNite/Rendering.py @@ -483,6 +483,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() diff --git a/PyNite/Section.py b/PyNite/Section.py index 38e046cc..bc5c2a9c 100644 --- a/PyNite/Section.py +++ b/PyNite/Section.py @@ -18,7 +18,8 @@ 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. + """ + 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. """ # Small increment for numerical differentiation diff --git a/Testing/__init__.py b/Testing/__init__.py index 45acb8d9..5b7ae608 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 @@ -18,7 +19,8 @@ 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) +if os.getenv('GITHUB_ACTIONS'): # Check if running in GitHub Actions + if result.wasSuccessful(): + exit(0) + else: + exit(1) From 811ef8ed655e866f8c59216854b87b5333e0c22e Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:09:32 -0700 Subject: [PATCH 08/35] Removed unused file --- .github/workflows/coverage.yml | 55 ---------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 7bfe76b7..00000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,55 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: build & test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] - - 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 - 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: | - 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: .github/coverage.xml - flags: unittests - name: codecov-coverage From 1e06b2cfb8cba4d9ae42f82419e131b2d4e55064 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:10:12 -0700 Subject: [PATCH 09/35] Placed coverage in build-and-test.yml --- .github/workflows/build-and-test.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 746e9131..20c9eb97 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: build & test +name: build, test, and code coverage on: push: @@ -11,7 +11,6 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: fail-fast: false @@ -20,22 +19,38 @@ 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 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 + 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: .github/coverage.xml + flags: unittests + name: codecov-coverage + From f197a8c7d19a2a82a0f53b76a9dd8d15277b1aeb Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:13:29 -0700 Subject: [PATCH 10/35] Install coverage in CI env --- .github/workflows/build-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 20c9eb97..fa7044d2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -30,6 +30,7 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 pytest python -m pip install scipy + python -m pip install covverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 From 019f8fa6c6c522d524cde9821e5e0cd49f67c2d7 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:15:30 -0700 Subject: [PATCH 11/35] Fixed typo --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fa7044d2..51af47e0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -30,7 +30,7 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 pytest python -m pip install scipy - python -m pip install covverage + python -m pip install coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 From d0c2b29400ecd4d231caf9dccd6cbe60ce3436af Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:32:45 -0700 Subject: [PATCH 12/35] Stopped exit(0) from executing in GitHub Actions --- Testing/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Testing/__init__.py b/Testing/__init__.py index 5b7ae608..0d6d5227 100644 --- a/Testing/__init__.py +++ b/Testing/__init__.py @@ -19,8 +19,8 @@ result = unittest.TextTestRunner().run(test_suite) # Send the proper exit code for GitHub Actions CI to read -if os.getenv('GITHUB_ACTIONS'): # Check if running in GitHub Actions - if result.wasSuccessful(): - exit(0) - else: - exit(1) +# if os.getenv('GITHUB_ACTIONS'): # Check if running in GitHub Actions +# if result.wasSuccessful(): +# exit(0) +# else: +# exit(1) From fcfa1265263553880a46c8d0e68f742f1bdd1c27 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 08:41:03 -0700 Subject: [PATCH 13/35] Adjusted badges --- .github/workflows/build-and-test.yml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 51af47e0..e23ae6aa 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: build, test, and code coverage +name: build & test on: push: diff --git a/README.md b/README.md index 188adb3d..6baf5dc5 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/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) From f496bbc844d6d3e9bf3c8b05a69c9cb9ec3da97f Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 09:09:51 -0700 Subject: [PATCH 14/35] Corrected .coverage filepath --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e23ae6aa..baac145a 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -51,7 +51,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: - file: .github/coverage.xml + file: .coverage.xml flags: unittests name: codecov-coverage From 2af47bddfe2142857b6f97a06b7fb6319003b8d9 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 09:21:19 -0700 Subject: [PATCH 15/35] Added codecove token --- .github/workflows/build-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index baac145a..524beab2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -54,4 +54,5 @@ jobs: file: .coverage.xml flags: unittests name: codecov-coverage + token: ${{ secrets.CODECOV_TOKEN }} From 19d4f7e8685a0d5e8c961023ae07827f170d06aa Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 09:26:43 -0700 Subject: [PATCH 16/35] Adjusted badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6baf5dc5..2984df4e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Build Status](https://github.com/JWock82/PyNite/actions/workflows/build-and-test.yml/badge.svg) -[![codecov](https://codecov.io/gh/JWock82/Pynite/graph/badge.svg?token=ZH18US3A7P)](https://codecov.io/gh/JWock82/Pynite) +[![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) From e984ca6133b167a1d3874a3f723293f02e78c887 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 09:30:59 -0700 Subject: [PATCH 17/35] Archived old MITC4 element --- {PyNite => Archived}/MITC4.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {PyNite => Archived}/MITC4.py (100%) diff --git a/PyNite/MITC4.py b/Archived/MITC4.py similarity index 100% rename from PyNite/MITC4.py rename to Archived/MITC4.py From 3bc1c65004e2c3e603c8cc3cb6a558d744a10e25 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 18:37:07 -0700 Subject: [PATCH 18/35] Added unit tests for `Visualization.py` --- Testing/test_Visualization.py | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Testing/test_Visualization.py diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py new file mode 100644 index 00000000..accb0f32 --- /dev/null +++ b/Testing/test_Visualization.py @@ -0,0 +1,111 @@ +import unittest +from unittest.mock import MagicMock +import vtk +from PyNite import FEModel3D +from PyNite.Visualization import Renderer, VisNode, VisSpring, VisMember + +class TestRenderer(unittest.TestCase): + + def setUp(self): + + # Mock the model with necessary attributes + self.model = FEModel3D() + + self.model.add_node('N1', 0, 0, 0) + self.model.add_node('N2', 20, 0, 0) + + self.model.def_support('N1', True, True, True, True, True, True) + self.model.def_support('N2', False, True, True, False, False, False) + + self.model.add_material('Steel', 29000/144, 11200/144, 0.3, 0.49, 36/144) + + self.model.add_section('Custom', 20, 100, 200, 150) + + self.model.add_member('M1', 'N1', 'N2', 'Steel', 'Custom') + + self.model.add_member_dist_load('M1', 'Fy', -1, -1, case='D') + + self.model.add_load_combo('1.4D', {'D': 1.4}) + + self.model.analyze_linear() + + # Create the Renderer instance + self.renderer = Renderer(self.model) + + # Set the renderer window to render offscreen + self.renderer.window.SetOffScreenRendering(1) + + 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): + self.renderer.update = MagicMock() + self.renderer.window.Render = MagicMock() + self.renderer.render_model(interact=False) + self.renderer.update.assert_called_once() + self.renderer.window.Render.assert_called_once() + + # def test_screenshot(self): + # self.renderer.render_model = MagicMock() + # self.renderer.render_model.return_value = self.renderer.window + # self.renderer.window.Finalize = MagicMock() + + # result = self.renderer.screenshot(filepath='console', interact=False) + # self.assertIsInstance(result, Image) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From f5f4907ef40fd2d9bc64b1dc0d0626289e0e3d50 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 18:44:41 -0700 Subject: [PATCH 19/35] Updated `requirements.txt` to match v0.0.97 --- .github/workflows/build-and-test.yml | 2 +- PyNite/Visualization.py | 3 +-- requirements.txt | 9 ++++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 524beab2..3c922142 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -29,7 +29,7 @@ jobs: 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 diff --git a/PyNite/Visualization.py b/PyNite/Visualization.py index 7b813cb0..00ce9d78 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): 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 From 668e69279cd985a704b91c4e0303e3865039d304 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 19:21:23 -0700 Subject: [PATCH 20/35] Deprecated old code that is no longer maintained --- PyNite/Visualization.py | 240 ---------------------------------------- 1 file changed, 240 deletions(-) diff --git a/PyNite/Visualization.py b/PyNite/Visualization.py index 00ce9d78..113ea586 100644 --- a/PyNite/Visualization.py +++ b/PyNite/Visualization.py @@ -358,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(): From ac141672c2b85aa0dd853ab87d907fb57896d81b Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 19:22:02 -0700 Subject: [PATCH 21/35] Improved unit tests --- Testing/test_Visualization.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py index accb0f32..b1327722 100644 --- a/Testing/test_Visualization.py +++ b/Testing/test_Visualization.py @@ -1,8 +1,7 @@ import unittest -from unittest.mock import MagicMock -import vtk from PyNite import FEModel3D -from PyNite.Visualization import Renderer, VisNode, VisSpring, VisMember +from PyNite.Visualization import Renderer, render_model +from PIL import Image class TestRenderer(unittest.TestCase): @@ -35,6 +34,13 @@ def setUp(self): # 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.render_loads = True + + def test_render_model_old(self): + self.render_model(self.model, combo_name='1.4D', render_loads=True, interact=False) + def test_set_annotation_size(self): self.renderer.set_annotation_size(10) self.assertEqual(self.renderer.annotation_size, 10) @@ -93,19 +99,10 @@ def test_window_size_properties(self): self.assertEqual(self.renderer.window.GetSize()[1], 768) def test_render_model(self): - self.renderer.update = MagicMock() - self.renderer.window.Render = MagicMock() self.renderer.render_model(interact=False) self.renderer.update.assert_called_once() self.renderer.window.Render.assert_called_once() - # def test_screenshot(self): - # self.renderer.render_model = MagicMock() - # self.renderer.render_model.return_value = self.renderer.window - # self.renderer.window.Finalize = MagicMock() - - # result = self.renderer.screenshot(filepath='console', interact=False) - # self.assertIsInstance(result, Image) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + def test_screenshot(self): + result = self.renderer.screenshot(filepath='console', interact=False) + self.assertIsInstance(result, Image) From a6a113c45a8b3092114a281f3a7dbd8c875f6f9e Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 19:22:35 -0700 Subject: [PATCH 22/35] Removed old unused code --- Testing/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Testing/__init__.py b/Testing/__init__.py index 0d6d5227..2ef48592 100644 --- a/Testing/__init__.py +++ b/Testing/__init__.py @@ -17,10 +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 os.getenv('GITHUB_ACTIONS'): # Check if running in GitHub Actions -# if result.wasSuccessful(): -# exit(0) -# else: -# exit(1) From e2c024052c3982acd23e3ffe1059c0894ff7cb31 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 19:30:33 -0700 Subject: [PATCH 23/35] Removed obsolete import --- Testing/test_Visualization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py index b1327722..fa1e726f 100644 --- a/Testing/test_Visualization.py +++ b/Testing/test_Visualization.py @@ -1,6 +1,6 @@ import unittest from PyNite import FEModel3D -from PyNite.Visualization import Renderer, render_model +from PyNite.Visualization import Renderer from PIL import Image class TestRenderer(unittest.TestCase): From 62b9511f6be8bd5a1559874dcce403d34457b46f Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 20:07:20 -0700 Subject: [PATCH 24/35] Improvements to unit tests --- Testing/test_Visualization.py | 79 ++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py index fa1e726f..0a9cfd20 100644 --- a/Testing/test_Visualization.py +++ b/Testing/test_Visualization.py @@ -1,13 +1,16 @@ +from io import BytesIO import unittest +from unittest.mock import MagicMock +import vtk from PyNite import FEModel3D from PyNite.Visualization import Renderer -from PIL import Image +from IPython.display import Image class TestRenderer(unittest.TestCase): def setUp(self): - # Mock the model with necessary attributes + # Create a simple model to visualize self.model = FEModel3D() self.model.add_node('N1', 0, 0, 0) @@ -36,11 +39,8 @@ def setUp(self): # Set up the load combo to be visualized self.renderer.combo_name = '1.4D' - self.render_loads = True + self.renderer.render_loads = True - def test_render_model_old(self): - self.render_model(self.model, combo_name='1.4D', render_loads=True, interact=False) - def test_set_annotation_size(self): self.renderer.set_annotation_size(10) self.assertEqual(self.renderer.annotation_size, 10) @@ -99,10 +99,69 @@ def test_window_size_properties(self): 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) - self.renderer.update.assert_called_once() - self.renderer.window.Render.assert_called_once() - def test_screenshot(self): - result = self.renderer.screenshot(filepath='console', 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): + + # 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='BytesIO', 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 BytesIO + self.assertIsInstance(result, BytesIO) + + def test_screenshot_file(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 + self.renderer.screenshot(filepath='test.png', 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 vtkPNGWriter's SetFileName and Write methods were called + # vtk.vtkPNGWriter().SetFileName.assert_called_once_with('test.png') + # vtk.vtkPNGWriter().Write.assert_called_once_with() \ No newline at end of file From 8f1571c051e69ebdba4ff19a6bd5d1f5eb2a65f4 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Tue, 12 Nov 2024 20:10:28 -0700 Subject: [PATCH 25/35] Update GitHub Action to run in a headless env --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3c922142..633b57a4 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -42,7 +42,7 @@ jobs: - name: Test with unittest run: | - coverage run --source=PyNite -m unittest discover + xvfb-run -a coverage run --source=PyNite -m unittest discover - name: Convert coverage to XML run: | From b8010773b73bffb5762c47acd1c162c89353448b Mon Sep 17 00:00:00 2001 From: JWock82 Date: Wed, 13 Nov 2024 18:35:55 -0700 Subject: [PATCH 26/35] Added plate rendering to test --- Testing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Testing/__init__.py b/Testing/__init__.py index 2ef48592..f5c34399 100644 --- a/Testing/__init__.py +++ b/Testing/__init__.py @@ -12,7 +12,7 @@ # Use warnings flag to suppress the PendingDeprecationWarning # from numpy.matrix # unittest.main(warnings='ignore') -test_suite = unittest.TestLoader().discover("Testing", pattern='test_*.py') +test_suite = unittest.TestLoader().discover("Testing", pattern='test_shear*.py') # `TextTestRunner` does not exit the module. CI will get confused unless we save the result # and send the proper exit code. From 6d4f9f01e45dd7c42126d09d8f14231b829a284c Mon Sep 17 00:00:00 2001 From: JWock82 Date: Wed, 13 Nov 2024 18:40:39 -0700 Subject: [PATCH 27/35] Fix for rendering test --- Testing/__init__.py | 2 +- Testing/test_shear_wall.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Testing/__init__.py b/Testing/__init__.py index f5c34399..2ef48592 100644 --- a/Testing/__init__.py +++ b/Testing/__init__.py @@ -12,7 +12,7 @@ # Use warnings flag to suppress the PendingDeprecationWarning # from numpy.matrix # unittest.main(warnings='ignore') -test_suite = unittest.TestLoader().discover("Testing", pattern='test_shear*.py') +test_suite = unittest.TestLoader().discover("Testing", pattern='test_*.py') # `TextTestRunner` does not exit the module. CI will get confused unless we save the result # and send the proper exit code. 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() From 75754e1e5994c28a6f0fa025e754092e3b6aa46a Mon Sep 17 00:00:00 2001 From: JWock82 Date: Fri, 15 Nov 2024 20:10:51 -0700 Subject: [PATCH 28/35] Allow for multiple screenshots w/o shutting down the plotter --- PyNite/Rendering.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/PyNite/Rendering.py b/PyNite/Rendering.py index 3737669e..11e257ae 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 multiple screenshots to be taken. Note that the window must be closed with `q`. Closing it with the 'x' button 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): """ From 7d0b802fff69c93b3981cfe9e8b513141d0ee8f8 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Wed, 20 Nov 2024 18:48:37 -0700 Subject: [PATCH 29/35] Updated method keyword args --- Examples/Braced Frame - Spring Supported.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/Braced Frame - Spring Supported.py b/Examples/Braced Frame - Spring Supported.py index 7dcab12d..0f3027fc 100644 --- a/Examples/Braced Frame - Spring Supported.py +++ b/Examples/Braced Frame - Spring Supported.py @@ -100,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 From e29261c1f4b9105090e67658497efe4216dd6fdd Mon Sep 17 00:00:00 2001 From: JWock82 Date: Thu, 21 Nov 2024 21:16:56 -0700 Subject: [PATCH 30/35] Improved spring rendering in `pyvista` --- PyNite/Rendering.py | 89 +++++++++++++++++++++++++++------------------ README.md | 3 ++ 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/PyNite/Rendering.py b/PyNite/Rendering.py index 11e257ae..b55cfc22 100644 --- a/PyNite/Rendering.py +++ b/PyNite/Rendering.py @@ -201,7 +201,7 @@ def screenshot(self, filepath='./Pynite_Image.png', interact=True, reset_camera= # Don't bother showing the image before capturing the screenshot self.plotter.off_screen = True - # 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 multiple screenshots to be taken. Note that the window must be closed with `q`. Closing it with the 'x' button will close the whole plotter down. + # 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): @@ -286,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, self.annotation_size, 'red', deformed=True) # _DeformedShape(self.model, self.deformed_scale, self.annotation_size, self.combo_name, self.render_nodes, self.theme) @@ -506,29 +506,69 @@ 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, size, 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 + # 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) + + # 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) - # Change the color - if color is None: - line.plot(color='magenta') - elif color == 'black': - line.plot(color='black') + # 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): @@ -684,27 +724,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/README.md b/README.md index 2984df4e..dec7e389 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ 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.98 (in progress) +* 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. From 4917ec0737fb3cf236b6901f87769dc438f2595f Mon Sep 17 00:00:00 2001 From: JWock82 Date: Sat, 23 Nov 2024 15:29:23 -0700 Subject: [PATCH 31/35] Removed argument from `plot_spring` --- PyNite/Rendering.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/PyNite/Rendering.py b/PyNite/Rendering.py index b55cfc22..8e97ace6 100644 --- a/PyNite/Rendering.py +++ b/PyNite/Rendering.py @@ -259,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) @@ -286,7 +286,7 @@ def update(self, reset_camera=True): # Render deformed springs for spring in self.model.springs.values(): - self.plot_spring(spring, self.annotation_size, 'red', deformed=True) + self.plot_spring(spring, 'red', deformed=True) # _DeformedShape(self.model, self.deformed_scale, self.annotation_size, self.combo_name, self.render_nodes, self.theme) @@ -506,10 +506,13 @@ 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', deformed=False): + 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.""" + # 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 From c375d2866078724ef12be82ca7a05a2d8e0a0024 Mon Sep 17 00:00:00 2001 From: MPCBBishop Date: Fri, 6 Dec 2024 19:26:56 +0800 Subject: [PATCH 32/35] Fix method keyword args Fix `add_member_dist_load` and `add_node_load` method in `Braced Frame - Tension Only.py` and `Moment Frame - Lateral Load.py`. Change kwargs from `Direction` to `direction`. --- Examples/Braced Frame - Tension Only.py | 10 +++++----- Examples/Moment Frame - Lateral Load.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Examples/Braced Frame - Tension Only.py b/Examples/Braced Frame - Tension Only.py index 31a47a2a..b0ad87b5 100644 --- a/Examples/Braced Frame - Tension Only.py +++ b/Examples/Braced Frame - Tension Only.py @@ -76,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/Moment Frame - Lateral Load.py b/Examples/Moment Frame - Lateral Load.py index f9b32e54..297a1c44 100644 --- a/Examples/Moment Frame - Lateral Load.py +++ b/Examples/Moment Frame - Lateral Load.py @@ -48,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}) From 4ee865975cc424b965b088910696c5318d9af518 Mon Sep 17 00:00:00 2001 From: MPCBBishop Date: Fri, 6 Dec 2024 19:30:48 +0800 Subject: [PATCH 33/35] Fix render code in examples Due to commit 668e69279cd985a704b91c4e0303e3865039d304 , `render_model` is deprecated. So change it to `Rendered` class. --- Examples/Circular Bin with Conical Hopper.py | 10 ++++++++-- Examples/Moment Frame - Lateral Load.py | 9 +++++++-- Examples/Space Frame - Nodal Loads 2.py | 12 +++++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) 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 297a1c44..10b58da4 100644 --- a/Examples/Moment Frame - Lateral Load.py +++ b/Examples/Moment Frame - Lateral Load.py @@ -71,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/Space Frame - Nodal Loads 2.py b/Examples/Space Frame - Nodal Loads 2.py index c1fd9fa4..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() @@ -49,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 From a35b5911a5dfa6f4b296d9a873b0e88bc05d89ce Mon Sep 17 00:00:00 2001 From: MPCBBishop Date: Fri, 6 Dec 2024 19:36:38 +0800 Subject: [PATCH 34/35] Fix FEModel3D and Renderer attr in Examples 1. `FEModel3D` does not have attr like `Quads` or `Meshes`, change it to `quads` and `meshes`, respectively. 2. `Renderer` does not have attr `labels`, change it to `show_labels`. --- Examples/Rectangular Tank Wall - Hydrostatic Loads.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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) From 9a3d9058b47b52b3c101b7418c0556176995c614 Mon Sep 17 00:00:00 2001 From: JWock82 Date: Fri, 6 Dec 2024 12:09:28 -0700 Subject: [PATCH 35/35] Update for v0.0.98 --- README.md | 3 ++- Testing/test_Visualization.py | 48 +++++++++-------------------------- setup.py | 2 +- 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index dec7e389..d8cf7e60 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,8 @@ 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.98 (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 diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py index 0a9cfd20..bd39e6ad 100644 --- a/Testing/test_Visualization.py +++ b/Testing/test_Visualization.py @@ -11,28 +11,28 @@ class TestRenderer(unittest.TestCase): def setUp(self): # Create a simple model to visualize - self.model = FEModel3D() + self.beam_model = FEModel3D() - self.model.add_node('N1', 0, 0, 0) - self.model.add_node('N2', 20, 0, 0) + self.beam_model.add_node('N1', 0, 0, 0) + self.beam_model.add_node('N2', 20, 0, 0) - self.model.def_support('N1', True, True, True, True, True, True) - self.model.def_support('N2', False, True, True, False, False, False) + 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.model.add_material('Steel', 29000/144, 11200/144, 0.3, 0.49, 36/144) + self.beam_model.add_material('Steel', 29000/144, 11200/144, 0.3, 0.49, 36/144) - self.model.add_section('Custom', 20, 100, 200, 150) + self.beam_model.add_section('Custom', 20, 100, 200, 150) - self.model.add_member('M1', 'N1', 'N2', 'Steel', 'Custom') + self.beam_model.add_member('M1', 'N1', 'N2', 'Steel', 'Custom') - self.model.add_member_dist_load('M1', 'Fy', -1, -1, case='D') + self.beam_model.add_member_dist_load('M1', 'Fy', -1, -1, case='D') - self.model.add_load_combo('1.4D', {'D': 1.4}) + self.beam_model.add_load_combo('1.4D', {'D': 1.4}) - self.model.analyze_linear() + self.beam_model.analyze_linear() # Create the Renderer instance - self.renderer = Renderer(self.model) + self.renderer = Renderer(self.beam_model) # Set the renderer window to render offscreen self.renderer.window.SetOffScreenRendering(1) @@ -130,38 +130,14 @@ def test_screenshot_console(self): self.assertIsInstance(result, Image) def test_screenshot_bytesio(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='BytesIO', 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 BytesIO self.assertIsInstance(result, BytesIO) def test_screenshot_file(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 self.renderer.screenshot(filepath='test.png', 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 vtkPNGWriter's SetFileName and Write methods were called - # vtk.vtkPNGWriter().SetFileName.assert_called_once_with('test.png') - # vtk.vtkPNGWriter().Write.assert_called_once_with() \ No newline at end of file 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.",