diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..12c17e97 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,14 @@ +name: lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + prek: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: j178/prek-action@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..4f830ef6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.9 + hooks: + - id: ruff-check + args: [--fix, --show-fixes] diff --git a/Archived/MITC4.py b/Archived/MITC4.py index eba33985..385b6098 100644 --- a/Archived/MITC4.py +++ b/Archived/MITC4.py @@ -10,7 +10,7 @@ from numpy.linalg import inv, det, norm from math import sin, cos -class MITC4(): +class MITC4: """ An isoparametric general quadrilateral element, formulated by superimposing an isoparametric MITC4 bending element with an isoparametric plane stress element. Drilling stability is diff --git a/Examples/Beam on Elastic Foundation.py b/Examples/Beam on Elastic Foundation.py index f5c77bb2..3e49d592 100644 --- a/Examples/Beam on Elastic Foundation.py +++ b/Examples/Beam on Elastic Foundation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ This example shows how to generate a beam on elastic foundation by using spring supports. All units in this model are expressed in terms of kips (force) and inches (length). diff --git a/Examples/Circular Bin with Conical Hopper.py b/Examples/Circular Bin with Conical Hopper.py index dd48beaf..52f3b58c 100644 --- a/Examples/Circular Bin with Conical Hopper.py +++ b/Examples/Circular Bin with Conical Hopper.py @@ -1,6 +1,4 @@ -from math import pi from Pynite.FEModel3D import FEModel3D -from Pynite.Mesh import FrustrumMesh, CylinderMesh t = 0.25/12 E = 29000*1000*12**2 diff --git a/Examples/Pushover Analysis.py b/Examples/Pushover Analysis.py index 26b4cc22..097d0ae2 100644 --- a/Examples/Pushover Analysis.py +++ b/Examples/Pushover Analysis.py @@ -1,4 +1,3 @@ -from math import isclose from Pynite.FEModel3D import FEModel3D diff --git a/Examples/Simple Beam - Factored Envelope.py b/Examples/Simple Beam - Factored Envelope.py index 957c50f3..f15f474f 100644 --- a/Examples/Simple Beam - Factored Envelope.py +++ b/Examples/Simple Beam - Factored Envelope.py @@ -2,11 +2,8 @@ # Import `FEModel3D` from `Pynite` from Pynite import FEModel3D -from matplotlib import pyplot as plt -import numpy as np # Import 'Visualization' for rendering the model -from Pynite import Visualization # Create a new finite element model simple_beam = FEModel3D() diff --git a/Examples/Simple Beam - Point Load.py b/Examples/Simple Beam - Point Load.py index e69429da..029c16a4 100644 --- a/Examples/Simple Beam - Point Load.py +++ b/Examples/Simple Beam - Point Load.py @@ -5,7 +5,6 @@ from Pynite import FEModel3D # Import 'Visualization' for rendering the model -from Pynite import Visualization # Create a new finite element model simple_beam = FEModel3D() diff --git a/Pynite/Analysis.py b/Pynite/Analysis.py index acb6e90f..708694e0 100644 --- a/Pynite/Analysis.py +++ b/Pynite/Analysis.py @@ -4,13 +4,12 @@ import warnings -from numpy import array, atleast_2d, zeros, subtract, matmul, divide, seterr, nanmax, asarray, isfinite +from numpy import array, zeros, subtract, matmul, asarray, isfinite from numpy.linalg import solve, norm, LinAlgError from Pynite.LoadCombo import LoadCombo if TYPE_CHECKING: - from typing import List, Tuple from Pynite.FEModel3D import FEModel3D from numpy import float64 from numpy.typing import NDArray @@ -79,7 +78,7 @@ def _prepare_model(model: FEModel3D, n_modes: int = 0) -> None: _renumber(model) -def _identify_combos(model: FEModel3D, combo_tags: List[str] | None = None) -> List[LoadCombo]: +def _identify_combos(model: FEModel3D, combo_tags: list[str] | None = None) -> list[LoadCombo]: """Returns a list of load combinations that are to be run based on tags given by the user. :param model: The model being analyzed. @@ -241,7 +240,7 @@ def _solve_unknown_disp(K11, rhs, sparse: bool = True, check_stability: bool = T return x -def _first_order(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: List[int], D2_indices: List[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, max_iter: int = 30, spring_tolerance: float = 0, member_tolerance: float = 0, num_steps: int = 1) -> None: +def _first_order(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: list[int], D2_indices: list[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, max_iter: int = 30, spring_tolerance: float = 0, member_tolerance: float = 0, num_steps: int = 1) -> None: """Performs the shared first-order elastic/tension-compression-only solution path. This helper is used by both ``FEModel3D.analyze`` and pushover preload so the normal @@ -326,7 +325,7 @@ def _first_order(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: model.solution = 'Nonlinear TC' -def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: List[int], D2_indices: List[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, max_iter: int = 30) -> None: +def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: list[int], D2_indices: list[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, max_iter: int = 30) -> None: """Performs second order (P-Delta) analysis. This type of analysis is appropriate for most models using beams, columns and braces. Second order analysis is usually required by material-specific codes. Models with slender members and/or members with combined bending and axial loads will generally have more significant P-Delta effects. P-Delta effects in plates/quads are not considered by Pynite at this time. :param model: The finite element model to be solved. @@ -471,7 +470,7 @@ def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArr model.solution = 'P-Delta' -def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num: int, Delta_P1: NDArray[float64], Delta_FER1: NDArray[float64], Delta_FER2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, tol: float = 0, P_Delta: bool = False, max_iter: int = 30) -> None: +def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num: int, Delta_P1: NDArray[float64], Delta_FER1: NDArray[float64], Delta_FER2: NDArray[float64], D1_indices: list[int], D2_indices: list[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, tol: float = 0, P_Delta: bool = False, max_iter: int = 30) -> None: # Run at least one iteration run_step = True @@ -700,7 +699,7 @@ def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num: print('- Restarting load step due to plastic load reversal') -def _unpartition(model: FEModel3D, V1: NDArray[float64], V2: NDArray[float64], V1_indices: List[int], V2_indices: List[int]) -> NDArray[float64]: +def _unpartition(model: FEModel3D, V1: NDArray[float64], V2: NDArray[float64], V1_indices: list[int], V2_indices: list[int]) -> NDArray[float64]: """Unpartitions a vector and returns it as a global vector including all dofs. :param model: The finite element model being evaluated @@ -737,7 +736,7 @@ def _unpartition(model: FEModel3D, V1: NDArray[float64], V2: NDArray[float64], V return V -def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], combo: LoadCombo) -> None: +def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: list[int], D2_indices: list[int], combo: LoadCombo) -> None: """Stores calculated displacements from the solver into the model's displacement vector `_D` and into each node object in the model :param model: The finite element model being evaluated. @@ -773,7 +772,7 @@ def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[flo node.RZ[combo.name] = D[node.ID*6 + 5, 0] -def _sum_displacements(model: FEModel3D, Delta_D1: NDArray[float64], Delta_D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], combo: LoadCombo) -> None: +def _sum_displacements(model: FEModel3D, Delta_D1: NDArray[float64], Delta_D2: NDArray[float64], D1_indices: list[int], D2_indices: list[int], combo: LoadCombo) -> None: """Sums calculated displacements for a load step from the solver into the model's displacement vector `_D` and into each node object in the model. :param model: The finite element model being evaluated. @@ -949,7 +948,7 @@ def _check_TC_convergence(model: FEModel3D, combo_name: str = "Combo 1", log: bo return convergence -def _calc_reactions(model: FEModel3D, log: bool = False, combo_tags: List[str] | None = None) -> None: +def _calc_reactions(model: FEModel3D, log: bool = False, combo_tags: list[str] | None = None) -> None: """ Calculates reactions internally once the model is solved. @@ -1206,7 +1205,7 @@ def _calc_reactions(model: FEModel3D, log: bool = False, combo_tags: List[str] | node.RxnMZ[combo.name] -= k*RZ -def _check_statics(model: FEModel3D, combo_tags: List[str] | None = None) -> None: +def _check_statics(model: FEModel3D, combo_tags: list[str] | None = None) -> None: ''' Checks static equilibrium and prints results to the console. @@ -1290,19 +1289,19 @@ def _check_statics(model: FEModel3D, combo_tags: List[str] | None = None) -> Non SumRMZ += RMZ - RFX*Y + RFY*X # Add the results to the table - statics_table.add_row([combo.name, '{:.3g}'.format(SumFX), '{:.3g}'.format(SumRFX), - '{:.3g}'.format(SumFY), '{:.3g}'.format(SumRFY), - '{:.3g}'.format(SumFZ), '{:.3g}'.format(SumRFZ), - '{:.3g}'.format(SumMX), '{:.3g}'.format(SumRMX), - '{:.3g}'.format(SumMY), '{:.3g}'.format(SumRMY), - '{:.3g}'.format(SumMZ), '{:.3g}'.format(SumRMZ)]) + statics_table.add_row([combo.name, f'{SumFX:.3g}', f'{SumRFX:.3g}', + f'{SumFY:.3g}', f'{SumRFY:.3g}', + f'{SumFZ:.3g}', f'{SumRFZ:.3g}', + f'{SumMX:.3g}', f'{SumRMX:.3g}', + f'{SumMY:.3g}', f'{SumRMY:.3g}', + f'{SumMZ:.3g}', f'{SumRMZ:.3g}']) # Print the static check table print(statics_table) print('') -def _partition_D(model: FEModel3D) -> Tuple[List[int], List[int], NDArray[float64]]: +def _partition_D(model: FEModel3D) -> tuple[list[int], list[int], NDArray[float64]]: """Builds a list with known nodal displacements and with the positions in global stiffness matrix of known and unknown nodal displacements :return: A list of the global matrix indices for the unknown nodal displacements (D1_indices). A list of the global matrix indices for the known nodal displacements (D2_indices). A list of the known nodal displacements (D2). @@ -1388,7 +1387,8 @@ def _partition_D(model: FEModel3D) -> Tuple[List[int], List[int], NDArray[float6 D2_indices.append(node.ID*6 + 5) D2.append(0.0) - # Legacy code on the next line. I will leave it here until the line that follows has been proven over time. + # Legacy code on the next lines. I will leave it here until the line that follows has been proven over time. + # from numpy import atleast_2d # D2 = atleast_2d(D2) # Convert D2 from a list to a matrix @@ -1398,7 +1398,7 @@ def _partition_D(model: FEModel3D) -> Tuple[List[int], List[int], NDArray[float6 return D1_indices, D2_indices, D2 -def _partition(model: FEModel3D, unp_matrix: NDArray[float64] | lil_matrix, D1_indices: List[int], D2_indices: List[int]) -> Tuple[NDArray[float64], NDArray[float64]] | Tuple[NDArray[float64], NDArray[float64], NDArray[float64], NDArray[float64]]: +def _partition(model: FEModel3D, unp_matrix: NDArray[float64] | lil_matrix, D1_indices: list[int], D2_indices: list[int]) -> tuple[NDArray[float64], NDArray[float64]] | tuple[NDArray[float64], NDArray[float64], NDArray[float64], NDArray[float64]]: """Partitions a matrix (or vector) into submatrices (or subvectors) based on degree of freedom boundary conditions. :param unp_matrix: The unpartitioned matrix (or vector) to be partitioned. diff --git a/Pynite/BeamSegZ.py b/Pynite/BeamSegZ.py index e7e02de7..99ab9639 100644 --- a/Pynite/BeamSegZ.py +++ b/Pynite/BeamSegZ.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Created on Mon Nov 6 20:52:31 2017 @@ -10,13 +9,13 @@ from numpy import full if TYPE_CHECKING: - from typing import Any, List + from typing import Any from numpy.typing import NDArray # %% # A mathematically continuous beam segment -class BeamSegZ(): +class BeamSegZ: """ A mathematically continuous beam segment @@ -141,7 +140,7 @@ def axial(self, x: float) -> float: return P1 + (p2 - p1)/(2*L)*x**2 + p1*x - def torsion(self, x: float | List[float] = 0) -> float | None | NDArray[Any]: + def torsion(self, x: float | list[float] = 0) -> float | None | NDArray[Any]: """ Returns the torsional moment in the segment. """ diff --git a/Pynite/FEModel3D.py b/Pynite/FEModel3D.py index e94b6301..e5c736d3 100644 --- a/Pynite/FEModel3D.py +++ b/Pynite/FEModel3D.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Literal import numpy as np -from numpy.linalg import solve import scipy as sp from Pynite.Node3D import Node3D @@ -22,13 +21,12 @@ from Pynite import Analysis if TYPE_CHECKING: - from typing import Dict, List from numpy import float64 from numpy.typing import NDArray # %% -class FEModel3D(): +class FEModel3D: """A 3D finite element model object. This object has methods and dictionaries to create, store, and retrieve results from a finite element model. """ @@ -41,19 +39,19 @@ def __init__(self) -> None: # the data types they store, and then those types will be removed. This will give us the # ability to get type-based hints when using the dictionaries. - self.nodes: Dict[str, Node3D] = {} # A dictionary of the model's nodes - self.materials: Dict[str, Material] = {} # A dictionary of the model's materials - self.sections: Dict[str, Section] = {} # A dictonary of the model's cross-sections - self.springs: Dict[str, Spring3D] = {} # A dictionary of the model's springs - self.members: Dict[str, PhysMember] = {} # A dictionary of the model's physical members - self.quads: Dict[str, Quad3D] = {} # A dictionary of the model's quadiralterals - self.plates: Dict[str, Plate3D] = {} # A dictionary of the model's rectangular plates - self.meshes: Dict[str, Mesh] = {} # A dictionary of the model's meshes - self.shear_walls: Dict[str, ShearWall] = {} # A dictionary of the model's shear walls - self.mats: Dict[str, MatFoundation] = {} # A dictionary of the model's mat foundations - self.load_combos: Dict[str, LoadCombo] = {} # A dictionary of the model's load combinations - self._D: Dict[str, NDArray[float64]] = {} # A dictionary of the model's nodal displacements by load combination - self._pushover_traces: Dict[str, Dict[str, List]] = {} + self.nodes: dict[str, Node3D] = {} # A dictionary of the model's nodes + self.materials: dict[str, Material] = {} # A dictionary of the model's materials + self.sections: dict[str, Section] = {} # A dictonary of the model's cross-sections + self.springs: dict[str, Spring3D] = {} # A dictionary of the model's springs + self.members: dict[str, PhysMember] = {} # A dictionary of the model's physical members + self.quads: dict[str, Quad3D] = {} # A dictionary of the model's quadiralterals + self.plates: dict[str, Plate3D] = {} # A dictionary of the model's rectangular plates + self.meshes: dict[str, Mesh] = {} # A dictionary of the model's meshes + self.shear_walls: dict[str, ShearWall] = {} # A dictionary of the model's shear walls + self.mats: dict[str, MatFoundation] = {} # A dictionary of the model's mat foundations + self.load_combos: dict[str, LoadCombo] = {} # A dictionary of the model's load combinations + self._D: dict[str, NDArray[float64]] = {} # A dictionary of the model's nodal displacements by load combination + self._pushover_traces: dict[str, dict[str, list]] = {} self.solution: str | None = None # Indicates the solution type for the latest run of the model @@ -167,12 +165,12 @@ def _add_dense_block(global_matrix: np.ndarray, dofs: NDArray[np.int64], block: global_matrix[np.ix_(dofs, dofs)] += block @property - def load_cases(self) -> List[str]: + def load_cases(self) -> list[str]: """Returns a list of all the load cases in the model (in alphabetical order). """ # Create an empty list of load cases - cases: List[str] = [] + cases: list[str] = [] # Step through each node for node in self.nodes.values(): @@ -887,7 +885,7 @@ def add_cylinder_mesh(self, name:str, mesh_size:float, radius:float, height:floa # Return the mesh's name return name - def add_shear_wall(self, name: str, mesh_size: float, length: float, height: float, thickness: float, material_name: str, ky_mod: float = 0.35, plane: Literal['XY', 'YZ'] = 'XY', origin: List[float] = [0, 0, 0]): + def add_shear_wall(self, name: str, mesh_size: float, length: float, height: float, thickness: float, material_name: str, ky_mod: float = 0.35, plane: Literal['XY', 'YZ'] = 'XY', origin: list[float] = [0, 0, 0]): """Adds a meshed shear wall helper to the model. The shear wall utility generates a regular mesh for a rectangular wall panel and @@ -2366,7 +2364,7 @@ def analyze(self, log=False, check_stability=True, check_statics=False, max_iter # Import `scipy` features if the sparse solver is being used if sparse == True: - from scipy.sparse.linalg import spsolve + from scipy.sparse.linalg import spsolve # noqa: F401 # Prepare the model for analysis Analysis._prepare_model(self) @@ -2432,7 +2430,7 @@ def analyze_PDelta(self, log=False, check_stability=True, max_iter=30, sparse=Tr # Import `scipy` features if the sparse solver is being used if sparse == True: - from scipy.sparse.linalg import spsolve + from scipy.sparse.linalg import spsolve # noqa: F401 # Prepare the model for analysis Analysis._prepare_model(self) diff --git a/Pynite/FixedEndReactions.py b/Pynite/FixedEndReactions.py index 71e79509..8e27cadf 100644 --- a/Pynite/FixedEndReactions.py +++ b/Pynite/FixedEndReactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Created on Fri Nov 3 20:58:03 2017 diff --git a/Pynite/LoadCombo.py b/Pynite/LoadCombo.py index ef020ad3..4bc7ce13 100644 --- a/Pynite/LoadCombo.py +++ b/Pynite/LoadCombo.py @@ -1,11 +1,10 @@ from __future__ import annotations # Allows more recent type hints features -from typing import Dict, List -class LoadCombo(): +class LoadCombo: """A class that stores all the information necessary to define a load combination. """ - def __init__(self, name: str, combo_tags: List[str] | None = None, factors: Dict[str, float] = {}) -> None: + def __init__(self, name: str, combo_tags: list[str] | None = None, factors: dict[str, float] = {}) -> None: """Initializes a new load combination. :param name: A unique name for the load combination. @@ -17,8 +16,8 @@ def __init__(self, name: str, combo_tags: List[str] | None = None, factors: Dict """ self.name: str = name # A unique user-defined name for the load combination - self.combo_tags: List[str] | None = combo_tags # Used to categorize the load combination (e.g. strength or serviceability) - self.factors: Dict[str, float] = factors # A dictionary containing each load case name and associated load factor + self.combo_tags: list[str] | None = combo_tags # Used to categorize the load combination (e.g. strength or serviceability) + self.factors: dict[str, float] = factors # A dictionary containing each load case name and associated load factor def __repr__(self) -> str: return f"LoadCombo(name={self.name!r}, factors={self.factors!r})" diff --git a/Pynite/Material.py b/Pynite/Material.py index bcc898a4..6495d6bb 100644 --- a/Pynite/Material.py +++ b/Pynite/Material.py @@ -4,7 +4,7 @@ if TYPE_CHECKING: from Pynite.FEModel3D import FEModel3D -class Material(): +class Material: """ A class representing a material assigned to a Member3D, Plate or Quad in a finite element model. diff --git a/Pynite/Member3D.py b/Pynite/Member3D.py index 66b0fc78..288df07e 100644 --- a/Pynite/Member3D.py +++ b/Pynite/Member3D.py @@ -1,5 +1,5 @@ from __future__ import annotations # Allows more recent type hints features -from typing import TYPE_CHECKING, Literal, Union, List +from typing import TYPE_CHECKING, Literal from math import isclose from numpy import array, zeros, add, subtract, matmul, insert, dot, cross, divide, count_nonzero, concatenate @@ -12,7 +12,7 @@ if TYPE_CHECKING: - from typing import Dict, List, Tuple, Optional, Any, Literal + from typing import Any, Literal from numpy import float64 from numpy.typing import NDArray @@ -24,7 +24,7 @@ from Pynite.LoadCombo import LoadCombo -class Member3D(): +class Member3D: """ A class representing a 3D frame element in a finite element model. @@ -95,17 +95,17 @@ def __init__(self, model: FEModel3D, name: str, i_node: Node3D, self.j_reversal: bool = False self.rotation: float = rotation # Member rotation (degrees) about its local x-axis - self.PtLoads: List[Tuple] = [] # A list of point loads & moments applied to the element (Direction, P, x, case='Case 1') or (Direction, M, x, case='Case 1') - self.DistLoads: List[Tuple] = [] # A list of linear distributed loads applied to the element (Direction, w1, w2, x1, x2, case='Case 1', self_weight=False) - self.SegmentsZ: List[BeamSegZ] = [] # A list of mathematically continuous beam segments for z-bending - self.SegmentsY: List[BeamSegY] = [] # A list of mathematically continuous beam segments for y-bending - self.SegmentsX: List[BeamSegZ] = [] # A list of mathematically continuous beam segments for torsion - self.Releases: List[bool] = [False, False, False, False, False, False, False, False, False, False, False, False] + self.PtLoads: list[tuple] = [] # A list of point loads & moments applied to the element (Direction, P, x, case='Case 1') or (Direction, M, x, case='Case 1') + self.DistLoads: list[tuple] = [] # A list of linear distributed loads applied to the element (Direction, w1, w2, x1, x2, case='Case 1', self_weight=False) + self.SegmentsZ: list[BeamSegZ] = [] # A list of mathematically continuous beam segments for z-bending + self.SegmentsY: list[BeamSegY] = [] # A list of mathematically continuous beam segments for y-bending + self.SegmentsX: list[BeamSegZ] = [] # A list of mathematically continuous beam segments for torsion + self.Releases: list[bool] = [False, False, False, False, False, False, False, False, False, False, False, False] self.tension_only: bool = tension_only # Indicates whether the member is tension-only self.comp_only: bool = comp_only # Indicates whether the member is compression-only # Members need to track whether they are active or not for any given load combination. They may become inactive for a load combination during a tension/compression-only analysis. This dictionary will be used when the model is solved. - self.active: Dict[str, bool] = {} # Key = load combo name, Value = True or False + self.active: dict[str, bool] = {} # Key = load combo name, Value = True or False # The 'Member3D' object will store results for one load combination at a time. To reduce repetative calculations the '_solved_combo' variable will be used to track whether the member needs to be resegmented before running calculations for any given load combination. self._solved_combo: LoadCombo | None = None # The current solved load combination @@ -130,7 +130,7 @@ def L(self) -> float: return self.i_node.distance(self.j_node) # %% - def _partition_D(self) -> Tuple[List[int], List[int]]: + def _partition_D(self) -> tuple[list[int], list[int]]: """ Builds lists of unreleased and released degree of freedom indices for the member. @@ -1200,7 +1200,7 @@ def shear(self, Direction: Literal['Fy', 'Fz'], x: float, combo_name: str = 'Com return 0 - def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum shear force in the member for the specified direction and load combination(s). @@ -1269,7 +1269,7 @@ def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[ return (Vmax_global, governing_combo) return Vmax_global - def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum shear force in the member for the specified direction and load combination(s). @@ -1338,7 +1338,7 @@ def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[ return (Vmin_global, governing_combo) return Vmin_global - def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the shear diagram for the member. @@ -1507,7 +1507,7 @@ def moment(self, Direction: Literal['My', 'Mz'], x: float, combo_name: str = 'Co return 0 - def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum bending moment in the member for the specified direction and load combination(s). @@ -1585,7 +1585,7 @@ def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List return (Mmax_global, governing_combo) return Mmax_global - def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum bending moment in the member for the specified direction and load combination(s). @@ -1663,7 +1663,7 @@ def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List return Mmin_global - def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the moment diagram for the member. @@ -1723,7 +1723,7 @@ def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, Lis ax.set_xlabel('Location') Member3D.__plt.show() - def moment_array(self, Direction: Literal['My', 'Mz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]: + def moment_array(self, Direction: Literal['My', 'Mz'], n_points: int, combo_name: str = 'Combo 1', x_array: NDArray[float64] | None = None) -> NDArray[float64]: """ Returns the array of the moment in the member for the given direction @@ -1814,7 +1814,7 @@ def torque(self, x: float, combo_name: str = 'Combo 1') -> float: return 0 - def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_torque(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum torsional moment in the member across the specified load combination(s). @@ -1879,7 +1879,7 @@ def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[flo return Tmax_global - def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_torque(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum torsional moment in the member across the specified load combination(s). @@ -1943,7 +1943,7 @@ def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[flo return (Tmin_global, governing_combo) return Tmin_global - def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_torque(self, combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the torque diagram for the member. @@ -2061,7 +2061,7 @@ def axial(self, x: float, combo_name: str = 'Combo 1') -> float: return 0 - def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_axial(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum axial force in the member across the specified load combination(s). @@ -2126,7 +2126,7 @@ def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[floa return (Pmax_global, governing_combo) return Pmax_global - def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_axial(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum axial force in the member across the specified load combination(s). @@ -2191,7 +2191,7 @@ def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[floa return (Pmin_global, governing_combo) return Pmin_global - def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_axial(self, combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the axial force diagram for the member. @@ -2247,7 +2247,7 @@ def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: in ax.set_xlabel('Location') Member3D.__plt.show() - def axial_array(self, n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]: + def axial_array(self, n_points: int, combo_name: str = 'Combo 1', x_array: NDArray[float64] | None = None) -> NDArray[float64]: """ Returns the array of the axial force in the member for the given direction @@ -2354,7 +2354,7 @@ def deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: return 0 - def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum deflection in the member across the specified load combination(s). @@ -2426,7 +2426,7 @@ def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union return dmax_global - def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum deflection in the member across the specified load combination(s). @@ -2497,7 +2497,7 @@ def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union return (dmin_global, governing_combo) return dmin_global - def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the deflection diagram for the member. @@ -2558,7 +2558,7 @@ def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Unio ax.set_xlabel('Location') Member3D.__plt.show() - def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]: + def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: NDArray[float64] | None = None) -> NDArray[float64]: """ Returns the array of the deflection in the member for the given direction @@ -2734,7 +2734,7 @@ def plot_rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Member3D.__plt.title('Member ' + self.name + '\n' + combo_name) Member3D.__plt.show() - def rel_deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]: + def rel_deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: NDArray[float64] | None = None) -> NDArray[float64]: """ Returns the array of the relative deflection in the member for the given direction @@ -3101,7 +3101,7 @@ def sum_load_effects(case, factor): for case, factor in self.model.load_combos[push_combo].factors.items(): sum_load_effects(case, factor*push_step) - def _extract_vector_results(self, segments: List, x_array: NDArray[float64], result_name: Literal['moment', 'shear', 'axial', 'torque', 'deflection', 'axial_deflection'], P_delta: bool = False) -> NDArray[float64]: + def _extract_vector_results(self, segments: list, x_array: NDArray[float64], result_name: Literal['moment', 'shear', 'axial', 'torque', 'deflection', 'axial_deflection'], P_delta: bool = False) -> NDArray[float64]: """ Extracts result values at specified locations along a structural member using efficient, vectorized evaluation of piecewise segment functions. diff --git a/Pynite/Mesh.py b/Pynite/Mesh.py index 0883eb37..fd728739 100644 --- a/Pynite/Mesh.py +++ b/Pynite/Mesh.py @@ -7,11 +7,10 @@ from Pynite.Plate3D import Plate3D if TYPE_CHECKING: - from typing import List, Union, Dict from Pynite.FEModel3D import FEModel3D -class Mesh(): +class Mesh: """ A parent class for meshes to inherit from. """ @@ -44,8 +43,8 @@ def __init__(self, thickness: float, material_name: str, model: FEModel3D, kx_mo self.last_node = None # The name of the last node in the mesh self.start_element = start_element # The name of the first element in the mesh self.last_element = None # The name of the last element in the mesh - self.nodes: Dict[str, Node3D] = {} # A dictionary containing the nodes in the mesh - self.elements: Dict[str, Union[Quad3D, Plate3D]] = {} # A dictionary containing the elements in the mesh + self.nodes: dict[str, Node3D] = {} # A dictionary containing the nodes in the mesh + self.elements: dict[str, Quad3D | Plate3D] = {} # A dictionary containing the elements in the mesh self.element_type = 'Quad' # The type of element used in the mesh self.is_generated = False # A flag indicating whether the mesh has been generated self.needs_update = False # A flag indicating whether the mesh needs regeneration due to changes @@ -112,8 +111,8 @@ def _rename_duplicates(self) -> None: """ # Initialize lists to track node and element name changes - revised_nodes: Dict[str, Node3D] = {} - revised_elements: Dict[str, Union[Quad3D, Plate3D]] = {} + revised_nodes: dict[str, Node3D] = {} + revised_elements: dict[str, Quad3D | Plate3D] = {} # Step through each node in the mesh for node in self.nodes.values(): @@ -718,7 +717,7 @@ def min_membrane(self, direction: str = 'Sx', combo_tags: str | list[str] = 'Com class RectangleMesh(Mesh): - def __init__(self, mesh_size: float, width: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0, ky_mod: float = 1.0, origin: List[float] = [0, 0, 0], plane: str = 'XY', x_control: List[float] | None = None, y_control: List[float] | None = None, start_node: str = 'N1', start_element: str = 'Q1', element_type: str = 'Quad') -> None: + def __init__(self, mesh_size: float, width: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0, ky_mod: float = 1.0, origin: list[float] = [0, 0, 0], plane: str = 'XY', x_control: list[float] | None = None, y_control: list[float] | None = None, start_node: str = 'N1', start_element: str = 'Q1', element_type: str = 'Quad') -> None: """ A rectangular mesh of elements. @@ -772,7 +771,7 @@ def __init__(self, mesh_size: float, width: float, height: float, thickness: flo else: self.y_control = y_control self.element_type = element_type - self.openings: Dict[str, RectOpening] = {} + self.openings: dict[str, RectOpening] = {} def __repr__(self) -> str: return f"RectangleMesh(material_name={self.material_name!r}, width={self.width}, height={self.height}, thickness={self.thickness})" @@ -1096,7 +1095,7 @@ def add_rect_opening(self, name: str, x_left: float, y_bott: float, width: float self.needs_update = True -class RectOpening(): +class RectOpening: """ Represents a rectangular opening in a rectangular mesh. """ @@ -1129,7 +1128,7 @@ class AnnulusMesh(Mesh): """ def __init__(self, mesh_size: float, outer_radius: float, inner_radius: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, - ky_mod: float = 1, origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: + ky_mod: float = 1, origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: """Annular (donut) mesh between inner and outer radii. @@ -1262,7 +1261,7 @@ class AnnulusRingMesh(Mesh): """ def __init__(self, outer_radius: float, inner_radius: float, num_quads: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, - origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: + origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: """Single annular ring of quads between two radii. @@ -1422,7 +1421,7 @@ class AnnulusTransRingMesh(Mesh): A mesh of quadrilaterals forming an annular ring (a donut) with the mesh transitioning to a finer on the outer edge. """ - def __init__(self, outer_radius: float, inner_radius: float, num_inner_quads: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: + def __init__(self, outer_radius: float, inner_radius: float, num_inner_quads: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: """Creates an annular ring (a donut) with the mesh transitioning to a finer mesh on the outer edge :param outer_radius: The outer radius of the annular ring. @@ -1626,7 +1625,7 @@ class FrustrumMesh(AnnulusMesh): """ def __init__(self, mesh_size: float, large_radius: float, small_radius: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, - origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: + origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None: """Conical frustum mesh generated from an annulus and then tapered to height. :param mesh_size: Target element size for the base annulus. @@ -1702,7 +1701,7 @@ def generate(self) -> None: #%% class CylinderMesh(Mesh): - def __init__(self, mesh_size: float, radius: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1', num_elements: int | None = None, element_type: str = 'Quad') -> None: + def __init__(self, mesh_size: float, radius: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1', num_elements: int | None = None, element_type: str = 'Quad') -> None: """Cylindrical shell mesh. @@ -1885,7 +1884,7 @@ class CylinderRingMesh(Mesh): """ def __init__(self, radius: float, height: float, num_elements: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, - origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1', + origin: list[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1', element_type: str = 'Quad') -> None: super().__init__(thickness, material_name, model, kx_mod, ky_mod, start_node=start_node, start_element=start_element) @@ -2035,7 +2034,7 @@ def generate(self) -> None: self.is_generated = True self.needs_update = False -def check_mesh_integrity(mesh: Mesh, console_log: bool = True) -> Union[str, List[str], None]: +def check_mesh_integrity(mesh: Mesh, console_log: bool = True) -> str | list[str] | None: """Runs basic integrity checks to ensure the mesh is in sync with its model. Usually you don't want to run this check unless the mesh has been generated since generating the mesh is what syncs it to the model. diff --git a/Pynite/Node3D.py b/Pynite/Node3D.py index 12344b06..9211a198 100644 --- a/Pynite/Node3D.py +++ b/Pynite/Node3D.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Created on Thu Nov 2 18:04:56 2017 @@ -6,21 +5,18 @@ """ from __future__ import annotations # Allows more recent type hints features # %% -from numpy import array, zeros +from numpy import zeros -from typing import List, Tuple, Dict, Optional,TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Dict, List, Tuple, Optional, Any, Literal - from numpy import float64 from numpy.typing import NDArray from Pynite.FEModel3D import FEModel3D - from Pynite.LoadCombo import LoadCombo -class Node3D(): +class Node3D: """ A class representing a node in a 3D finite element model. """ @@ -28,29 +24,29 @@ class Node3D(): def __init__(self, model: FEModel3D, name: str, X: float, Y: float, Z: float): self.name = name # A unique name for the node assigned by the user - self.ID: Optional[int] = None # A unique index number for the node assigned by the program + self.ID: int | None = None # A unique index number for the node assigned by the program self.X = X # Global X coordinate self.Y = Y # Global Y coordinate self.Z = Z # Global Z coordinate - self.NodeLoads: List[Tuple[str, float, str]] = [] # A list of loads applied to the node (Direction, P, case) or (Direction, M, case) + self.NodeLoads: list[tuple[str, float, str]] = [] # A list of loads applied to the node (Direction, P, case) or (Direction, M, case) # Initialize the dictionaries of calculated node displacements - self.DX: Dict[str, float] = {} - self.DY: Dict[str, float] = {} - self.DZ: Dict[str, float] = {} - self.RX: Dict[str, float] = {} - self.RY: Dict[str, float] = {} - self.RZ: Dict[str, float] = {} + self.DX: dict[str, float] = {} + self.DY: dict[str, float] = {} + self.DZ: dict[str, float] = {} + self.RX: dict[str, float] = {} + self.RY: dict[str, float] = {} + self.RZ: dict[str, float] = {} # Initialize the dictionaries of calculated node reactions - self.RxnFX: Dict[str, float] = {} - self.RxnFY: Dict[str, float] = {} - self.RxnFZ: Dict[str, float] = {} - self.RxnMX: Dict[str, float] = {} - self.RxnMY: Dict[str, float] = {} - self.RxnMZ: Dict[str, float] = {} + self.RxnFX: dict[str, float] = {} + self.RxnFY: dict[str, float] = {} + self.RxnFZ: dict[str, float] = {} + self.RxnMX: dict[str, float] = {} + self.RxnMY: dict[str, float] = {} + self.RxnMZ: dict[str, float] = {} # Initialize all support conditions to `False` self.support_DX: bool = False @@ -61,12 +57,12 @@ def __init__(self, model: FEModel3D, name: str, X: float, Y: float, Z: float): self.support_RZ: bool = False # Inititialize all support springs - self.spring_DX: List[float | str | bool | None] = [None, None, None] # [stiffness, direction, active] - self.spring_DY: List[float | str | bool | None] = [None, None, None] - self.spring_DZ: List[float | str | bool | None] = [None, None, None] - self.spring_RX: List[float | str | bool | None] = [None, None, None] - self.spring_RY: List[float | str | bool | None] = [None, None, None] - self.spring_RZ: List[float | str | bool | None] = [None, None, None] + self.spring_DX: list[float | str | bool | None] = [None, None, None] # [stiffness, direction, active] + self.spring_DY: list[float | str | bool | None] = [None, None, None] + self.spring_DZ: list[float | str | bool | None] = [None, None, None] + self.spring_RX: list[float | str | bool | None] = [None, None, None] + self.spring_RY: list[float | str | bool | None] = [None, None, None] + self.spring_RZ: list[float | str | bool | None] = [None, None, None] # Initialize all enforced displacements to `None` self.EnforcedDX: float | None = None @@ -77,7 +73,7 @@ def __init__(self, model: FEModel3D, name: str, X: float, Y: float, Z: float): self.EnforcedRZ: float | None = None # Initialize the color contour value for the node. This will be used for contour smoothing. - self.contour: List[float] = [] + self.contour: list[float] = [] # The 'Node3D' object will store results for one load combination at a time. @@ -87,7 +83,7 @@ def __init__(self, model: FEModel3D, name: str, X: float, Y: float, Z: float): def __repr__(self) -> str: return f"Node3D(name={self.name!r}, X={self.X}, Y={self.Y}, Z={self.Z})" - def distance(self, other: 'Node3D') -> float: + def distance(self, other: Node3D) -> float: """ Returns the distance to another node. diff --git a/Pynite/PhysMember.py b/Pynite/PhysMember.py index bf761a09..742f3b74 100644 --- a/Pynite/PhysMember.py +++ b/Pynite/PhysMember.py @@ -1,18 +1,17 @@ from __future__ import annotations # Allows more recent type hints features -from typing import Dict, List, Literal, Tuple, Union, TYPE_CHECKING +from typing import Literal, TYPE_CHECKING from Pynite.Member3D import Member3D if TYPE_CHECKING: from Pynite.Node3D import Node3D from Pynite.FEModel3D import FEModel3D - import numpy.typing as npt from numpy import float64 from numpy.typing import NDArray -from numpy import array, dot, linspace, hstack, empty, maximum, minimum +from numpy import array, linspace, hstack, empty, maximum, minimum from numpy.linalg import norm -from math import isclose, acos +from math import isclose class PhysMember(Member3D): """ @@ -29,7 +28,7 @@ def __init__(self, model: FEModel3D, name: str, i_node: Node3D, j_node: Node3D, tension_only: bool = False, comp_only: bool = False) -> None: super().__init__(model, name, i_node, j_node, material_name, section_name, rotation, tension_only, comp_only) - self.sub_members: Dict[str, Member3D] = {} + self.sub_members: dict[str, Member3D] = {} def __repr__(self) -> str: return f"PhysMember(name={self.name!r}, i_node={self.i_node.name!r}, j_node={self.j_node.name!r})" @@ -43,7 +42,7 @@ def descritize(self) -> None: self.sub_members = {} # Start a new list of nodes along the member - int_nodes: List[Tuple[Node3D, float]] = [] + int_nodes: list[tuple[Node3D, float]] = [] # Create a vector from the i-node to the j-node Xi, Yi, Zi = self.i_node.X, self.i_node.Y, self.i_node.Z @@ -218,7 +217,7 @@ def shear(self, Direction: Literal['Fy', 'Fz'], x: float, combo_name: str = 'Com member, x_mod = self.find_member(x) return member.shear(Direction, x_mod, combo_name) - def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum shear in the member for the given direction. @@ -260,7 +259,7 @@ def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[ return (Vmax, governing_combo) return Vmax - def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum shear in the member for the given direction. @@ -302,7 +301,7 @@ def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[ return (Vmin, governing_combo) return Vmin - def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the shear diagram for the member. @@ -452,7 +451,7 @@ def moment(self, Direction: Literal['My', 'Mz'], x: float, combo_name: str = 'Co member, x_mod = self.find_member(x) return member.moment(Direction, x_mod, combo_name) - def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum moment in the member for the given direction. @@ -494,7 +493,7 @@ def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List return (Mmax, governing_combo) return Mmax - def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum moment in the member for the given direction. @@ -536,7 +535,7 @@ def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List return (Mmin, governing_combo) return Mmin - def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the moment diagram for the member. @@ -691,7 +690,7 @@ def torque(self, x: float, combo_name: str = 'Combo 1') -> float: member, x_mod = self.find_member(x) return member.torque(x_mod, combo_name) - def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_torque(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum torsional moment in the member. @@ -729,7 +728,7 @@ def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[flo return (Tmax, governing_combo) return Tmax - def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_torque(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum torsional moment in the member. @@ -767,7 +766,7 @@ def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[flo return (Tmin, governing_combo) return Tmin - def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_torque(self, combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the torque diagram for the member. @@ -901,7 +900,7 @@ def axial(self, x: float, combo_name: str = 'Combo 1') -> float: member, x_mod = self.find_member(x) return member.axial(x_mod, combo_name) - def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_axial(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum axial force in the member. @@ -939,7 +938,7 @@ def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[floa return (Pmax, governing_combo) return Pmax - def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_axial(self, combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum axial force in the member. @@ -977,7 +976,7 @@ def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[floa return (Pmin, governing_combo) return Pmin - def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_axial(self, combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the axial force diagram for the member. @@ -1115,7 +1114,7 @@ def deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: member, x_mod = self.find_member(x) return member.deflection(Direction, x_mod, combo_name) - def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the maximum deflection in the member. @@ -1155,7 +1154,7 @@ def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union return (dmax, governing_combo) return dmax - def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]: + def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: str | list[str] = 'Combo 1') -> float | tuple[float, str]: """ Returns the minimum deflection in the member. @@ -1215,7 +1214,7 @@ def rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_n member, x_mod = self.find_member(x) return member.rel_deflection(Direction, x_mod, combo_name) - def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20, + def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str | list[str] = 'Combo 1', n_points: int = 20, figsize: tuple[float, float] = (7, 3)) -> None: """ Plots the deflection diagram for the member. @@ -1359,7 +1358,7 @@ def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, # Return the results return d_array2 - def find_member(self, x: float) -> Tuple[Member3D, float]: + def find_member(self, x: float) -> tuple[Member3D, float]: """ Returns the sub-member that the physical member's local point 'x' lies on, and 'x' modified for that sub-member's local coordinate system. """ diff --git a/Pynite/Plate3D.py b/Pynite/Plate3D.py index df7e71b5..f363f6dc 100644 --- a/Pynite/Plate3D.py +++ b/Pynite/Plate3D.py @@ -1,5 +1,5 @@ from __future__ import annotations # Allows more recent type hints features -from typing import List, Tuple, Optional,TYPE_CHECKING +from typing import TYPE_CHECKING from numpy import zeros, array, matmul, cross, add from numpy.linalg import inv, norm, det @@ -11,7 +11,7 @@ from Pynite.Node3D import Node3D #%% -class Plate3D(): +class Plate3D: def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_node: Node3D, t: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0, @@ -46,7 +46,7 @@ def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_ """ self.name: str = name - self.ID: Optional[int] = None + self.ID: int | None = None self.type: str = 'Rect' self.i_node: Node3D = i_node @@ -59,7 +59,7 @@ def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_ self.kx_mod: float = kx_mod self.ky_mod: float = ky_mod - self.pressures: List[Tuple[float, str]] = [] # A list of surface pressures [pressure, case='Case 1'] + self.pressures: list[tuple[float, str]] = [] # A list of surface pressures [pressure, case='Case 1'] # Plates need a link to the model they belong to self.model: FEModel3D = model diff --git a/Pynite/Quad3D.py b/Pynite/Quad3D.py index 7225be8d..8cf26bd2 100644 --- a/Pynite/Quad3D.py +++ b/Pynite/Quad3D.py @@ -13,14 +13,13 @@ import warnings if TYPE_CHECKING: - from typing import List, Tuple, Optional from numpy import float64 from numpy.typing import NDArray from Pynite.FEModel3D import FEModel3D from Pynite.Node3D import Node3D -class Quad3D(): +class Quad3D: """ An isoparametric general quadrilateral element, formulated by superimposing an isoparametric DKMQ bending element with an isoparametric plane stress element. Drilling stability is provided by adding a weak rotational spring stiffness at each node. Isotropic behavior is the default, but orthotropic in-plane behavior can be modeled by specifying stiffness modification factors for the element's local x and y axes. @@ -32,7 +31,7 @@ def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_ ky_mod: float = 1.0): self.name: str = name - self.ID: Optional[int] = None + self.ID: int | None = None self.type: str = 'Quad' self.i_node: Node3D = i_node @@ -44,7 +43,7 @@ def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_ self.kx_mod: float = kx_mod self.ky_mod: float = ky_mod - self.pressures: List[Tuple[float, str]] = [] # A list of surface pressures [pressure, case='Case 1'] + self.pressures: list[tuple[float, str]] = [] # A list of surface pressures [pressure, case='Case 1'] # Quads need a link to the model they belong to self.model: FEModel3D = model @@ -154,7 +153,7 @@ def L_k(self, k: Literal[5, 6, 7, 8]) -> float: else: raise Exception('Invalid value for k. k must be 5, 6, 7, or 8.') - def dir_cos(self, k: Literal[5, 6, 7, 8]) -> Tuple[float, float]: + def dir_cos(self, k: Literal[5, 6, 7, 8]) -> tuple[float, float]: L_k = self.L_k(k) diff --git a/Pynite/Rendering.py b/Pynite/Rendering.py index 151631c6..737477de 100644 --- a/Pynite/Rendering.py +++ b/Pynite/Rendering.py @@ -6,11 +6,10 @@ """ from __future__ import annotations # Allows more recent type hints features -from json import load import warnings -from typing import TYPE_CHECKING, Callable, List, Any, Optional, Union, Tuple +from typing import TYPE_CHECKING +from collections.abc import Callable -from IPython.display import Image import numpy as np import pyvista as pv import math @@ -21,7 +20,6 @@ # For type checking only - these imports are only used during type checking if TYPE_CHECKING: - from typing import List, Union, Tuple, Optional from Pynite.Node3D import Node3D from Pynite.Member3D import Member3D from Pynite.Spring3D import Spring3D @@ -42,7 +40,7 @@ class Renderer: plates, quads) with deformed shapes, loads, contours, and diagrams. """ - scalar: Optional[str] = None + scalar: str | None = None def __init__(self, model: FEModel3D) -> None: """Initialize the renderer with a finite element model. @@ -52,20 +50,20 @@ def __init__(self, model: FEModel3D) -> None: self.model: FEModel3D = model # Default settings for rendering - self._annotation_size: Optional[float] = None # None means auto-calculate + self._annotation_size: float | None = None # None means auto-calculate self._annotation_size_manual: bool = False # Track if user manually set the size - self._annotation_size_cached: Optional[float] = None # Cache to avoid recalculating 2600+ times per render + self._annotation_size_cached: float | None = None # Cache to avoid recalculating 2600+ times per render self._deformed_shape: bool = False self._deformed_scale: float = 30.0 self._render_nodes: bool = True self._render_loads: bool = True - self._color_map: Optional[str] = None - self._combo_name: Optional[str] = 'Combo 1' - self._case: Optional[str] = None + self._color_map: str | None = None + self._combo_name: str | None = 'Combo 1' + self._case: str | None = None self._labels: bool = True self._scalar_bar: bool = False self._scalar_bar_text_size: int = 24 - self._member_diagrams: Optional[str] = None # Options: None, 'Fy', 'Fz', 'My', 'Mz', 'Fx', 'Tx' + self._member_diagrams: str | None = None # Options: None, 'Fy', 'Fz', 'My', 'Mz', 'Fx', 'Tx' self._diagram_scale: float = 30.0 self._member_csys: bool = False self._member_csys_scale: float = 30.0 @@ -75,7 +73,7 @@ def __init__(self, model: FEModel3D) -> None: # This is added because `self.update()` clears the plotter, removing user self.plotter configurations. # Functions in this list run after Pynite adds actors, allowing further PyVista customizations # (e.g., grid, axes) before render. Each func in this list must accept a `pyvista.Plotter` argument. - self.post_update_callbacks: List[Callable[[pv.Plotter], None]] = [] + self.post_update_callbacks: list[Callable[[pv.Plotter], None]] = [] # Create plotter with off_screen mode if pyvista is set globally to OFF_SCREEN # This is important for headless CI/testing environments @@ -100,12 +98,12 @@ def __init__(self, model: FEModel3D) -> None: self.plotter.iren.add_observer('ExitEvent', lambda obj, event: obj.TerminateApp()) # Initialize load labels - self._load_label_points: List[List[float]] = [] - self._load_labels: List[Union[str, float, int]] = [] + self._load_label_points: list[list[float]] = [] + self._load_labels: list[str | float | int] = [] # Initialize spring labels - self._spring_label_points: List[List[float]] = [] - self._spring_labels: List[str] = [] + self._spring_label_points: list[list[float]] = [] + self._spring_labels: list[str] = [] def __repr__(self) -> str: return f"Renderer(model={self.model!r})" @@ -180,28 +178,28 @@ def render_loads(self, render_loads: bool) -> None: self._render_loads = render_loads @property - def color_map(self) -> Optional[str]: + def color_map(self) -> str | None: return self._color_map @color_map.setter - def color_map(self, color_map: Optional[str]) -> None: + def color_map(self, color_map: str | None) -> None: self._color_map = color_map @property - def combo_name(self) -> Optional[str]: + def combo_name(self) -> str | None: return self._combo_name @combo_name.setter - def combo_name(self, combo_name: Optional[str]) -> None: + def combo_name(self, combo_name: str | None) -> None: self._combo_name = combo_name self._case = None @property - def case(self) -> Optional[str]: + def case(self) -> str | None: return self._case @case.setter - def case(self, case: Optional[str]) -> None: + def case(self, case: str | None) -> None: self._case = case self._combo_name = None @@ -230,7 +228,7 @@ def scalar_bar_text_size(self, text_size: int) -> None: self._scalar_bar_text_size = text_size @property - def member_diagrams(self) -> Optional[str]: + def member_diagrams(self) -> str | None: """Member diagram type to display. Options: ``None``, ``'Fy'``, ``'Fz'``, ``'My'``, ``'Mz'``, ``'Fx'``, ``'Tx'``. @@ -238,7 +236,7 @@ def member_diagrams(self) -> Optional[str]: return self._member_diagrams @member_diagrams.setter - def member_diagrams(self, diagram_type: Optional[str]) -> None: + def member_diagrams(self, diagram_type: str | None) -> None: valid_options = [None, 'Fy', 'Fz', 'My', 'Mz', 'Fx', 'Tx'] if diagram_type not in valid_options: raise ValueError(f"member_diagrams must be one of {valid_options}, got '{diagram_type}'") @@ -461,9 +459,9 @@ def update(self, reset_camera: bool = True, off_screen: bool = False) -> None: # Build visual helper objects. These classes encapsulate geometry and label bookkeeping # so that the renderer logic stays readable while still leveraging PyVista efficiently. - vis_nodes: List[VisNode] = [] - vis_springs: List[VisSpring] = [] - vis_members: List[VisMember] = [] + vis_nodes: list[VisNode] = [] + vis_springs: list[VisSpring] = [] + vis_members: list[VisMember] = [] if self.render_nodes: node_color = 'black' if self.theme == 'print' else 'grey' @@ -836,7 +834,7 @@ def plot_spring(self, spring: Spring3D, color: str = 'grey', deformed: bool = Fa self._spring_label_points.append([(Xi + Xj) / 2, (Yi + Yj) / 2, (Zi + Zj) / 2]) - def plot_plates(self, deformed_shape: bool, deformed_scale: float, color_map: Optional[str], combo_name: Optional[str]) -> None: + def plot_plates(self, deformed_shape: bool, deformed_scale: float, color_map: str | None, combo_name: str | None) -> None: """Add plate/quad elements to the plotter with optional contours. :param bool deformed_shape: Plot deformed geometry if ``True``. @@ -1009,8 +1007,8 @@ def plot_deformed_member(self, member: Member3D, scale_factor: float) -> None: line = pv.Line(D_plot[i], D_plot[i+1]) self.plotter.add_mesh(line, color='red', line_width=2) - def plot_pt_load(self, position: Tuple[float, float, float], direction: Union[Tuple[float, float, float], np.ndarray], - length: float, label_text: Optional[Union[str, float, int]] = None, color: str = 'green') -> None: + def plot_pt_load(self, position: tuple[float, float, float], direction: tuple[float, float, float] | np.ndarray, + length: float, label_text: str | float | int | None = None, color: str = 'green') -> None: """Add a point-load arrow to the plotter. :param tuple position: Arrow tip coordinates ``(X, Y, Z)``. @@ -1055,9 +1053,9 @@ def plot_pt_load(self, position: Tuple[float, float, float], direction: Union[Tu # Plot the shaft self.plotter.add_mesh(shaft, line_width=2, color=color) - def plot_dist_load(self, position1: Tuple[float, float, float], position2: Tuple[float, float, float], - direction: Union[np.ndarray, Tuple[float, float, float]], length1: float, length2: float, - label_text1: Optional[Union[str, float, int]], label_text2: Optional[Union[str, float, int]], + def plot_dist_load(self, position1: tuple[float, float, float], position2: tuple[float, float, float], + direction: np.ndarray | tuple[float, float, float], length1: float, length2: float, + label_text1: str | float | int | None, label_text2: str | float | int | None, color: str = 'green') -> None: """Add a linearly varying distributed load to the plotter. @@ -1119,8 +1117,8 @@ def plot_dist_load(self, position1: Tuple[float, float, float], position2: Tuple # Combine all geometry into a single PolyData object self.plotter.add_mesh(tail_line, color=color) - def plot_moment(self, center: Tuple[float, float, float], direction: Union[Tuple[float, float, float], np.ndarray], - radius: float, label_text: Optional[Union[str, float, int]] = None, color: str = 'green') -> None: + def plot_moment(self, center: tuple[float, float, float], direction: tuple[float, float, float] | np.ndarray, + radius: float, label_text: str | float | int | None = None, color: str = 'green') -> None: """Add a concentrated moment to the plotter. :param tuple center: Center point of the moment arc. @@ -1779,7 +1777,7 @@ class VisNode: Encapsulates construction of node and support-condition glyphs for the plotter. """ - def __init__(self, node: 'Node3D', annotation_size: float, color: str) -> None: + def __init__(self, node: Node3D, annotation_size: float, color: str) -> None: """Build visual elements for a node. :param Node3D node: Node to visualize. @@ -1791,7 +1789,7 @@ def __init__(self, node: 'Node3D', annotation_size: float, color: str) -> None: self.color = color self.label = node.name self.label_point = [node.X, node.Y, node.Z] - self.meshes: List[pv.PolyData] = [] + self.meshes: list[pv.PolyData] = [] self._build_geometry() def _build_geometry(self) -> None: @@ -1859,7 +1857,7 @@ def add_to_plotter(self, plotter: pv.Plotter) -> None: class VisSpring: """Visual wrapper for a Spring3D with optional deformed rendering.""" - def __init__(self, spring: 'Spring3D', annotation_size: float, color: str, deformed: bool, scale: float, combo_name: Optional[str]) -> None: + def __init__(self, spring: Spring3D, annotation_size: float, color: str, deformed: bool, scale: float, combo_name: str | None) -> None: """Build visual elements for a spring. :param Spring3D spring: Spring to visualize. @@ -1875,9 +1873,9 @@ def __init__(self, spring: 'Spring3D', annotation_size: float, color: str, defor self.deformed = deformed self.scale = scale self.combo_name = combo_name - self.mesh: Optional[pv.PolyData] = None + self.mesh: pv.PolyData | None = None self.label = spring.name - self.label_point: Optional[List[float]] = None + self.label_point: list[float] | None = None self._build_geometry() def _build_geometry(self) -> None: @@ -1949,7 +1947,7 @@ def add_to_plotter(self, plotter: pv.Plotter) -> None: class VisMember: """Visual wrapper for a Member3D as a simple line.""" - def __init__(self, member: 'Member3D', theme: str, annotation_size: float) -> None: + def __init__(self, member: Member3D, theme: str, annotation_size: float) -> None: """Build visual elements for a member. :param Member3D member: Member to visualize. @@ -1960,7 +1958,7 @@ def __init__(self, member: 'Member3D', theme: str, annotation_size: float) -> No self.theme = theme self.annotation_size = annotation_size self.mesh: pv.PolyData = self._build_geometry() - self.release_meshes: List[pv.PolyData] = self._build_release_meshes() + self.release_meshes: list[pv.PolyData] = self._build_release_meshes() self.label = member.name self.label_point = [(member.i_node.X + member.j_node.X) / 2, (member.i_node.Y + member.j_node.Y) / 2, @@ -1972,7 +1970,7 @@ def _build_geometry(self) -> pv.PolyData: line.points[1] = [self.member.j_node.X, self.member.j_node.Y, self.member.j_node.Z] return line - def _build_release_meshes(self) -> List[pv.PolyData]: + def _build_release_meshes(self) -> list[pv.PolyData]: releases = self.member.Releases if not releases: return [] @@ -2004,7 +2002,7 @@ def _build_release_meshes(self) -> List[pv.PolyData]: i_point = np.array([self.member.i_node.X, self.member.i_node.Y, self.member.i_node.Z]) j_point = np.array([self.member.j_node.X, self.member.j_node.Y, self.member.j_node.Z]) - release_meshes: List[pv.PolyData] = [] + release_meshes: list[pv.PolyData] = [] def add_circle(center: np.ndarray, axis1: np.ndarray, axis2: np.ndarray) -> None: angles = np.linspace(0.0, 2 * np.pi, num_segments, endpoint=False) @@ -2043,7 +2041,7 @@ def add_to_plotter(self, plotter: pv.Plotter) -> None: class VisDeformedMember: """Visual wrapper for a deformed Member3D polyline.""" - def __init__(self, member: 'Member3D', scale_factor: float, combo_name: Optional[str]) -> None: + def __init__(self, member: Member3D, scale_factor: float, combo_name: str | None) -> None: """Build a deformed member polyline. :param Member3D member: Member to visualize in deformed state. @@ -2053,7 +2051,7 @@ def __init__(self, member: 'Member3D', scale_factor: float, combo_name: Optional self.member = member self.scale_factor = scale_factor self.combo_name = combo_name - self.mesh: Optional[pv.PolyData] = None + self.mesh: pv.PolyData | None = None self._build_geometry() def _build_geometry(self) -> None: diff --git a/Pynite/Section.py b/Pynite/Section.py index b0cfa9a0..f315fd76 100644 --- a/Pynite/Section.py +++ b/Pynite/Section.py @@ -8,13 +8,13 @@ from numpy.typing import NDArray from Pynite.FEModel3D import FEModel3D -class Section(): +class Section: """ 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 """ - def __init__(self, model: 'FEModel3D', name: str, A: float, Iy: float, Iz: float, J: float) -> None: + def __init__(self, model: FEModel3D, 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 @@ -29,7 +29,7 @@ def __init__(self, model: 'FEModel3D', name: str, A: float, Iy: float, Iz: float :param J: The torsion constant of the section :type J: float """ - self.model: 'FEModel3D' = model + self.model: FEModel3D = model self.name: str = name self.A: float = A self.Iy: float = Iy @@ -84,7 +84,7 @@ def G(self, fx: float, my: float, mz: float) -> NDArray: class SteelSection(Section): - def __init__(self, model: 'FEModel3D', name: str, A: float, Iy: float, Iz: float, J: float, + def __init__(self, model: FEModel3D, name: str, A: float, Iy: float, Iz: float, J: float, Zy: float, Zz: float, material_name: str) -> None: """ Initialize a steel section diff --git a/Pynite/ShearWall.py b/Pynite/ShearWall.py index 159921b9..d6e4333c 100644 --- a/Pynite/ShearWall.py +++ b/Pynite/ShearWall.py @@ -1,7 +1,5 @@ from __future__ import annotations # Allows more recent type hints features from math import isclose -from numpy import average -import io import os from typing import TYPE_CHECKING, Literal @@ -11,12 +9,11 @@ from matplotlib.patches import Rectangle if TYPE_CHECKING: - from typing import List, Dict, Tuple from Pynite.Quad3D import Quad3D import matplotlib.figure -class ShearWall(): +class ShearWall: """Creates a new shear wall model that allows for modeling of complex shear walls. You can add openings and flanges (wall returns or intersections). Diaphragm levels can be defined in order to apply shear forces along the length of the wall. Diaphragms can be full or partial length. Supports can be applied at any level in the shear wall. Supports can also be full or partial length. A `ky_mod` factor is built in to account for cracking. Shear walls can automatically detect shear wall piers and coupling beams, and sum internal forces in those components. """ @@ -30,15 +27,15 @@ def __init__(self, model, name, mesh_size, length, height, thickness, material_n self.ky_mod = ky_mod self.origin = origin self.plane = plane - self._openings: List[List[str | float | None]] = [] - self._flanges: List[List[str | float]] = [] - self._supports: List[List[float]] = [] - self._stories: List[List[str | float]] = [] - self._shears: List[List[str | float]] = [] - self._axials: List[List[str | float]] = [] - self._materials: List[List[str | float]] = [] - self.piers: Dict[str, Pier] = {} - self.coupling_beams: Dict[str, CouplingBeam] = {} + self._openings: list[list[str | float | None]] = [] + self._flanges: list[list[str | float]] = [] + self._supports: list[list[float]] = [] + self._stories: list[list[str | float]] = [] + self._shears: list[list[str | float]] = [] + self._axials: list[list[str | float]] = [] + self._materials: list[list[str | float]] = [] + self.piers: dict[str, Pier] = {} + self.coupling_beams: dict[str, CouplingBeam] = {} self.is_generated: bool = False self.needs_update: bool = False self.asign_material(material_name, thickness) @@ -139,8 +136,8 @@ def generate(self) -> None: self._remove_from_model() # Identify mesh control points - x_control: List[float] = [0, self.L] - y_control: List[float] = [0, self.H] + x_control: list[float] = [0, self.L] + y_control: list[float] = [0, self.H] for material in self._materials: x_control.append(material[2]) @@ -148,7 +145,7 @@ def generate(self) -> None: y_control.append(material[4]) y_control.append(material[5]) - z_control: List[float] = [0] + z_control: list[float] = [0] for flg in self._flanges: if flg[6] == '+z': z_control.append(flg[1]) else: z_control.append(-flg[1]) @@ -347,8 +344,8 @@ def _identify_piers(self) -> None: self.piers = {} # Create a list of x and y coordinates that represent the edges of the wall - x_vals: List[float] = [0, self.L] - y_vals: List[float] = [0, self.H] + x_vals: list[float] = [0, self.L] + y_vals: list[float] = [0, self.H] # Add the edges of the openings to the lists for opng in self._openings: @@ -362,7 +359,7 @@ def _identify_piers(self) -> None: y_vals = sorted(y_vals) # Remove duplicate (or near duplicate) values - unique_list: List[float] = [] + unique_list: list[float] = [] for i in range(len(x_vals) - 1): # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value if not isclose(x_vals[i], x_vals[i+1]): @@ -388,7 +385,7 @@ def _identify_piers(self) -> None: self.piers['P' + str(i+1)] = Pier('P' + str(i+1), x, y, width, height, self) # Divide the strip piers further into rectanglular piers using the top and bottom of each opening as pier boundaries - new_piers: Dict[str, Pier] = {} + new_piers: dict[str, Pier] = {} pier_count = 1 for pier in self.piers.values(): for i in range(len(y_vals) - 1): @@ -401,7 +398,7 @@ def _identify_piers(self) -> None: self.piers = new_piers # Delete any piers that fall within an opening - delete_list: List[str] = [] + delete_list: list[str] = [] for pier in self.piers.values(): # Check if this pier is inside any of the openings @@ -478,7 +475,7 @@ def _identify_piers(self) -> None: break # Generate a list of new keys in ascending order - new_keys: List[str] = [f'P{i+1}' for i in range(len(self.piers))] + new_keys: list[str] = [f'P{i+1}' for i in range(len(self.piers))] # Replace the old dicionary with one that has updated keys self.piers = dict(zip(new_keys, self.piers.values())) @@ -510,8 +507,8 @@ def _identify_coupling_beams(self) -> None: self.coupling_beams = {} # Create a list of x and y coordinates that represent the edges of the wall - x_vals: List[float] = [0, self.L] - y_vals: List[float] = [0, self.H] + x_vals: list[float] = [0, self.L] + y_vals: list[float] = [0, self.H] # Add the edges of the openings to the lists for opng in self._openings: @@ -525,7 +522,7 @@ def _identify_coupling_beams(self) -> None: y_vals = sorted(y_vals) # Remove duplicate (or near duplicate) values - unique_list: List[float] = [] + unique_list: list[float] = [] for i in range(len(x_vals) - 1): # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value if not isclose(x_vals[i], x_vals[i+1]): @@ -551,7 +548,7 @@ def _identify_coupling_beams(self) -> None: self.coupling_beams['B' + str(i+1)] = CouplingBeam('B' + str(i+1), x, y, length, height, self) # Divide the strips further into rectanglular beams using the left and right of each opening as beam boundaries - new_beams: Dict[str, CouplingBeam] = {} + new_beams: dict[str, CouplingBeam] = {} beam_count = 1 for beam in self.coupling_beams.values(): for i in range(len(x_vals) - 1): @@ -564,7 +561,7 @@ def _identify_coupling_beams(self) -> None: self.coupling_beams = new_beams # Delete any beams that fall within an opening - delete_list: List[str] = [] + delete_list: list[str] = [] for beam in self.coupling_beams.values(): # Check if this beam is inside any of the openings @@ -651,7 +648,7 @@ def _identify_coupling_beams(self) -> None: del self.coupling_beams[beam] # Generate a list of new keys in ascending order - new_keys: List[str] = [f'B{i + 1}' for i in range(len(self.coupling_beams))] + new_keys: list[str] = [f'B{i + 1}' for i in range(len(self.coupling_beams))] # Replace the old dicionary with one that has updated keys self.coupling_beams = dict(zip(new_keys, self.coupling_beams.values())) @@ -878,7 +875,7 @@ def print_coupling_beams(self, combo_name: str = 'Combo 1') -> None: print('+----------------------------+') print(table) - def _local2global(self, x: float, y: float, z: float, plane: Literal['XY', 'XZ', 'YZ'] = 'XY') -> List[float]: + def _local2global(self, x: float, y: float, z: float, plane: Literal['XY', 'XZ', 'YZ'] = 'XY') -> list[float]: Xo, Yo, Zo = self.origin[0], self.origin[1], self.origin[2] @@ -898,7 +895,7 @@ def _local2global(self, x: float, y: float, z: float, plane: Literal['XY', 'XZ', return [X, Y, Z] -def _global2local(X: float, Y: float, Z: float, origin: List[float] = [0, 0, 0], plane: Literal['XY', 'YZ', 'XZ'] = 'XY') -> List[float]: +def _global2local(X: float, Y: float, Z: float, origin: list[float] = [0, 0, 0], plane: Literal['XY', 'YZ', 'XZ'] = 'XY') -> list[float]: Xo, Yo, Zo = origin[0], origin[1], origin[2] @@ -921,7 +918,7 @@ def _global2local(X: float, Y: float, Z: float, origin: List[float] = [0, 0, 0], # %% -class Pier(): +class Pier: def __init__(self, name: str, x: float, y: float, width: float, height: float, shear_wall: ShearWall) -> None: self.name: str = name @@ -936,12 +933,12 @@ def __init__(self, name: str, x: float, y: float, width: float, height: float, s self.origin = shear_wall.origin # This list will be used by the parent shear wall to store a list of only the plates in this pier - self.plates: List[Quad3D] = [] + self.plates: list[Quad3D] = [] def __repr__(self) -> str: return f"Pier(name={self.name!r}, width={self.width}, height={self.height})" - def sum_forces(self, combo_name: str = 'Combo 1') -> Tuple[float, float, float, float]: + def sum_forces(self, combo_name: str = 'Combo 1') -> tuple[float, float, float, float]: # Initialize the forces in the plate P, M, V = 0, 0, 0 @@ -985,7 +982,7 @@ def sum_forces(self, combo_name: str = 'Combo 1') -> Tuple[float, float, float, # %% -class CouplingBeam(): +class CouplingBeam: def __init__(self, name: str, x: float, y: float, length: float, height: float, shear_wall: ShearWall) -> None: self.name: str = name @@ -999,12 +996,12 @@ def __init__(self, name: str, x: float, y: float, length: float, height: float, self.plane = shear_wall.plane self.origin = shear_wall.origin - self.plates: List[Quad3D] = [] + self.plates: list[Quad3D] = [] def __repr__(self) -> str: return f"CouplingBeam(name={self.name!r}, length={self.length}, height={self.height})" - def sum_forces(self, combo_name: str = 'Combo 1') -> Tuple[float, float, float, float]: + def sum_forces(self, combo_name: str = 'Combo 1') -> tuple[float, float, float, float]: # Initialize plate forces to zero P, M, V = 0, 0, 0 diff --git a/Pynite/Spring3D.py b/Pynite/Spring3D.py index df29dc82..a8d71c59 100644 --- a/Pynite/Spring3D.py +++ b/Pynite/Spring3D.py @@ -1,20 +1,18 @@ # %% from __future__ import annotations # Allows more recent type hints features -from numpy import zeros, array, add, subtract, matmul, insert, cross, divide +from numpy import zeros, array, matmul, cross, divide from numpy.linalg import inv from math import isclose -import Pynite.FixedEndReactions from Pynite.LoadCombo import LoadCombo from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Dict, Optional from numpy import float64 from numpy.typing import NDArray from Pynite.Node3D import Node3D # %% -class Spring3D(): +class Spring3D: """A class representing a 3D spring element in a finite element model. """ @@ -23,24 +21,24 @@ class Spring3D(): #%% def __init__(self, name: str, i_node: Node3D, j_node: Node3D, ks: float, - LoadCombos: Dict[str, LoadCombo] = {'Combo 1': LoadCombo('Combo 1', factors={'Case 1': 1.0})}, + LoadCombos: dict[str, LoadCombo] = {'Combo 1': LoadCombo('Combo 1', factors={'Case 1': 1.0})}, tension_only: bool = False, comp_only: bool = False) -> None: ''' Initializes a new spring. ''' self.name: str = name # A unique name for the spring given by the user - self.ID: Optional[int] = None # Unique index number for the spring assigned by the program + self.ID: int | None = None # Unique index number for the spring assigned by the program self.i_node: Node3D = i_node # The spring's i-node self.j_node: Node3D = j_node # The spring's j-node self.ks: float = ks # The spring constant (force/displacement) - self.load_combos: Dict[str, LoadCombo] = LoadCombos # The dictionary of load combinations in the model this spring belongs to + self.load_combos: dict[str, LoadCombo] = LoadCombos # The dictionary of load combinations in the model this spring belongs to self.tension_only: bool = tension_only # Indicates whether the spring is tension-only self.comp_only: bool = comp_only # Indicates whether the spring is compression-only # Springs need to track whether they are active or not for any given load combination. # They may become inactive for a load combination during a tension/compression-only # analysis. This dictionary will be used when the model is solved. - self.active: Dict[str, bool] = {} # Key = load combo name, Value = True or False + self.active: dict[str, bool] = {} # Key = load combo name, Value = True or False #%% def __repr__(self) -> str: diff --git a/Pynite/Tri3D.py b/Pynite/Tri3D.py index e15d54d9..28534b80 100644 --- a/Pynite/Tri3D.py +++ b/Pynite/Tri3D.py @@ -1,5 +1,5 @@ from __future__ import annotations # Allows more recent type hints features -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING from numpy import zeros, array, matmul, cross, add, float64 from numpy.linalg import inv, norm, det @@ -10,7 +10,7 @@ from Pynite.FEModel3D import FEModel3D #%% -class Tri3D(): +class Tri3D: def __init__(self, name: str, i_node: Node3D, j_node: Node3D, k_node: Node3D, t: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0, diff --git a/Pynite/VTKWriter.py b/Pynite/VTKWriter.py index 83f3f135..245219a9 100644 --- a/Pynite/VTKWriter.py +++ b/Pynite/VTKWriter.py @@ -16,7 +16,6 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Dict, Tuple, List from Pynite.FEModel3D import FEModel3D, Member3D class VTKWriter: @@ -80,7 +79,7 @@ def _write_node_data(self, path:str): node_names.SetName("Name") node_cells = vtk.vtkCellArray() - node_ids: Dict[str, int] = {} + node_ids: dict[str, int] = {} for node in self.model.nodes.values(): point_id = points.InsertNextPoint(node.X, node.Y, node.Z) node_ids[node.name] = point_id @@ -130,8 +129,8 @@ def _write_node_data(self, path:str): moments.InsertTuple3(node_id, node.RxnMX[combo], node.RxnMY[combo], node.RxnMZ[combo]) # calculate the NodeLoad for each node - fl: Dict[str,float] = {"X":0,"Y":0,"Z":0} - ml: Dict[str,float] = {"X":0,"Y":0,"Z":0} + fl: dict[str,float] = {"X":0,"Y":0,"Z":0} + ml: dict[str,float] = {"X":0,"Y":0,"Z":0} for (f_or_m, direction), magnitude, case in node.NodeLoads: if f_or_m == "F": fl[direction] += magnitude @@ -170,7 +169,7 @@ def _write_member_data(self, path: str): #### CREATE LINE CELLS #### # each (sub)member is further subdivided into line segments lines = vtk.vtkCellArray() - submembers: List[Tuple[Tuple[float, float], Member3D, vtk.vtkLine]] = [] + submembers: list[tuple[tuple[float, float], Member3D, vtk.vtkLine]] = [] for member in self.model.members.values(): if len(member.sub_members) == 0: # The model has not been analyzed yet. Only add straight lines between the nodes @@ -363,7 +362,7 @@ def _write_quad_data(self, path: str): quads = vtk.vtkCellArray() # this holds all cell data. A cell is in this case the vtkBiQuadraticQuads # keeps track of all vtk subquads for every Pynite Quad by name - quad_references: Dict[str, List[vtk.vtkBiQuadraticQuad]] = {} + quad_references: dict[str, list[vtk.vtkBiQuadraticQuad]] = {} # this loops through every Pynite quad and creates 4 vtkBiQuadraticQuad elements and their point data for quad in self.model.quads.values(): diff --git a/Pynite/Visualization.py b/Pynite/Visualization.py index 22d68fcb..eaf84629 100644 --- a/Pynite/Visualization.py +++ b/Pynite/Visualization.py @@ -14,7 +14,7 @@ from numpy.linalg import norm import vtk -class Renderer(): +class Renderer: """Renderer for visualizing finite element models using VTK. This class provides a flexible interface for rendering 3D finite element @@ -624,7 +624,7 @@ def update(self, reset_camera=True): # Converts a node object into a node for the viewer -class VisNode(): +class VisNode: """VTK representation of a node and its supports.""" # Constructor @@ -902,7 +902,7 @@ def __init__(self, node, annotation_size=5): self.actor.SetMapper(mapper) -class VisSpring(): +class VisSpring: """VTK representation of a spring element.""" def __init__(self, spring, nodes, annotation_size=5, color=None): @@ -967,7 +967,7 @@ def __init__(self, spring, nodes, annotation_size=5, color=None): self.lblActor.GetProperty().SetColor(0, 0, 0) # Converts a member object into a member for the viewer -class VisMember(): +class VisMember: """VTK representation of a frame/beam member.""" # Constructor @@ -1121,7 +1121,7 @@ def create_circle_actor(center, axis1, axis2): self.release_actors.append(create_circle_actor(center, local_x, local_y)) # Converts a node object into a node in its deformed position for the viewer -class VisDeformedNode(): +class VisDeformedNode: """Sphere representing a node in its deformed position.""" def __init__(self, node, scale_factor, annotation_size=5, combo_name='Combo 1'): @@ -1144,7 +1144,7 @@ def __init__(self, node, scale_factor, annotation_size=5, combo_name='Combo 1'): self.source.SetRadius(0.6*annotation_size) self.source.Update() -class VisDeformedMember(): +class VisDeformedMember: """Polyline representing a member's deformed shape.""" def __init__(self, member, nodes, scale_factor, combo_name='Combo 1'): @@ -1231,7 +1231,7 @@ def __init__(self, member, nodes, scale_factor, combo_name='Combo 1'): self.source.SetPoints(points) self.source.SetLines(lines) -class VisDeformedSpring(): +class VisDeformedSpring: """Line representation of a spring in its deformed position.""" def __init__(self, spring, nodes, scale_factor, combo_name='Combo 1'): @@ -1269,7 +1269,7 @@ def __init__(self, spring, nodes, scale_factor, combo_name='Combo 1'): self.source.Update() -class VisPtLoad(): +class VisPtLoad: """Arrow representation of a concentrated load.""" def __init__(self, position, direction, length, label_text: str | None = None, annotation_size=5, theme: str = 'default'): @@ -1358,7 +1358,7 @@ def __init__(self, position, direction, length, label_text: str | None = None, a self.lblActor.GetProperty().SetColor(0, 0.75, 0) # Black -class VisDistLoad(): +class VisDistLoad: """Series of arrows representing a distributed load.""" def __init__(self, position1, position2, direction, length1, length2, label_text1, label_text2, annotation_size=5, theme = 'default'): @@ -1443,7 +1443,7 @@ def __init__(self, position1, position2, direction, length1, length2, label_text # Get the actors for the labels self.lblActors = [ptLoads[0].lblActor, ptLoads[len(ptLoads) - 1].lblActor] -class VisMoment(): +class VisMoment: """Arc-and-arrow representation of a concentrated moment.""" def __init__(self, center, direction, radius, label_text=None, annotation_size=5, theme='default'): @@ -1508,7 +1508,7 @@ def __init__(self, center, direction, radius, label_text=None, annotation_size=5 elif theme == 'print': self.lblActor.GetProperty().SetColor(0, 0.75, 0) # Dark Green -class VisAreaLoad(): +class VisAreaLoad: """Polygon and arrows used to visualize a uniform area load.""" def __init__(self, position0, position1, position2, position3, direction, length, label_text, annotation_size=5, theme='default'): @@ -1794,17 +1794,17 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa # Display the load if load[0] == 'FX': - ptLoad = VisPtLoad((node.X - 0.6*annotation_size*sign, node.Y, node.Z), [1, 0, 0], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad((node.X - 0.6*annotation_size*sign, node.Y, node.Z), [1, 0, 0], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'FY': - ptLoad = VisPtLoad((node.X, node.Y - 0.6*annotation_size*sign, node.Z), [0, 1, 0], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad((node.X, node.Y - 0.6*annotation_size*sign, node.Z), [0, 1, 0], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'FZ': - ptLoad = VisPtLoad((node.X, node.Y, node.Z - 0.6*annotation_size*sign), [0, 0, 1], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad((node.X, node.Y, node.Z - 0.6*annotation_size*sign), [0, 0, 1], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MX': - ptLoad = VisMoment((node.X, node.Y, node.Z), (1*sign, 0, 0), abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment((node.X, node.Y, node.Z), (1*sign, 0, 0), abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MY': - ptLoad = VisMoment((node.X, node.Y, node.Z), (0, 1*sign, 0), abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment((node.X, node.Y, node.Z), (0, 1*sign, 0), abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MZ': - ptLoad = VisMoment((node.X, node.Y, node.Z), (0, 0, 1*sign), abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment((node.X, node.Y, node.Z), (0, 0, 1*sign), abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) polydata.AddInputData(ptLoad.polydata.GetOutput()) renderer.AddActor(ptLoad.lblActor) @@ -1836,29 +1836,29 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa # Display the load if load[0] == 'Fx': - ptLoad = VisPtLoad(position, dir_cos[0, :], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, dir_cos[0, :], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'Fy': - ptLoad = VisPtLoad(position, dir_cos[1, :], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, dir_cos[1, :], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'Fz': - ptLoad = VisPtLoad(position, dir_cos[2, :], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, dir_cos[2, :], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'Mx': - ptLoad = VisMoment(position, dir_cos[0, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, dir_cos[0, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'My': - ptLoad = VisMoment(position, dir_cos[1, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, dir_cos[1, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'Mz': - ptLoad = VisMoment(position, dir_cos[2, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, dir_cos[2, :]*sign, abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'FX': - ptLoad = VisPtLoad(position, [1, 0, 0], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, [1, 0, 0], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'FY': - ptLoad = VisPtLoad(position, [0, 1, 0], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, [0, 1, 0], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'FZ': - ptLoad = VisPtLoad(position, [0, 0, 1], load_value/max_pt_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisPtLoad(position, [0, 0, 1], load_value/max_pt_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MX': - ptLoad = VisMoment(position, [1*sign, 0, 0], abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, [1*sign, 0, 0], abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MY': - ptLoad = VisMoment(position, [0, 1*sign, 0], abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, [0, 1*sign, 0], abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) elif load[0] == 'MZ': - ptLoad = VisMoment(position, [0, 0, 1*sign], abs(load_value)/max_moment*2.5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + ptLoad = VisMoment(position, [0, 0, 1*sign], abs(load_value)/max_moment*2.5*annotation_size, f'{load_value:.3g}', annotation_size, theme) polydata.AddInputData(ptLoad.polydata.GetOutput()) renderer.AddActor(ptLoad.lblActor) @@ -1883,17 +1883,17 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa # Display the load if load[0] == 'Fx': - distLoad = VisDistLoad(position1, position2, dir_cos[0, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, dir_cos[0, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) elif load[0] == 'Fy': - distLoad = VisDistLoad(position1, position2, dir_cos[1, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, dir_cos[1, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) elif load[0] == 'Fz': - distLoad = VisDistLoad(position1, position2, dir_cos[2, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, dir_cos[2, :], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) elif load[0] == 'FX': - distLoad = VisDistLoad(position1, position2, [1, 0, 0], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, [1, 0, 0], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) elif load[0] == 'FY': - distLoad = VisDistLoad(position1, position2, [0, 1, 0], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, [0, 1, 0], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) elif load[0] == 'FZ': - distLoad = VisDistLoad(position1, position2, [0, 0, 1], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, '{:.3g}'.format(w1), '{:.3g}'.format(w2), annotation_size) + distLoad = VisDistLoad(position1, position2, [0, 0, 1], w1/max_dist_load*5*annotation_size, w2/max_dist_load*5*annotation_size, f'{w1:.3g}', f'{w2:.3g}', annotation_size) polydata.AddInputData(distLoad.polydata.GetOutput()) renderer.AddActor(distLoad.lblActors[0]) @@ -1932,7 +1932,7 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa position3 = [plate.n_node.X, plate.n_node.Y, plate.n_node.Z] # Create an area load and get its data - area_load = VisAreaLoad(position0, position1, position2, position3, dir_cos*sign, abs(load_value)/max_area_load*5*annotation_size, '{:.3g}'.format(load_value), annotation_size, theme) + area_load = VisAreaLoad(position0, position1, position2, position3, dir_cos*sign, abs(load_value)/max_area_load*5*annotation_size, f'{load_value:.3g}', annotation_size, theme) # Add the area load's arrows to the overall load polydata polydata.AddInputData(area_load.polydata.GetOutput()) @@ -2392,7 +2392,7 @@ def _MaxInternalForces(model, diagram_type, combo_name): return max_value -class VisMemberDiagram(): +class VisMemberDiagram: """Creates a visual representation of internal forces/moments along a member.""" def __init__(self, member, nodes, diagram_type, scale_factor, combo_name='Combo 1', theme='default', n_points=20, annotation_size=5, global_max=None): diff --git a/Pynite/__init__.py b/Pynite/__init__.py index 1edea31c..7fa885eb 100644 --- a/Pynite/__init__.py +++ b/Pynite/__init__.py @@ -5,6 +5,14 @@ from importlib.metadata import version, PackageNotFoundError + +__all__ = ( + "FEModel3D", + "ShearWall", + "Pynite", + "__version__", +) + try: __version__ = version("PyniteFEA") except PackageNotFoundError: diff --git a/Testing/test_2D_frames.py b/Testing/test_2D_frames.py index 84c48fac..09649fc7 100644 --- a/Testing/test_2D_frames.py +++ b/Testing/test_2D_frames.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_TC_analysis.py b/Testing/test_TC_analysis.py index ef44e617..974cad35 100644 --- a/Testing/test_TC_analysis.py +++ b/Testing/test_TC_analysis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_Visualization.py b/Testing/test_Visualization.py index 9767fea3..58bbb357 100644 --- a/Testing/test_Visualization.py +++ b/Testing/test_Visualization.py @@ -1,4 +1,3 @@ -import os import pytest import vtk from io import BytesIO diff --git a/Testing/test_end_releases.py b/Testing/test_end_releases.py index 1ba61f89..1449edbf 100644 --- a/Testing/test_end_releases.py +++ b/Testing/test_end_releases.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_loads.py b/Testing/test_loads.py index 8da2ae08..6d556043 100644 --- a/Testing/test_loads.py +++ b/Testing/test_loads.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -6,7 +5,7 @@ """ import unittest -from Pynite import FEModel3D, Section +from Pynite import FEModel3D import sys from io import StringIO diff --git a/Testing/test_meshes.py b/Testing/test_meshes.py index b6f85cd7..122f3fda 100644 --- a/Testing/test_meshes.py +++ b/Testing/test_meshes.py @@ -1,5 +1,4 @@ from Pynite import FEModel3D -from Pynite.Rendering import Renderer from math import isclose diff --git a/Testing/test_plates&quads.py b/Testing/test_plates&quads.py index c64ecaa6..31eb300f 100644 --- a/Testing/test_plates&quads.py +++ b/Testing/test_plates&quads.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License diff --git a/Testing/test_pushover.py b/Testing/test_pushover.py index efa217d9..38331e25 100644 --- a/Testing/test_pushover.py +++ b/Testing/test_pushover.py @@ -2,7 +2,6 @@ import matplotlib matplotlib.use('TkAgg') -import matplotlib.pyplot as plt # noqa: E402 import numpy as np # noqa: E402 from Pynite.FEModel3D import FEModel3D # noqa: E402 @@ -68,6 +67,7 @@ def test_plastic_beam(): ) # Plot the two traces on the same graph for each combo + # import matplotlib.pyplot as plt # for combo_name, trace_data in plastic_beam._pushover_traces.items(): # steps = list(range(len(trace_data['M_ba']))) # plt.figure() diff --git a/Testing/test_reactions.py b/Testing/test_reactions.py index 28639a0a..26944ec5 100644 --- a/Testing/test_reactions.py +++ b/Testing/test_reactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from Pynite import FEModel3D diff --git a/Testing/test_reanalysis.py b/Testing/test_reanalysis.py index 3f47911c..91cdc368 100644 --- a/Testing/test_reanalysis.py +++ b/Testing/test_reanalysis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_reporting.py b/Testing/test_reporting.py index 7494eec3..df9ba674 100644 --- a/Testing/test_reporting.py +++ b/Testing/test_reporting.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Regression tests for the HTML reporting module (Pynite.Reporting). diff --git a/Testing/test_sloped_beam.py b/Testing/test_sloped_beam.py index f09e5c7c..f852b159 100644 --- a/Testing/test_sloped_beam.py +++ b/Testing/test_sloped_beam.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_spring_support.py b/Testing/test_spring_support.py index 84156d6d..7e35d9a9 100644 --- a/Testing/test_spring_support.py +++ b/Testing/test_spring_support.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License diff --git a/Testing/test_springs.py b/Testing/test_springs.py index 77777e04..b408d5d5 100644 --- a/Testing/test_springs.py +++ b/Testing/test_springs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from Pynite import FEModel3D import numpy as np diff --git a/Testing/test_support_settlement.py b/Testing/test_support_settlement.py index d7762c11..d05e538e 100644 --- a/Testing/test_support_settlement.py +++ b/Testing/test_support_settlement.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -12,7 +11,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_torsion.py b/Testing/test_torsion.py index 6f09f542..5c7673af 100644 --- a/Testing/test_torsion.py +++ b/Testing/test_torsion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest from Pynite import FEModel3D -import math import sys from io import StringIO diff --git a/Testing/test_unstable_structure.py b/Testing/test_unstable_structure.py index 158be3e9..6241ffcb 100644 --- a/Testing/test_unstable_structure.py +++ b/Testing/test_unstable_structure.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ MIT License @@ -7,7 +6,6 @@ import unittest -from numpy import True_ from Pynite import FEModel3D import sys from io import StringIO diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..1fbda16c --- /dev/null +++ b/ruff.toml @@ -0,0 +1,16 @@ +target-version = "py311" +extend-exclude = [ + "*.ipynb", + ".Archived", + "Derivations", + "docs", + "Resources", +] + +[lint] +select = [ + # https://docs.astral.sh/ruff/rules/unused-import/ + "F401", + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP", +] diff --git a/setup.py b/setup.py index 75f11189..a789c322 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -with open("README.md", "r") as fh: +with open("README.md") as fh: long_description = fh.read() setuptools.setup(