PyNiteFEA 3.0.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PKG-INFO +20 -29
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PyNiteFEA.egg-info/PKG-INFO +20 -29
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PyNiteFEA.egg-info/SOURCES.txt +1 -1
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PyNiteFEA.egg-info/requires.txt +11 -13
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Analysis.py +206 -32
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/FEModel3D.py +30 -49
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Member3D.py +92 -47
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/PhysMember.py +19 -1
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Rendering.py +6 -13
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Report_Template.html +86 -86
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Reporting.py +53 -24
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Section.py +0 -1
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/ShearWall.py +15 -7
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Visualization.py +29 -42
- {pynitefea-3.0.0 → pynitefea-3.1.0}/README.md +4 -0
- pynitefea-3.1.0/pyproject.toml +70 -0
- pynitefea-3.0.0/setup.py +0 -40
- {pynitefea-3.0.0 → pynitefea-3.1.0}/LICENSE +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/BeamSegY.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/BeamSegZ.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/FixedEndReactions.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/LoadCombo.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/MainStyleSheet.css +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/MatFoundation.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Material.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Mesh.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Node3D.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Plate3D.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Quad3D.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Spring3D.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/Tri3D.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/VTKWriter.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/Pynite/__init__.py +0 -0
- {pynitefea-3.0.0 → pynitefea-3.1.0}/setup.cfg +0 -0
|
@@ -1,55 +1,42 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version: 3.
|
|
4
|
-
Summary: A simple
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: A simple 3D structural finite element library for Python.
|
|
5
|
+
Author-email: "D. Craig Brinck, PE, SE" <Building.Code@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JWock82/Pynite
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Requires-Python: >=3.11
|
|
15
14
|
Description-Content-Type: text/markdown
|
|
16
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
17
|
Requires-Dist: numpy>=2.4.0
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: prettytable
|
|
19
19
|
Requires-Dist: scipy
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
20
|
Provides-Extra: all
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: vtk>=9.3.0; extra == "all"
|
|
24
|
-
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "all"
|
|
25
|
-
Requires-Dist: trame_jupyter_extension; extra == "all"
|
|
21
|
+
Requires-Dist: ipython; extra == "all"
|
|
26
22
|
Requires-Dist: ipywidgets; extra == "all"
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: jinja2; extra == "all"
|
|
24
|
+
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "all"
|
|
25
|
+
Requires-Dist: trame-jupyter-extension; extra == "all"
|
|
26
|
+
Requires-Dist: vtk>=9.3.0; extra == "all"
|
|
29
27
|
Provides-Extra: vtk
|
|
30
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: ipython; extra == "vtk"
|
|
31
29
|
Requires-Dist: vtk>=9.3.0; extra == "vtk"
|
|
32
30
|
Provides-Extra: pyvista
|
|
33
|
-
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "pyvista"
|
|
34
|
-
Requires-Dist: trame_jupyter_extension; extra == "pyvista"
|
|
35
31
|
Requires-Dist: ipywidgets; extra == "pyvista"
|
|
32
|
+
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "pyvista"
|
|
33
|
+
Requires-Dist: trame-jupyter-extension; extra == "pyvista"
|
|
36
34
|
Provides-Extra: reporting
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist: Jinja2; extra == "reporting"
|
|
35
|
+
Requires-Dist: jinja2; extra == "reporting"
|
|
39
36
|
Provides-Extra: derivations
|
|
40
37
|
Requires-Dist: jupyterlab; extra == "derivations"
|
|
41
38
|
Requires-Dist: sympy; extra == "derivations"
|
|
42
|
-
Dynamic: author
|
|
43
|
-
Dynamic: author-email
|
|
44
|
-
Dynamic: classifier
|
|
45
|
-
Dynamic: description
|
|
46
|
-
Dynamic: description-content-type
|
|
47
|
-
Dynamic: home-page
|
|
48
39
|
Dynamic: license-file
|
|
49
|
-
Dynamic: provides-extra
|
|
50
|
-
Dynamic: requires-dist
|
|
51
|
-
Dynamic: requires-python
|
|
52
|
-
Dynamic: summary
|
|
53
40
|
|
|
54
41
|
<div align="center">
|
|
55
42
|
<img src="https://github.com/JWock82/Pynite/raw/main/Resources/Full Logo No Buffer.png" width=40% align="center"/>
|
|
@@ -122,6 +109,10 @@ Here's a list of projects that use Pynite:
|
|
|
122
109
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
123
110
|
|
|
124
111
|
# What's New?
|
|
112
|
+
3.1.0
|
|
113
|
+
* Improved detection of unstable structures.
|
|
114
|
+
* Bug fix for shear walls. Shear walls were erroneously tracking other plates in the model for internal pier/coupling beam force summations, rather than just the plates related to the individual shear wall.
|
|
115
|
+
|
|
125
116
|
3.0.0
|
|
126
117
|
* Added pushover analysis! Currently, only steel sections are supported for pushover analysis. The assumed interaction equation used to estimate plastic behavior is currently calibrated toward I-shaped members.
|
|
127
118
|
* Refactored all elastic stiffness matrix references from `K` and `k` to `Ke` and `ke`. Most users will not notice this change. Power users who were accessing the stiffness matrices directly will want to note this refactor. This clarifies the code base by indicating which type of stiffness these matrices represent, and reduces ambiguity in preparation for addition of further geometric `Kg/kg` and plastic `Km/km` stiffness matrices.
|
|
@@ -1,55 +1,42 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version: 3.
|
|
4
|
-
Summary: A simple
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: A simple 3D structural finite element library for Python.
|
|
5
|
+
Author-email: "D. Craig Brinck, PE, SE" <Building.Code@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JWock82/Pynite
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Requires-Python: >=3.11
|
|
15
14
|
Description-Content-Type: text/markdown
|
|
16
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
17
|
Requires-Dist: numpy>=2.4.0
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: prettytable
|
|
19
19
|
Requires-Dist: scipy
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
20
|
Provides-Extra: all
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: vtk>=9.3.0; extra == "all"
|
|
24
|
-
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "all"
|
|
25
|
-
Requires-Dist: trame_jupyter_extension; extra == "all"
|
|
21
|
+
Requires-Dist: ipython; extra == "all"
|
|
26
22
|
Requires-Dist: ipywidgets; extra == "all"
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: jinja2; extra == "all"
|
|
24
|
+
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "all"
|
|
25
|
+
Requires-Dist: trame-jupyter-extension; extra == "all"
|
|
26
|
+
Requires-Dist: vtk>=9.3.0; extra == "all"
|
|
29
27
|
Provides-Extra: vtk
|
|
30
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: ipython; extra == "vtk"
|
|
31
29
|
Requires-Dist: vtk>=9.3.0; extra == "vtk"
|
|
32
30
|
Provides-Extra: pyvista
|
|
33
|
-
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "pyvista"
|
|
34
|
-
Requires-Dist: trame_jupyter_extension; extra == "pyvista"
|
|
35
31
|
Requires-Dist: ipywidgets; extra == "pyvista"
|
|
32
|
+
Requires-Dist: pyvista[all,trame]>=0.43.4; extra == "pyvista"
|
|
33
|
+
Requires-Dist: trame-jupyter-extension; extra == "pyvista"
|
|
36
34
|
Provides-Extra: reporting
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist: Jinja2; extra == "reporting"
|
|
35
|
+
Requires-Dist: jinja2; extra == "reporting"
|
|
39
36
|
Provides-Extra: derivations
|
|
40
37
|
Requires-Dist: jupyterlab; extra == "derivations"
|
|
41
38
|
Requires-Dist: sympy; extra == "derivations"
|
|
42
|
-
Dynamic: author
|
|
43
|
-
Dynamic: author-email
|
|
44
|
-
Dynamic: classifier
|
|
45
|
-
Dynamic: description
|
|
46
|
-
Dynamic: description-content-type
|
|
47
|
-
Dynamic: home-page
|
|
48
39
|
Dynamic: license-file
|
|
49
|
-
Dynamic: provides-extra
|
|
50
|
-
Dynamic: requires-dist
|
|
51
|
-
Dynamic: requires-python
|
|
52
|
-
Dynamic: summary
|
|
53
40
|
|
|
54
41
|
<div align="center">
|
|
55
42
|
<img src="https://github.com/JWock82/Pynite/raw/main/Resources/Full Logo No Buffer.png" width=40% align="center"/>
|
|
@@ -122,6 +109,10 @@ Here's a list of projects that use Pynite:
|
|
|
122
109
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
123
110
|
|
|
124
111
|
# What's New?
|
|
112
|
+
3.1.0
|
|
113
|
+
* Improved detection of unstable structures.
|
|
114
|
+
* Bug fix for shear walls. Shear walls were erroneously tracking other plates in the model for internal pier/coupling beam force summations, rather than just the plates related to the individual shear wall.
|
|
115
|
+
|
|
125
116
|
3.0.0
|
|
126
117
|
* Added pushover analysis! Currently, only steel sections are supported for pushover analysis. The assumed interaction equation used to estimate plastic behavior is currently calibrated toward I-shaped members.
|
|
127
118
|
* Refactored all elastic stiffness matrix references from `K` and `k` to `Ke` and `ke`. Most users will not notice this change. Power users who were accessing the stiffness matrices directly will want to note this refactor. This clarifies the code base by indicating which type of stiffness these matrices represent, and reduces ambiguity in preparation for addition of further geometric `Kg/kg` and plastic `Km/km` stiffness matrices.
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
+
matplotlib
|
|
1
2
|
numpy>=2.4.0
|
|
2
|
-
|
|
3
|
+
prettytable
|
|
3
4
|
scipy
|
|
4
|
-
matplotlib
|
|
5
5
|
|
|
6
6
|
[all]
|
|
7
|
-
|
|
8
|
-
vtk>=9.3.0
|
|
9
|
-
pyvista[all,trame]>=0.43.4
|
|
10
|
-
trame_jupyter_extension
|
|
7
|
+
ipython
|
|
11
8
|
ipywidgets
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
jinja2
|
|
10
|
+
pyvista[all,trame]>=0.43.4
|
|
11
|
+
trame-jupyter-extension
|
|
12
|
+
vtk>=9.3.0
|
|
14
13
|
|
|
15
14
|
[derivations]
|
|
16
15
|
jupyterlab
|
|
17
16
|
sympy
|
|
18
17
|
|
|
19
18
|
[pyvista]
|
|
20
|
-
pyvista[all,trame]>=0.43.4
|
|
21
|
-
trame_jupyter_extension
|
|
22
19
|
ipywidgets
|
|
20
|
+
pyvista[all,trame]>=0.43.4
|
|
21
|
+
trame-jupyter-extension
|
|
23
22
|
|
|
24
23
|
[reporting]
|
|
25
|
-
|
|
26
|
-
Jinja2
|
|
24
|
+
jinja2
|
|
27
25
|
|
|
28
26
|
[vtk]
|
|
29
|
-
|
|
27
|
+
ipython
|
|
30
28
|
vtk>=9.3.0
|
|
@@ -2,8 +2,10 @@ from __future__ import annotations # Allows more recent type hints features
|
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
from math import isclose
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from numpy import array, atleast_2d, zeros, subtract, matmul, divide, seterr, nanmax, asarray, isfinite
|
|
8
|
+
from numpy.linalg import solve, norm, LinAlgError
|
|
7
9
|
|
|
8
10
|
from Pynite.LoadCombo import LoadCombo
|
|
9
11
|
|
|
@@ -166,6 +168,79 @@ def _check_stability(model: FEModel3D, K: NDArray[float64]) -> None:
|
|
|
166
168
|
return
|
|
167
169
|
|
|
168
170
|
|
|
171
|
+
# Exception message shared by the global-instability checks below
|
|
172
|
+
_SINGULAR_MSG = ('The stiffness matrix is singular, which implies rigid body motion. The structure '
|
|
173
|
+
'is unstable. Aborting analysis.')
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _solve_unknown_disp(K11, rhs, sparse: bool = True, check_stability: bool = True, tol: float = 1e-6):
|
|
177
|
+
"""Solves ``K11 @ x = rhs`` for the unknown displacements, raising on a singular stiffness matrix.
|
|
178
|
+
|
|
179
|
+
The nodal stability check (:func:`_check_stability`) only catches degrees of freedom with a zero
|
|
180
|
+
stiffness on the diagonal. It does not catch *global* instability, where every diagonal term is
|
|
181
|
+
non-zero but the assembled matrix is still rank-deficient -- for example a mechanism created by
|
|
182
|
+
member end releases (a beam with an internal hinge and no support there), or a structure with
|
|
183
|
+
insufficient supports (rigid body motion). In those cases the underlying solvers do not reliably
|
|
184
|
+
report the problem: ``scipy.sparse.linalg.spsolve`` may return ``NaN`` (with only a
|
|
185
|
+
``MatrixRankWarning``) or even finite-but-meaningless values, and ``numpy.linalg.solve`` may miss
|
|
186
|
+
the singularity due to floating point round-off. The model then silently reports erroneous
|
|
187
|
+
results.
|
|
188
|
+
|
|
189
|
+
When ``check_stability`` is ``True`` the computed displacements are verified against equilibrium
|
|
190
|
+
by measuring the relative residual ``||K11 @ x - rhs|| / ||rhs||``. A stable structure -- even a
|
|
191
|
+
numerically ill-conditioned one -- solves to within round-off (the direct solvers are backward
|
|
192
|
+
stable), whereas a singular system yields ``NaN``/``inf`` or finite values with an O(1) residual.
|
|
193
|
+
For the homogeneous ``rhs == 0`` edge case, an invertible system has the unique solution
|
|
194
|
+
``x == 0``; a non-zero solved displacement is therefore treated as unstable. These checks are
|
|
195
|
+
robust indicators of global instability that, unlike inspecting the factorization's pivot
|
|
196
|
+
magnitudes, do not misclassify legitimately ill-conditioned but stable models. When
|
|
197
|
+
``check_stability`` is ``False`` the bare solvers are used unchanged.
|
|
198
|
+
|
|
199
|
+
:param K11: The partitioned stiffness matrix for the unknown displacements (square, ``n``x``n``).
|
|
200
|
+
:param rhs: The right-hand side vector (shape ``(n, 1)``).
|
|
201
|
+
:param sparse: Whether ``K11`` is a SciPy sparse matrix. Defaults to ``True``.
|
|
202
|
+
:param check_stability: Whether to check for global instability after solving. Defaults to ``True``.
|
|
203
|
+
:param tol: Relative residual above which the matrix is treated as singular. Defaults to ``1e-6``.
|
|
204
|
+
:raises Exception: If the stiffness matrix is singular (the structure is unstable).
|
|
205
|
+
:return: The solved displacement vector, shape ``(n, 1)``.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
singular_warning = False
|
|
209
|
+
if sparse:
|
|
210
|
+
from scipy.sparse.linalg import MatrixRankWarning, spsolve
|
|
211
|
+
|
|
212
|
+
K = K11.tocsr()
|
|
213
|
+
# A singular matrix makes `spsolve` emit a `MatrixRankWarning`; we detect that case below via
|
|
214
|
+
# the residual/finiteness check and raise a descriptive error, so suppress the bare warning.
|
|
215
|
+
with warnings.catch_warnings(record=True) as caught_warnings:
|
|
216
|
+
warnings.simplefilter('always', MatrixRankWarning)
|
|
217
|
+
x = spsolve(K, rhs)
|
|
218
|
+
singular_warning = any(warning.category is MatrixRankWarning for warning in caught_warnings)
|
|
219
|
+
else:
|
|
220
|
+
K = asarray(K11, dtype=float)
|
|
221
|
+
try:
|
|
222
|
+
x = solve(K, rhs)
|
|
223
|
+
except LinAlgError as exc:
|
|
224
|
+
raise Exception(_SINGULAR_MSG) from exc
|
|
225
|
+
|
|
226
|
+
x = asarray(x, dtype=float).reshape(len(x), 1)
|
|
227
|
+
|
|
228
|
+
if check_stability:
|
|
229
|
+
residual = norm(K @ x - rhs)
|
|
230
|
+
rhs_norm = norm(rhs)
|
|
231
|
+
if rhs_norm > 0:
|
|
232
|
+
unstable = residual > tol * rhs_norm
|
|
233
|
+
else:
|
|
234
|
+
# For a homogeneous system, an invertible matrix has the unique solution x == 0, so an
|
|
235
|
+
# absolute displacement tolerance is appropriate here.
|
|
236
|
+
unstable = norm(x) > tol
|
|
237
|
+
|
|
238
|
+
if (sparse and singular_warning) or not isfinite(x).all() or unstable:
|
|
239
|
+
raise Exception(_SINGULAR_MSG)
|
|
240
|
+
|
|
241
|
+
return x
|
|
242
|
+
|
|
243
|
+
|
|
169
244
|
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:
|
|
170
245
|
"""Performs the shared first-order elastic/tension-compression-only solution path.
|
|
171
246
|
|
|
@@ -173,10 +248,6 @@ def _first_order(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1:
|
|
|
173
248
|
first-order workflow lives in one place.
|
|
174
249
|
"""
|
|
175
250
|
|
|
176
|
-
# Import `scipy` features if the sparse solver is being used
|
|
177
|
-
if sparse == True:
|
|
178
|
-
from scipy.sparse.linalg import spsolve
|
|
179
|
-
|
|
180
251
|
# Calculate the incremental enforced displacement vector
|
|
181
252
|
Delta_D2 = D2/num_steps
|
|
182
253
|
|
|
@@ -217,19 +288,15 @@ def _first_order(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1:
|
|
|
217
288
|
# All displacements are known, so Delta_D1 is an empty vector
|
|
218
289
|
Delta_D1 = []
|
|
219
290
|
else:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Delta_D1 = solve(K11, subtract(subtract(Delta_P1, Delta_FER1), matmul(K12, Delta_D2)))
|
|
230
|
-
except:
|
|
231
|
-
# Return out of the method if 'K' is singular and provide an error message
|
|
232
|
-
raise Exception('The stiffness matrix is singular, which implies rigid body motion. The structure is unstable. Aborting analysis.')
|
|
291
|
+
# Calculate the unknown displacements Delta_D1. The partitioned stiffness matrix
|
|
292
|
+
# originates as `coo` and is converted to `csr`/`csc` for mathematical operations.
|
|
293
|
+
# `_solve_unknown_disp` also detects global instability (a singular matrix) that
|
|
294
|
+
# the bare solvers can silently miss.
|
|
295
|
+
if sparse == True:
|
|
296
|
+
rhs = subtract(subtract(Delta_P1, Delta_FER1), K12 @ Delta_D2)
|
|
297
|
+
else:
|
|
298
|
+
rhs = subtract(subtract(Delta_P1, Delta_FER1), matmul(K12, Delta_D2))
|
|
299
|
+
Delta_D1 = _solve_unknown_disp(K11, rhs, sparse, check_stability)
|
|
233
300
|
|
|
234
301
|
# Store or sum the calculated displacements to the model and the nodes in the model
|
|
235
302
|
if load_step == 1:
|
|
@@ -603,19 +670,12 @@ def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num:
|
|
|
603
670
|
|
|
604
671
|
# Calculate change in the local end force vector for this load step
|
|
605
672
|
Delta_f = sub_member.f(combo_name, Delta_d, Delta_fer)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
sub_member.
|
|
612
|
-
sub_member._mzi[combo_name] += Delta_f[5, 0]
|
|
613
|
-
sub_member._fxj[combo_name] += Delta_f[6, 0]
|
|
614
|
-
sub_member._fyj[combo_name] += Delta_f[7, 0]
|
|
615
|
-
sub_member._fzj[combo_name] += Delta_f[8, 0]
|
|
616
|
-
sub_member._mxj[combo_name] += Delta_f[9, 0]
|
|
617
|
-
sub_member._myj[combo_name] += Delta_f[10, 0]
|
|
618
|
-
sub_member._mzj[combo_name] += Delta_f[11, 0]
|
|
673
|
+
|
|
674
|
+
# TODO: Before accepting `Delta_f`, check if we've overshot any member section
|
|
675
|
+
# capacity interaction curves, and adjust the load step if necessary
|
|
676
|
+
|
|
677
|
+
# Store the change in the local end force vector for this load step
|
|
678
|
+
sub_member.f_nonlin[combo_name] += Delta_f.reshape(12)
|
|
619
679
|
|
|
620
680
|
else:
|
|
621
681
|
|
|
@@ -633,6 +693,120 @@ def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num:
|
|
|
633
693
|
print('- Restarting load step due to plastic load reversal')
|
|
634
694
|
|
|
635
695
|
|
|
696
|
+
def _load_step_ratio(model: FEModel3D, combo_name: str, Delta_f: NDArray[float64]) -> float:
|
|
697
|
+
"""
|
|
698
|
+
Determines the fraction of the pushover load step that should be applied.
|
|
699
|
+
|
|
700
|
+
It is desirable to ensure plastic hinges occur at the end of a load step. This method checks
|
|
701
|
+
if the current load step has caused any plastic hinges to form. If so, it calculates the
|
|
702
|
+
fraction of the load step that should be applied to ensure the hinge forms at the end of the
|
|
703
|
+
load step.
|
|
704
|
+
|
|
705
|
+
See Section 12.6 of `Matrix Structural Analysis, Second Edition` for more information.
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
# Define a local helper method for this method
|
|
709
|
+
def tal(p, my, mz, dp, dmy, dmz, Phi):
|
|
710
|
+
|
|
711
|
+
# Initial guesses
|
|
712
|
+
tal_l = 0.0 # Lower bound guess
|
|
713
|
+
tal_u = 1.0 # Upper bound guess
|
|
714
|
+
tal_r = tal_u # Assumed initial value of the root we are seeking
|
|
715
|
+
|
|
716
|
+
# Counter variable
|
|
717
|
+
num_iter = 0
|
|
718
|
+
|
|
719
|
+
while (not isclose(Phi(p + tal_r*dp, my + tal_r*dmy, mz + tal_r*dmz) - 1.0, 0.0)
|
|
720
|
+
and num_iter < 100):
|
|
721
|
+
|
|
722
|
+
# Increment the iteration counter
|
|
723
|
+
num_iter += 1
|
|
724
|
+
|
|
725
|
+
# Calculate `Phi` (the utilization ratio) for the lower and upper guesses
|
|
726
|
+
Phi_tal_l = Phi(p + tal_l*dp, my + tal_l*dmy, mz + tal_l*dmz)
|
|
727
|
+
Phi_tal_u = Phi(p + tal_u*dp, my + tal_u*dmy, mz + tal_u*dmz)
|
|
728
|
+
|
|
729
|
+
# Estimate the root and its `Phi` value
|
|
730
|
+
tal_r = 1 - (Phi_tal_u - 1.0)*(tal_l - tal_u)/(Phi_tal_l - Phi_tal_u)
|
|
731
|
+
Phi_tal_r = Phi(p + tal_r*dp, my + tal_r*dmy, mz + tal_r*dmz)
|
|
732
|
+
|
|
733
|
+
# Replace the lower or upper guess with the new estimate depending on which (Phi - 1)
|
|
734
|
+
# one has the same sign as (Phi - 1) for the new root estimate.
|
|
735
|
+
if (Phi_tal_l - 1.0)*(Phi_tal_r - 1.0) < 0:
|
|
736
|
+
tal_u = tal_r
|
|
737
|
+
elif (Phi_tal_u - 1.0)*(Phi_tal_r - 1.0) < 0:
|
|
738
|
+
tal_l = tal_r
|
|
739
|
+
|
|
740
|
+
# Cap the number of iterations to avoid infinite loops
|
|
741
|
+
if num_iter >= 100:
|
|
742
|
+
raise Exception('Unable to determine the fraction of the pushover load step that should be applied.')
|
|
743
|
+
|
|
744
|
+
# Return the fraction of the pushover load step that should be applied
|
|
745
|
+
return tal_r
|
|
746
|
+
|
|
747
|
+
# Initilize the lowest value for the fraction of the pushover load step that should be applied
|
|
748
|
+
tal_min = 1.0
|
|
749
|
+
|
|
750
|
+
# Step through each physical member in the model
|
|
751
|
+
for phys_member in model.members.values():
|
|
752
|
+
|
|
753
|
+
# Step through each sub-member of the physical member
|
|
754
|
+
for sub_member in phys_member.sub_members.values():
|
|
755
|
+
|
|
756
|
+
# Obtain the member's plastic load capacities
|
|
757
|
+
section = sub_member.section
|
|
758
|
+
Py = section.material.fy*section.A
|
|
759
|
+
Mpy = section.material.fy*section.Zy
|
|
760
|
+
Mpz = section.material.fy*section.Zz
|
|
761
|
+
|
|
762
|
+
# Obtain member end forces for the latest load step
|
|
763
|
+
fxi = sub_member.f_nonlin[combo_name][0]
|
|
764
|
+
myi = sub_member.f_nonlin[combo_name][4]
|
|
765
|
+
mzi = sub_member.f_nonlin[combo_name][5]
|
|
766
|
+
fxj = sub_member.f_nonlin[combo_name][6]
|
|
767
|
+
myj = sub_member.f_nonlin[combo_name][10]
|
|
768
|
+
mzj = sub_member.f_nonlin[combo_name][11]
|
|
769
|
+
|
|
770
|
+
# Check if the yield surface has been exceeded at the i-end of the member
|
|
771
|
+
if sub_member.section.Phi(fxi, myi, mzi) > 1.0:
|
|
772
|
+
|
|
773
|
+
# Convert the member end forces to nondiminensionalized values
|
|
774
|
+
p = abs(fxi/Py)
|
|
775
|
+
m_y = abs(myi/Mpy)
|
|
776
|
+
m_z = abs(mzi/Mpz)
|
|
777
|
+
|
|
778
|
+
dp = abs(Delta_f[0, 0]/Py)
|
|
779
|
+
dmy = abs(Delta_f[4, 0]/Mpy)
|
|
780
|
+
dmz = abs(Delta_f[5, 0]/Mpz)
|
|
781
|
+
|
|
782
|
+
tal_i = tal(p, m_y, m_z, dp, dmy, dmz, sub_member.section.Phi)
|
|
783
|
+
|
|
784
|
+
else: tal_i = 1.0
|
|
785
|
+
|
|
786
|
+
# Check if the yield surface has been exceeded at the j-end of the member
|
|
787
|
+
if sub_member.section.Phi(fxj, myj, mzj) > 1.0:
|
|
788
|
+
|
|
789
|
+
# Convert the member end forces to nondiminensionalized values
|
|
790
|
+
p = abs(fxj/Py)
|
|
791
|
+
m_y = abs(myj/Mpy)
|
|
792
|
+
m_z = abs(mzj/Mpz)
|
|
793
|
+
|
|
794
|
+
dp = abs(Delta_f[6, 0]/Py)
|
|
795
|
+
dmy = abs(Delta_f[10, 0]/Mpy)
|
|
796
|
+
dmz = abs(Delta_f[11, 0]/Mpz)
|
|
797
|
+
|
|
798
|
+
tal_j = tal(p, m_y, m_z, dp, dmy, dmz, sub_member.section.Phi)
|
|
799
|
+
|
|
800
|
+
else: tal_j = 1.0
|
|
801
|
+
|
|
802
|
+
if tal_i < tal_min:
|
|
803
|
+
tal_min = tal_i
|
|
804
|
+
if tal_j < tal_min:
|
|
805
|
+
tal_min = tal_j
|
|
806
|
+
|
|
807
|
+
# Return the fraction of the pushover load step that should be applied
|
|
808
|
+
return tal_min
|
|
809
|
+
|
|
636
810
|
def _unpartition(model: FEModel3D, V1: NDArray[float64], V2: NDArray[float64], V1_indices: List[int], V2_indices: List[int]) -> NDArray[float64]:
|
|
637
811
|
"""Unpartitions a vector and returns it as a global vector including all dofs.
|
|
638
812
|
|
|
@@ -1843,7 +1843,7 @@ class FEModel3D():
|
|
|
1843
1843
|
else:
|
|
1844
1844
|
if self.solution == 'Pushover':
|
|
1845
1845
|
# Use the axial force at the current nonlinear/inelastic load step
|
|
1846
|
-
P = member.
|
|
1846
|
+
P = member.f_nonlin[combo_name][6] - member.f_nonlin[combo_name][0]
|
|
1847
1847
|
else:
|
|
1848
1848
|
# Calculate the member axial force due to linear/elastic axial strain
|
|
1849
1849
|
d = member.d(combo_name)
|
|
@@ -2266,10 +2266,6 @@ class FEModel3D():
|
|
|
2266
2266
|
print('| Analyzing: Linear |')
|
|
2267
2267
|
print('+-------------------+')
|
|
2268
2268
|
|
|
2269
|
-
# Import `scipy` features if the sparse solver is being used
|
|
2270
|
-
if sparse == True:
|
|
2271
|
-
from scipy.sparse.linalg import spsolve
|
|
2272
|
-
|
|
2273
2269
|
# Prepare the model for analysis
|
|
2274
2270
|
Analysis._prepare_model(self)
|
|
2275
2271
|
|
|
@@ -2307,19 +2303,15 @@ class FEModel3D():
|
|
|
2307
2303
|
# All displacements are known, so D1 is an empty vector
|
|
2308
2304
|
D1 = []
|
|
2309
2305
|
else:
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
D1 = solve(K11, np.subtract(np.subtract(P1, FER1), np.matmul(K12, D2)))
|
|
2320
|
-
except:
|
|
2321
|
-
# Return out of the method if 'K' is singular and provide an error message
|
|
2322
|
-
raise Exception('The stiffness matrix is singular, which implies rigid body motion. The structure is unstable. Aborting analysis.')
|
|
2306
|
+
# Calculate the unknown displacements D1. The partitioned stiffness matrix
|
|
2307
|
+
# originates as `coo` and is converted to `csr`/`csc` for mathematical
|
|
2308
|
+
# operations. `_solve_unknown_disp` also detects global instability (a singular
|
|
2309
|
+
# matrix) that the bare solvers can silently miss.
|
|
2310
|
+
if sparse == True:
|
|
2311
|
+
rhs = np.subtract(np.subtract(P1, FER1), K12.tocsr() @ D2)
|
|
2312
|
+
else:
|
|
2313
|
+
rhs = np.subtract(np.subtract(P1, FER1), np.matmul(K12, D2))
|
|
2314
|
+
D1 = Analysis._solve_unknown_disp(K11, rhs, sparse, check_stability)
|
|
2323
2315
|
|
|
2324
2316
|
# Store the calculated displacements to the model and the nodes in the model
|
|
2325
2317
|
Analysis._store_displacements(self, D1, D2, D1_indices, D2_indices, combo)
|
|
@@ -2657,13 +2649,12 @@ class FEModel3D():
|
|
|
2657
2649
|
|
|
2658
2650
|
return fig, ax, save_path
|
|
2659
2651
|
|
|
2660
|
-
def analyze_pushover(self, log=False, check_stability=True, push_combo='Push', max_iter=30, tol=0.01, sparse=True, combo_tags=None, control_node=None, control_direction='DX', control_limit=None, traces=None):
|
|
2661
|
-
"""Performs a pushover analysis
|
|
2652
|
+
def analyze_pushover(self, log=False, check_stability=True, push_combo='Push', max_iter=30, tol=0.01, sparse=True, combo_tags=None, control_node=None, control_direction='DX', control_limit=None, traces=None, P_Delta=False):
|
|
2653
|
+
"""Performs a pushover analysis with optional second-order P-Delta effects.
|
|
2662
2654
|
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
production-ready.
|
|
2655
|
+
By default, the initial preload state is established using first-order analysis and the
|
|
2656
|
+
incremental pushover steps omit the geometric stiffness matrix. Set ``P_Delta=True`` to
|
|
2657
|
+
opt into second-order behavior for both the preload and the subsequent pushover steps.
|
|
2667
2658
|
|
|
2668
2659
|
:param log: Prints updates to the console if set to True. Default is False.
|
|
2669
2660
|
:type log: bool, optional
|
|
@@ -2690,6 +2681,9 @@ class FEModel3D():
|
|
|
2690
2681
|
:type control_limit: float | None, optional
|
|
2691
2682
|
:param traces: Optional dictionary of trace names mapped to callables that accept combo_name.
|
|
2692
2683
|
:type traces: dict | None, optional
|
|
2684
|
+
:param P_Delta: Set to True to include geometric stiffness and second-order effects during
|
|
2685
|
+
pushover analysis. Defaults to False.
|
|
2686
|
+
:type P_Delta: bool, optional
|
|
2693
2687
|
"""
|
|
2694
2688
|
|
|
2695
2689
|
if control_limit is not None and control_node is None:
|
|
@@ -2744,9 +2738,7 @@ class FEModel3D():
|
|
|
2744
2738
|
# Initialize per-combo pushover result state.
|
|
2745
2739
|
self._pushover_state = {}
|
|
2746
2740
|
self._pushover_traces = {}
|
|
2747
|
-
|
|
2748
|
-
# User-facing pushover is currently first-order only.
|
|
2749
|
-
self._pushover_P_Delta = False
|
|
2741
|
+
self._pushover_P_Delta = P_Delta
|
|
2750
2742
|
|
|
2751
2743
|
# Step through each load combination
|
|
2752
2744
|
for combo in combo_list:
|
|
@@ -2759,10 +2751,7 @@ class FEModel3D():
|
|
|
2759
2751
|
# Set member end force summations to zero
|
|
2760
2752
|
for phys_member in self.members.values():
|
|
2761
2753
|
for sub_member in phys_member.sub_members.values():
|
|
2762
|
-
sub_member.
|
|
2763
|
-
sub_member._mxi, sub_member._myi, sub_member._mzi = {}, {}, {}
|
|
2764
|
-
sub_member._fxj, sub_member._fyj, sub_member._fzj = {}, {}, {}
|
|
2765
|
-
sub_member._mxj, sub_member._myj, sub_member._mzj = {}, {}, {}
|
|
2754
|
+
sub_member.f_nonlin = {}
|
|
2766
2755
|
|
|
2767
2756
|
# Get the partitioned global fixed end reaction vector for the load combination
|
|
2768
2757
|
FER1, FER2 = Analysis._partition(self, self.FER(combo.name), D1_indices, D2_indices)
|
|
@@ -2771,29 +2760,22 @@ class FEModel3D():
|
|
|
2771
2760
|
P1, P2 = Analysis._partition(self, self.P(combo.name), D1_indices, D2_indices)
|
|
2772
2761
|
|
|
2773
2762
|
# Preload the primary load combination before any pushover increments are applied.
|
|
2774
|
-
#
|
|
2763
|
+
# By default, pushover uses the first-order analysis path. Set `P_Delta=True` to opt
|
|
2764
|
+
# into a second-order preload state.
|
|
2775
2765
|
if log:
|
|
2776
|
-
print('- Preloading the primary combination using first-order analysis')
|
|
2766
|
+
print('- Preloading the primary combination using ' + ('P-Delta' if P_Delta else 'first-order') + ' analysis')
|
|
2777
2767
|
|
|
2778
|
-
|
|
2768
|
+
if P_Delta:
|
|
2769
|
+
Analysis._PDelta(self, combo.name, P1, FER1, D1_indices, D2_indices, D2, False, sparse, check_stability, max_iter)
|
|
2770
|
+
else:
|
|
2771
|
+
Analysis._first_order(self, combo.name, P1, FER1, D1_indices, D2_indices, D2, False, sparse, check_stability, max_iter)
|
|
2779
2772
|
|
|
2780
2773
|
# Seed the nonlinear end-force history with the elastic preload state from the
|
|
2781
2774
|
# primary load combination before any pushover increments are applied.
|
|
2782
2775
|
for phys_member in self.members.values():
|
|
2783
2776
|
for sub_member in phys_member.sub_members.values():
|
|
2784
2777
|
f = sub_member.f(combo.name)
|
|
2785
|
-
sub_member.
|
|
2786
|
-
sub_member._fyi[combo.name] = f[1, 0]
|
|
2787
|
-
sub_member._fzi[combo.name] = f[2, 0]
|
|
2788
|
-
sub_member._mxi[combo.name] = f[3, 0]
|
|
2789
|
-
sub_member._myi[combo.name] = f[4, 0]
|
|
2790
|
-
sub_member._mzi[combo.name] = f[5, 0]
|
|
2791
|
-
sub_member._fxj[combo.name] = f[6, 0]
|
|
2792
|
-
sub_member._fyj[combo.name] = f[7, 0]
|
|
2793
|
-
sub_member._fzj[combo.name] = f[8, 0]
|
|
2794
|
-
sub_member._mxj[combo.name] = f[9, 0]
|
|
2795
|
-
sub_member._myj[combo.name] = f[10, 0]
|
|
2796
|
-
sub_member._mzj[combo.name] = f[11, 0]
|
|
2778
|
+
sub_member.f_nonlin[combo.name] = f.reshape(12)
|
|
2797
2779
|
|
|
2798
2780
|
# The P-Delta analysis above flagged the solution as a P-Delta solution, but we need to
|
|
2799
2781
|
# indicate that this is actually a Pushover solution so that the calls to Member3D.f()
|
|
@@ -2820,7 +2802,7 @@ class FEModel3D():
|
|
|
2820
2802
|
# each load step and can be queried for results during or after the pushover analysis.
|
|
2821
2803
|
self._pushover_state[combo.name] = {
|
|
2822
2804
|
'push_combo': push_combo,
|
|
2823
|
-
'P_Delta':
|
|
2805
|
+
'P_Delta': P_Delta,
|
|
2824
2806
|
'status': 'running',
|
|
2825
2807
|
'step_num': 0,
|
|
2826
2808
|
'load_factor': 0.0,
|
|
@@ -2841,8 +2823,7 @@ class FEModel3D():
|
|
|
2841
2823
|
|
|
2842
2824
|
# Run the next pushover load step
|
|
2843
2825
|
# Note: The validity of the pushover step is checked and handled within the _pushover_step method
|
|
2844
|
-
|
|
2845
|
-
Analysis._pushover_step(self, combo.name, push_combo, step_num, P1_push, FER1_push, FER2_push, D1_indices, D2_indices, D2, log, sparse, check_stability, tol, False, max_iter)
|
|
2826
|
+
Analysis._pushover_step(self, combo.name, push_combo, step_num, P1_push, FER1_push, FER2_push, D1_indices, D2_indices, D2, log, sparse, check_stability, tol, P_Delta, max_iter)
|
|
2846
2827
|
|
|
2847
2828
|
control_displacement = None
|
|
2848
2829
|
if control_node is not None:
|