Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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]
2 changes: 1 addition & 1 deletion Archived/MITC4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Examples/Beam on Elastic Foundation.py
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
2 changes: 0 additions & 2 deletions Examples/Circular Bin with Conical Hopper.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion Examples/Pushover Analysis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from math import isclose

from Pynite.FEModel3D import FEModel3D

Expand Down
3 changes: 0 additions & 3 deletions Examples/Simple Beam - Factored Envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion Examples/Simple Beam - Point Load.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
40 changes: 20 additions & 20 deletions Pynite/Analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
7 changes: 3 additions & 4 deletions Pynite/BeamSegZ.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 6 20:52:31 2017

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

Expand Down Expand Up @@ -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.
"""
Expand Down
40 changes: 19 additions & 21 deletions Pynite/FEModel3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
"""
Expand All @@ -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

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion Pynite/FixedEndReactions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 3 20:58:03 2017

Expand Down
9 changes: 4 additions & 5 deletions Pynite/LoadCombo.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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})"
Expand Down
2 changes: 1 addition & 1 deletion Pynite/Material.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading
Loading