PyNiteFEA 3.0.0__tar.gz → 3.2.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.
Files changed (36) hide show
  1. {pynitefea-3.0.0 → pynitefea-3.2.0}/PKG-INFO +26 -29
  2. {pynitefea-3.0.0 → pynitefea-3.2.0}/PyNiteFEA.egg-info/PKG-INFO +26 -29
  3. {pynitefea-3.0.0 → pynitefea-3.2.0}/PyNiteFEA.egg-info/SOURCES.txt +1 -1
  4. {pynitefea-3.0.0 → pynitefea-3.2.0}/PyNiteFEA.egg-info/requires.txt +11 -13
  5. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Analysis.py +206 -32
  6. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/FEModel3D.py +30 -49
  7. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Member3D.py +92 -47
  8. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/PhysMember.py +19 -1
  9. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Rendering.py +6 -13
  10. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Report_Template.html +86 -86
  11. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Reporting.py +53 -24
  12. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Section.py +0 -1
  13. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/ShearWall.py +196 -64
  14. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Visualization.py +29 -42
  15. {pynitefea-3.0.0 → pynitefea-3.2.0}/README.md +10 -0
  16. pynitefea-3.2.0/pyproject.toml +70 -0
  17. pynitefea-3.0.0/setup.py +0 -40
  18. {pynitefea-3.0.0 → pynitefea-3.2.0}/LICENSE +0 -0
  19. {pynitefea-3.0.0 → pynitefea-3.2.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
  20. {pynitefea-3.0.0 → pynitefea-3.2.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
  21. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/BeamSegY.py +0 -0
  22. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/BeamSegZ.py +0 -0
  23. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/FixedEndReactions.py +0 -0
  24. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/LoadCombo.py +0 -0
  25. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/MainStyleSheet.css +0 -0
  26. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/MatFoundation.py +0 -0
  27. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Material.py +0 -0
  28. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Mesh.py +0 -0
  29. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Node3D.py +0 -0
  30. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Plate3D.py +0 -0
  31. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Quad3D.py +0 -0
  32. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Spring3D.py +0 -0
  33. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/Tri3D.py +0 -0
  34. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/VTKWriter.py +0 -0
  35. {pynitefea-3.0.0 → pynitefea-3.2.0}/Pynite/__init__.py +0 -0
  36. {pynitefea-3.0.0 → pynitefea-3.2.0}/setup.cfg +0 -0
@@ -1,55 +1,42 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyNiteFEA
3
- Version: 3.0.0
4
- Summary: A simple elastic 3D structural finite element library for Python.
5
- Home-page: https://github.com/JWock82/Pynite.git
6
- Author: D. Craig Brinck, PE, SE
7
- Author-email: Building.Code@outlook.com
3
+ Version: 3.2.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: PrettyTable
18
+ Requires-Dist: prettytable
19
19
  Requires-Dist: scipy
20
- Requires-Dist: matplotlib
21
20
  Provides-Extra: all
22
- Requires-Dist: Ipython; extra == "all"
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: pdfkit; extra == "all"
28
- Requires-Dist: Jinja2; extra == "all"
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: IPython; extra == "vtk"
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: pdfkit; extra == "reporting"
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,16 @@ 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.2.0
113
+ * Added shear wall pier top results and coupling beam right hand side results to the pier/coupling beam force results output.
114
+ * Adjusted shear wall pier and coupling beam internal force results sign convention to match the sign convention used for members.
115
+ * Added targeted testing for shear wall pier and coupling beam sign convention and load magnitudes.
116
+ * Added shear wall docstrings and a readthedocs documentation page.
117
+
118
+ 3.1.0
119
+ * Improved detection of unstable structures.
120
+ * 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.
121
+
125
122
  3.0.0
126
123
  * 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
124
  * 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.0.0
4
- Summary: A simple elastic 3D structural finite element library for Python.
5
- Home-page: https://github.com/JWock82/Pynite.git
6
- Author: D. Craig Brinck, PE, SE
7
- Author-email: Building.Code@outlook.com
3
+ Version: 3.2.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: PrettyTable
18
+ Requires-Dist: prettytable
19
19
  Requires-Dist: scipy
20
- Requires-Dist: matplotlib
21
20
  Provides-Extra: all
22
- Requires-Dist: Ipython; extra == "all"
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: pdfkit; extra == "all"
28
- Requires-Dist: Jinja2; extra == "all"
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: IPython; extra == "vtk"
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: pdfkit; extra == "reporting"
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,16 @@ 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.2.0
113
+ * Added shear wall pier top results and coupling beam right hand side results to the pier/coupling beam force results output.
114
+ * Adjusted shear wall pier and coupling beam internal force results sign convention to match the sign convention used for members.
115
+ * Added targeted testing for shear wall pier and coupling beam sign convention and load magnitudes.
116
+ * Added shear wall docstrings and a readthedocs documentation page.
117
+
118
+ 3.1.0
119
+ * Improved detection of unstable structures.
120
+ * 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.
121
+
125
122
  3.0.0
126
123
  * 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
124
  * 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,6 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
- setup.py
3
+ pyproject.toml
4
4
  PyNiteFEA.egg-info/PKG-INFO
5
5
  PyNiteFEA.egg-info/SOURCES.txt
6
6
  PyNiteFEA.egg-info/dependency_links.txt
@@ -1,30 +1,28 @@
1
+ matplotlib
1
2
  numpy>=2.4.0
2
- PrettyTable
3
+ prettytable
3
4
  scipy
4
- matplotlib
5
5
 
6
6
  [all]
7
- Ipython
8
- vtk>=9.3.0
9
- pyvista[all,trame]>=0.43.4
10
- trame_jupyter_extension
7
+ ipython
11
8
  ipywidgets
12
- pdfkit
13
- Jinja2
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
- pdfkit
26
- Jinja2
24
+ jinja2
27
25
 
28
26
  [vtk]
29
- IPython
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
- from numpy import array, atleast_2d, zeros, subtract, matmul, divide, seterr, nanmax
6
- from numpy.linalg import solve
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
- try:
221
- # Calculate the unknown displacements Delta_D1
222
- if sparse == True:
223
- # The partitioned stiffness matrix originates as `coo` and is converted to `csr`
224
- # format for mathematical operations. The `@` operator performs matrix multiplication
225
- # on sparse matrices.
226
- Delta_D1 = spsolve(K11, subtract(subtract(Delta_P1, Delta_FER1), K12 @ Delta_D2))
227
- Delta_D1 = Delta_D1.reshape(len(Delta_D1), 1)
228
- else:
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
- sub_member._fxi[combo_name] += Delta_f[0, 0]
608
- sub_member._fyi[combo_name] += Delta_f[1, 0]
609
- sub_member._fzi[combo_name] += Delta_f[2, 0]
610
- sub_member._mxi[combo_name] += Delta_f[3, 0]
611
- sub_member._myi[combo_name] += Delta_f[4, 0]
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