PyNiteFEA 2.4.0__tar.gz → 3.0.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-2.4.0 → pynitefea-3.0.0}/PKG-INFO +14 -3
- {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/PKG-INFO +14 -3
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Analysis.py +263 -64
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/BeamSegY.py +12 -8
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/BeamSegZ.py +38 -28
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/FEModel3D.py +296 -149
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/LoadCombo.py +3 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/MatFoundation.py +3 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Material.py +4 -1
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Member3D.py +274 -147
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Mesh.py +27 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Node3D.py +3 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/PhysMember.py +66 -43
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Plate3D.py +27 -24
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Quad3D.py +32 -29
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Rendering.py +128 -20
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Section.py +28 -17
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/ShearWall.py +38 -12
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Spring3D.py +12 -8
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Tri3D.py +27 -24
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/VTKWriter.py +3 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Visualization.py +133 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/README.md +13 -2
- {pynitefea-2.4.0 → pynitefea-3.0.0}/setup.py +1 -1
- {pynitefea-2.4.0 → pynitefea-3.0.0}/LICENSE +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/requires.txt +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/FixedEndReactions.py +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/MainStyleSheet.css +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Report_Template.html +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Reporting.py +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/__init__.py +0 -0
- {pynitefea-2.4.0 → pynitefea-3.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A simple elastic 3D structural finite element library for Python.
|
|
5
5
|
Home-page: https://github.com/JWock82/Pynite.git
|
|
6
6
|
Author: D. Craig Brinck, PE, SE
|
|
@@ -60,7 +60,7 @@ Dynamic: summary
|
|
|
60
60
|

|
|
61
61
|
[](https://codecov.io/gh/JWock82/Pynite)
|
|
62
62
|

|
|
63
|
-

|
|
64
64
|
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JWock82/Pynite">
|
|
65
65
|

|
|
66
66
|

|
|
@@ -77,6 +77,7 @@ For a more detailed discussion on installation options and dependencies see http
|
|
|
77
77
|
* 3D static analysis of elastic structures.
|
|
78
78
|
* P-Δ analysis of frame type structures.
|
|
79
79
|
* Modal analysis of frame type structures.
|
|
80
|
+
* Nonlinear pushover analysis of steel frames.
|
|
80
81
|
* Member point loads, linearly varying distributed loads, and nodal loads are supported.
|
|
81
82
|
* Classify loads by load case and create load combinations from load cases.
|
|
82
83
|
* Produces shear, moment, and deflection results and diagrams for each member.
|
|
@@ -121,6 +122,16 @@ Here's a list of projects that use Pynite:
|
|
|
121
122
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
122
123
|
|
|
123
124
|
# What's New?
|
|
125
|
+
3.0.0
|
|
126
|
+
* 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
|
+
* 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.
|
|
128
|
+
* User's can now control the size of member plots.
|
|
129
|
+
* Improved `ShearWall.screenshots()` method. Added coupling beam plots. Added the ability to use the `VTK` renderer instead of the `pyvista` renderer for screenshots.
|
|
130
|
+
* Removed individual load combo plots from member plots when an envelope plot is requested. They cluttered the plots without adding much value.
|
|
131
|
+
|
|
132
|
+
2.4.1
|
|
133
|
+
* Fixed a bug where enveloped results did not work if both tagged and untagged combos were in a model.
|
|
134
|
+
|
|
124
135
|
2.4.0
|
|
125
136
|
* Member `max` and `min` methods now return governing load combination along with the member max/min forces.
|
|
126
137
|
* Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
|
|
@@ -144,7 +155,7 @@ v2.2.0
|
|
|
144
155
|
* Updated documentation for rendering.
|
|
145
156
|
|
|
146
157
|
v2.1.0
|
|
147
|
-
* Major speed boost and leaner memory usage: the global stiffness/mass assembly plus the nodal load and fixed-end reaction builders now use block-based vectorized writes instead of Python loops, which keeps data in contiguous NumPy buffers rather than thousands of temporary Python objects. The dense solver sees ~15-25% faster `
|
|
158
|
+
* Major speed boost and leaner memory usage: the global stiffness/mass assembly plus the nodal load and fixed-end reaction builders now use block-based vectorized writes instead of Python loops, which keeps data in contiguous NumPy buffers rather than thousands of temporary Python objects. The dense solver sees ~15-25% faster `Ke`/`M` builds in our targeted benchmarks, while sparse solver runs see ~30% faster stiffness assembly in our benchmarks.
|
|
148
159
|
* Meshes can now be regenerated with the `FEModel3D.meshes[mesh_name].generate()` command. This command removes the old mesh and replaces it with an updated one. The mesh will not remove nodes needed by other elements outside the mesh. Note that any loads applied to the old mesh will be lost when its elements and nodes are deleted. This makes meshes truly parametric.
|
|
149
160
|
* Added `FEModel3D.delete_mesh()` to help deleting old meshes properly.
|
|
150
161
|
* Renderers now automatically set the annotation size as 5% of the minimum distance between nodes. You still have the option of overiding this by setting the annotation size manually.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A simple elastic 3D structural finite element library for Python.
|
|
5
5
|
Home-page: https://github.com/JWock82/Pynite.git
|
|
6
6
|
Author: D. Craig Brinck, PE, SE
|
|
@@ -60,7 +60,7 @@ Dynamic: summary
|
|
|
60
60
|

|
|
61
61
|
[](https://codecov.io/gh/JWock82/Pynite)
|
|
62
62
|

|
|
63
|
-

|
|
64
64
|
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JWock82/Pynite">
|
|
65
65
|

|
|
66
66
|

|
|
@@ -77,6 +77,7 @@ For a more detailed discussion on installation options and dependencies see http
|
|
|
77
77
|
* 3D static analysis of elastic structures.
|
|
78
78
|
* P-Δ analysis of frame type structures.
|
|
79
79
|
* Modal analysis of frame type structures.
|
|
80
|
+
* Nonlinear pushover analysis of steel frames.
|
|
80
81
|
* Member point loads, linearly varying distributed loads, and nodal loads are supported.
|
|
81
82
|
* Classify loads by load case and create load combinations from load cases.
|
|
82
83
|
* Produces shear, moment, and deflection results and diagrams for each member.
|
|
@@ -121,6 +122,16 @@ Here's a list of projects that use Pynite:
|
|
|
121
122
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
122
123
|
|
|
123
124
|
# What's New?
|
|
125
|
+
3.0.0
|
|
126
|
+
* 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
|
+
* 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.
|
|
128
|
+
* User's can now control the size of member plots.
|
|
129
|
+
* Improved `ShearWall.screenshots()` method. Added coupling beam plots. Added the ability to use the `VTK` renderer instead of the `pyvista` renderer for screenshots.
|
|
130
|
+
* Removed individual load combo plots from member plots when an envelope plot is requested. They cluttered the plots without adding much value.
|
|
131
|
+
|
|
132
|
+
2.4.1
|
|
133
|
+
* Fixed a bug where enveloped results did not work if both tagged and untagged combos were in a model.
|
|
134
|
+
|
|
124
135
|
2.4.0
|
|
125
136
|
* Member `max` and `min` methods now return governing load combination along with the member max/min forces.
|
|
126
137
|
* Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
|
|
@@ -144,7 +155,7 @@ v2.2.0
|
|
|
144
155
|
* Updated documentation for rendering.
|
|
145
156
|
|
|
146
157
|
v2.1.0
|
|
147
|
-
* Major speed boost and leaner memory usage: the global stiffness/mass assembly plus the nodal load and fixed-end reaction builders now use block-based vectorized writes instead of Python loops, which keeps data in contiguous NumPy buffers rather than thousands of temporary Python objects. The dense solver sees ~15-25% faster `
|
|
158
|
+
* Major speed boost and leaner memory usage: the global stiffness/mass assembly plus the nodal load and fixed-end reaction builders now use block-based vectorized writes instead of Python loops, which keeps data in contiguous NumPy buffers rather than thousands of temporary Python objects. The dense solver sees ~15-25% faster `Ke`/`M` builds in our targeted benchmarks, while sparse solver runs see ~30% faster stiffness assembly in our benchmarks.
|
|
148
159
|
* Meshes can now be regenerated with the `FEModel3D.meshes[mesh_name].generate()` command. This command removes the old mesh and replaces it with an updated one. The mesh will not remove nodes needed by other elements outside the mesh. Note that any loads applied to the old mesh will be lost when its elements and nodes are deleted. This makes meshes truly parametric.
|
|
149
160
|
* Added `FEModel3D.delete_mesh()` to help deleting old meshes properly.
|
|
150
161
|
* Renderers now automatically set the annotation size as 5% of the minimum distance between nodes. You still have the option of overiding this by setting the annotation size manually.
|
|
@@ -166,6 +166,99 @@ def _check_stability(model: FEModel3D, K: NDArray[float64]) -> None:
|
|
|
166
166
|
return
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
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
|
+
"""Performs the shared first-order elastic/tension-compression-only solution path.
|
|
171
|
+
|
|
172
|
+
This helper is used by both ``FEModel3D.analyze`` and pushover preload so the normal
|
|
173
|
+
first-order workflow lives in one place.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# Import `scipy` features if the sparse solver is being used
|
|
177
|
+
if sparse == True:
|
|
178
|
+
from scipy.sparse.linalg import spsolve
|
|
179
|
+
|
|
180
|
+
# Calculate the incremental enforced displacement vector
|
|
181
|
+
Delta_D2 = D2/num_steps
|
|
182
|
+
|
|
183
|
+
# Calculate the incremental global fixed end reaction vector
|
|
184
|
+
Delta_FER1 = FER1/num_steps
|
|
185
|
+
|
|
186
|
+
# Calculate the incremental global nodal force vector
|
|
187
|
+
Delta_P1 = P1/num_steps
|
|
188
|
+
|
|
189
|
+
# Apply the load incrementally
|
|
190
|
+
load_step = 1
|
|
191
|
+
while load_step <= num_steps:
|
|
192
|
+
|
|
193
|
+
# Keep track of the number of iterations in this load step
|
|
194
|
+
iter_count = 1
|
|
195
|
+
convergence = False
|
|
196
|
+
divergence = False
|
|
197
|
+
|
|
198
|
+
# Iterate until convergence or divergence occurs
|
|
199
|
+
while convergence == False and divergence == False:
|
|
200
|
+
|
|
201
|
+
# Check for tension/compression-only divergence
|
|
202
|
+
if iter_count > max_iter:
|
|
203
|
+
divergence = True
|
|
204
|
+
raise Exception('Model diverged during tension/compression-only analysis')
|
|
205
|
+
|
|
206
|
+
# Report which load step we are on
|
|
207
|
+
if log:
|
|
208
|
+
print(f'- Analyzing load step #{str(load_step)}')
|
|
209
|
+
|
|
210
|
+
# Get the partitioned global stiffness matrix K11, K12, K21, K22
|
|
211
|
+
if sparse == True:
|
|
212
|
+
K11, K12, K21, K22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse).tocsr(), D1_indices, D2_indices)
|
|
213
|
+
else:
|
|
214
|
+
K11, K12, K21, K22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
|
|
215
|
+
|
|
216
|
+
if K11.shape == (0, 0):
|
|
217
|
+
# All displacements are known, so Delta_D1 is an empty vector
|
|
218
|
+
Delta_D1 = []
|
|
219
|
+
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.')
|
|
233
|
+
|
|
234
|
+
# Store or sum the calculated displacements to the model and the nodes in the model
|
|
235
|
+
if load_step == 1:
|
|
236
|
+
_store_displacements(model, Delta_D1, Delta_D2, D1_indices, D2_indices, model.load_combos[combo_name])
|
|
237
|
+
else:
|
|
238
|
+
_sum_displacements(model, Delta_D1, Delta_D2, D1_indices, D2_indices, model.load_combos[combo_name])
|
|
239
|
+
|
|
240
|
+
# Check for tension/compression-only convergence at this load step
|
|
241
|
+
convergence = _check_TC_convergence(model, combo_name, log=log, spring_tolerance=spring_tolerance, member_tolerance=member_tolerance)
|
|
242
|
+
|
|
243
|
+
if convergence == False:
|
|
244
|
+
|
|
245
|
+
if log:
|
|
246
|
+
print(f'- Undoing load step #{load_step} due to failed convergence.')
|
|
247
|
+
|
|
248
|
+
# Undo the latest analysis step to prepare for re-analysis of the load step
|
|
249
|
+
_sum_displacements(model, -Delta_D1, -Delta_D2, D1_indices, D2_indices, model.load_combos[combo_name])
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
# Move on to the next load step
|
|
253
|
+
load_step += 1
|
|
254
|
+
|
|
255
|
+
# Keep track of the number of tension/compression only iterations
|
|
256
|
+
iter_count += 1
|
|
257
|
+
|
|
258
|
+
# Flag the model as solved
|
|
259
|
+
model.solution = 'Nonlinear TC'
|
|
260
|
+
|
|
261
|
+
|
|
169
262
|
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:
|
|
170
263
|
"""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.
|
|
171
264
|
|
|
@@ -219,7 +312,7 @@ def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArr
|
|
|
219
312
|
# Calculate the partitioned initial stiffness matrices. These matrices must be recalculated on each T/C iteration due to tension/compression-only members deactivating or reactivating.
|
|
220
313
|
if log:
|
|
221
314
|
print('- Calculating initial stiffness matrix')
|
|
222
|
-
K11, K12, K21, K22 = _partition(model, model.
|
|
315
|
+
K11, K12, K21, K22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
|
|
223
316
|
|
|
224
317
|
# The initial stiffness matrices are in `coo` format from construction. They will be converted to `csr` format for efficient mathematical operations.
|
|
225
318
|
K11 = K11.tocsr()
|
|
@@ -247,13 +340,13 @@ def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArr
|
|
|
247
340
|
if solution_step == 1:
|
|
248
341
|
|
|
249
342
|
# Calculate the partitioned initial stiffness matrices. These matrices must be recalculated on each T/C iteration due to tension/compression-only members deactivating or reactivating.
|
|
250
|
-
K11, K12, K21, K22 = _partition(model, model.
|
|
343
|
+
K11, K12, K21, K22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
|
|
251
344
|
|
|
252
345
|
# Check if we are ready to calculate the geometric stiffness
|
|
253
346
|
if solution_step == 2:
|
|
254
347
|
|
|
255
348
|
# After the first iteration, the geometric stiffness matrix will be added to the linear elastic stiffness matrix.
|
|
256
|
-
Kg11, Kg12, Kg21, Kg22 = _partition(model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
349
|
+
Kg11, Kg12, Kg21, Kg22 = _partition(model, model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
257
350
|
|
|
258
351
|
K11 = K11 + Kg11
|
|
259
352
|
K12 = K12 + Kg12
|
|
@@ -311,28 +404,41 @@ def _PDelta(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArr
|
|
|
311
404
|
model.solution = 'P-Delta'
|
|
312
405
|
|
|
313
406
|
|
|
314
|
-
def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num: int,
|
|
407
|
+
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:
|
|
315
408
|
|
|
316
409
|
# Run at least one iteration
|
|
317
410
|
run_step = True
|
|
411
|
+
iter_count = 0
|
|
412
|
+
tc_changed_last = False
|
|
413
|
+
reversal_locations_last = []
|
|
318
414
|
|
|
319
415
|
# Run/rerun the load step until convergence occurs
|
|
320
416
|
while run_step == True:
|
|
321
417
|
|
|
418
|
+
iter_count += 1
|
|
419
|
+
if iter_count > max_iter:
|
|
420
|
+
reasons = []
|
|
421
|
+
if tc_changed_last:
|
|
422
|
+
reasons.append('tension/compression-only status changes')
|
|
423
|
+
if reversal_locations_last:
|
|
424
|
+
reasons.append('plastic load reversal at ' + ', '.join(reversal_locations_last))
|
|
425
|
+
if not reasons:
|
|
426
|
+
reasons.append('non-convergent step validation checks')
|
|
427
|
+
raise RuntimeError(
|
|
428
|
+
f"Pushover load step {step_num} for load combination '{combo_name}' did not converge "
|
|
429
|
+
f"after {max_iter} retries ({'; '.join(reasons)}). "
|
|
430
|
+
"Reduce the pushover increment, relax nonlinear demands, or increase max_iter."
|
|
431
|
+
)
|
|
432
|
+
|
|
322
433
|
# Calculate the partitioned global stiffness matrices
|
|
323
434
|
# Sparse solver
|
|
324
435
|
if sparse == True:
|
|
325
436
|
|
|
326
437
|
from scipy.sparse.linalg import spsolve
|
|
327
438
|
|
|
328
|
-
# Calculate the
|
|
439
|
+
# Calculate the elastic stiffness matrix
|
|
329
440
|
if log: print('- Calculating elastic stiffness matrix [Ke]')
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# Calculate the geometric stiffness matrix
|
|
333
|
-
# The `combo_name` variable in the code below is not the name of the pushover load combination. Rather it is the name of the primary combination that the pushover load will be added to. Axial loads used to develop Kg are calculated from the displacements stored in `combo_name`.
|
|
334
|
-
if log: print('- Calculating geometric stiffness matrix [Kg]')
|
|
335
|
-
Kg11, Kg12, Kg21, Kg22 = _partition(model, model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
441
|
+
Ke11, Ke12, Ke21, Ke22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
|
|
336
442
|
|
|
337
443
|
# Calculate the stiffness reduction matrix
|
|
338
444
|
if log: print('- Calculating plastic reduction matrix [Km]')
|
|
@@ -341,31 +447,44 @@ def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num:
|
|
|
341
447
|
# The stiffness matrices are in `coo` format from construction. They will be
|
|
342
448
|
# converted to `csr` format for efficient operations. The `+` operator
|
|
343
449
|
# performs matrix addition on `csr` matrices.
|
|
344
|
-
K11 =
|
|
345
|
-
K12 =
|
|
346
|
-
K21 =
|
|
347
|
-
K22 =
|
|
450
|
+
K11 = Ke11.tocsr() + Km11.tocsr()
|
|
451
|
+
K12 = Ke12.tocsr() + Km12.tocsr()
|
|
452
|
+
# K21 = Ke21.tocsr() + Km21.tocsr() # This matrix is not used
|
|
453
|
+
# K22 = Ke22.tocsr() + Km22.tocsr() # This matrix is not used
|
|
454
|
+
|
|
455
|
+
if P_Delta:
|
|
456
|
+
# The `combo_name` variable in the code below is not the name of the pushover load combination. Rather it is the name of the primary combination that the pushover load will be added to. Axial loads used to develop Kg are calculated from the displacements stored in `combo_name`.
|
|
457
|
+
if log: print('- Calculating geometric stiffness matrix [Kg]')
|
|
458
|
+
Kg11, Kg12, Kg21, Kg22 = _partition(model, model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
459
|
+
K11 = K11 + Kg11.tocsr()
|
|
460
|
+
K12 = K12 + Kg12.tocsr()
|
|
461
|
+
# K21 = K21 + Kg21.tocsr() # This matrix is not used
|
|
462
|
+
# K22 = K22 + Kg22.tocsr() # This matrix is not used
|
|
348
463
|
|
|
349
464
|
# Dense solver
|
|
350
465
|
else:
|
|
351
466
|
|
|
352
467
|
# Initial stiffness matrix
|
|
353
468
|
if log: print('- Calculating elastic stiffness matrix [Ke]')
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
# Geometric stiffness matrix
|
|
357
|
-
# The `combo_name` variable in the code below is not the name of the pushover load combination. Rather it is the name of the primary combination that the pushover load will be added to. Axial loads used to develop Kg are calculated from the displacements stored in `combo_name`.
|
|
358
|
-
if log: print('Calculating geometric stiffness matrix [Kg]')
|
|
359
|
-
Kg11, Kg12, Kg21, Kg22 = _partition(model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
469
|
+
Ke11, Ke12, Ke21, Ke22 = _partition(model, model.Ke(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
|
|
360
470
|
|
|
361
471
|
# Calculate the stiffness reduction matrix
|
|
362
472
|
if log: print('Calculating plastic reduction matrix [Km]')
|
|
363
473
|
Km11, Km12, Km21, Km22 = _partition(model, model.Km(combo_name, push_combo, step_num, log, sparse), D1_indices, D2_indices)
|
|
364
474
|
|
|
365
|
-
K11 =
|
|
366
|
-
K12 =
|
|
367
|
-
K21 =
|
|
368
|
-
K22 =
|
|
475
|
+
K11 = Ke11 + Km11
|
|
476
|
+
K12 = Ke12 + Km12
|
|
477
|
+
# K21 = Ke21 + Km21 # This matrix is not used
|
|
478
|
+
# K22 = Ke22 + Km22 # This matrix is not used
|
|
479
|
+
|
|
480
|
+
if P_Delta:
|
|
481
|
+
# The `combo_name` variable in the code below is not the name of the pushover load combination. Rather it is the name of the primary combination that the pushover load will be added to. Axial loads used to develop Kg are calculated from the displacements stored in `combo_name`.
|
|
482
|
+
if log: print('Calculating geometric stiffness matrix [Kg]')
|
|
483
|
+
Kg11, Kg12, Kg21, Kg22 = _partition(model, model.Kg(combo_name, log, sparse, False), D1_indices, D2_indices)
|
|
484
|
+
K11 = K11 + Kg11
|
|
485
|
+
K12 = K12 + Kg12
|
|
486
|
+
# K21 = K21 + Kg21 # This matrix is not used
|
|
487
|
+
# K22 = K22 + Kg22 # This matrix is not used
|
|
369
488
|
|
|
370
489
|
# Calculate the changes to the global displacement vector
|
|
371
490
|
if log: print('- Calculating changes to the global displacement vector')
|
|
@@ -378,81 +497,160 @@ def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num:
|
|
|
378
497
|
if sparse == True:
|
|
379
498
|
# The partitioned stiffness matrix is already in `csr` format. The `@`
|
|
380
499
|
# operator performs matrix multiplication on sparse matrices.
|
|
381
|
-
Delta_D1 = spsolve(K11.tocsr(), subtract(subtract(
|
|
500
|
+
Delta_D1 = spsolve(K11.tocsr(), subtract(subtract(Delta_P1, Delta_FER1), K12.tocsr() @ D2))
|
|
382
501
|
Delta_D1 = Delta_D1.reshape(len(Delta_D1), 1)
|
|
383
502
|
else:
|
|
384
503
|
# The partitioned stiffness matrix is in `csr` format. It will be
|
|
385
504
|
# converted to a 2D dense array for mathematical operations.
|
|
386
|
-
Delta_D1 = solve(K11, subtract(subtract(
|
|
505
|
+
Delta_D1 = solve(K11, subtract(subtract(Delta_P1, Delta_FER1), K12 @ D2))
|
|
387
506
|
|
|
388
507
|
except:
|
|
389
508
|
# Return out of the method if 'K' is singular and provide an error message
|
|
390
509
|
raise ValueError('The structure is unstable. Unable to proceed any further with analysis.')
|
|
391
510
|
|
|
392
|
-
#
|
|
393
|
-
|
|
511
|
+
# Tentatively apply the load step so step-validation checks see the current state.
|
|
512
|
+
_sum_displacements(model, Delta_D1, D2, D1_indices, D2_indices, model.load_combos[combo_name])
|
|
394
513
|
|
|
395
|
-
# Assume no need to rerun this load step
|
|
514
|
+
# Assume no need to rerun this load step until one of the validation checks says otherwise.
|
|
396
515
|
run_step = False
|
|
516
|
+
tc_changed = False
|
|
517
|
+
reversal = False
|
|
518
|
+
reversal_locations = []
|
|
519
|
+
|
|
520
|
+
# Check for tension/compression-only status changes caused by this tentative load step.
|
|
521
|
+
if _check_TC_convergence(model, combo_name, log, spring_tolerance=tol, member_tolerance=tol) == False:
|
|
522
|
+
tc_changed = True
|
|
523
|
+
run_step = True
|
|
524
|
+
|
|
525
|
+
# Unpartition the displacement results from the analysis step for the `lamb` calculations below
|
|
526
|
+
Delta_D = _unpartition(model, Delta_D1, D2, D1_indices, D2_indices)
|
|
527
|
+
Delta_FER = _unpartition(model, Delta_FER1, Delta_FER2, D1_indices, D2_indices)
|
|
397
528
|
|
|
398
|
-
# Step through each member in the model and check for plastic load reversal
|
|
529
|
+
# Step through each member in the model and check for plastic load reversal.
|
|
399
530
|
for phys_member in model.members.values():
|
|
400
531
|
|
|
401
532
|
for sub_member in phys_member.sub_members.values():
|
|
402
533
|
|
|
403
|
-
|
|
534
|
+
lamb = sub_member.lamb(Delta_D, combo_name, push_combo, step_num)
|
|
404
535
|
|
|
405
536
|
# Check for plastic load reversal at the i-node in this load step
|
|
406
|
-
if
|
|
537
|
+
if lamb[0, 0] < 0:
|
|
407
538
|
|
|
408
539
|
# Flag the load step for reanalysis
|
|
409
540
|
sub_member.i_reversal = True
|
|
541
|
+
reversal = True
|
|
410
542
|
run_step = True
|
|
411
|
-
|
|
543
|
+
reversal_locations.append(f'{sub_member.name} i-node')
|
|
544
|
+
if log: print(f'- Plastic load reversal encountered at member {sub_member.name} i-node')
|
|
412
545
|
|
|
413
546
|
else:
|
|
414
547
|
|
|
415
548
|
sub_member.i_reversal = False
|
|
416
549
|
|
|
417
550
|
# Check for plastic load reversal at the j-node in this load step
|
|
418
|
-
if
|
|
551
|
+
if lamb[1, 0] < 0:
|
|
419
552
|
|
|
420
553
|
# Flag the load step for reanalysis
|
|
421
554
|
sub_member.j_reversal = True
|
|
555
|
+
reversal = True
|
|
422
556
|
run_step = True
|
|
423
|
-
|
|
557
|
+
reversal_locations.append(f'{sub_member.name} j-node')
|
|
558
|
+
if log: print(f'- Plastic load reversal encountered at member {sub_member.name} j-node')
|
|
424
559
|
|
|
425
560
|
else:
|
|
426
561
|
|
|
427
562
|
sub_member.j_reversal = False
|
|
428
563
|
|
|
429
|
-
#
|
|
430
|
-
if run_step ==
|
|
431
|
-
|
|
432
|
-
|
|
564
|
+
# Check if this iteration was valid
|
|
565
|
+
if run_step == False:
|
|
566
|
+
|
|
567
|
+
# Step through each physical member in the model
|
|
568
|
+
for phys_member in model.members.values():
|
|
433
569
|
|
|
434
|
-
|
|
435
|
-
|
|
570
|
+
# Step through each sub-member of the physical member
|
|
571
|
+
for sub_member in phys_member.sub_members.values():
|
|
436
572
|
|
|
573
|
+
# Extract the change in the global displacement and fixed end reaction vectors for this member from the full global vectors. The `reshape` method is used to ensure these are column vectors for the matrix operations below.
|
|
574
|
+
Delta_D_member = array([Delta_D[sub_member.i_node.ID*6 + 0, 0],
|
|
575
|
+
Delta_D[sub_member.i_node.ID*6 + 1, 0],
|
|
576
|
+
Delta_D[sub_member.i_node.ID*6 + 2, 0],
|
|
577
|
+
Delta_D[sub_member.i_node.ID*6 + 3, 0],
|
|
578
|
+
Delta_D[sub_member.i_node.ID*6 + 4, 0],
|
|
579
|
+
Delta_D[sub_member.i_node.ID*6 + 5, 0],
|
|
580
|
+
Delta_D[sub_member.j_node.ID*6 + 0, 0],
|
|
581
|
+
Delta_D[sub_member.j_node.ID*6 + 1, 0],
|
|
582
|
+
Delta_D[sub_member.j_node.ID*6 + 2, 0],
|
|
583
|
+
Delta_D[sub_member.j_node.ID*6 + 3, 0],
|
|
584
|
+
Delta_D[sub_member.j_node.ID*6 + 4, 0],
|
|
585
|
+
Delta_D[sub_member.j_node.ID*6 + 5, 0]]).reshape(12, 1)
|
|
586
|
+
|
|
587
|
+
Delta_FER_member = array([Delta_FER[sub_member.i_node.ID*6 + 0, 0],
|
|
588
|
+
Delta_FER[sub_member.i_node.ID*6 + 1, 0],
|
|
589
|
+
Delta_FER[sub_member.i_node.ID*6 + 2, 0],
|
|
590
|
+
Delta_FER[sub_member.i_node.ID*6 + 3, 0],
|
|
591
|
+
Delta_FER[sub_member.i_node.ID*6 + 4, 0],
|
|
592
|
+
Delta_FER[sub_member.i_node.ID*6 + 5, 0],
|
|
593
|
+
Delta_FER[sub_member.j_node.ID*6 + 0, 0],
|
|
594
|
+
Delta_FER[sub_member.j_node.ID*6 + 1, 0],
|
|
595
|
+
Delta_FER[sub_member.j_node.ID*6 + 2, 0],
|
|
596
|
+
Delta_FER[sub_member.j_node.ID*6 + 3, 0],
|
|
597
|
+
Delta_FER[sub_member.j_node.ID*6 + 4, 0],
|
|
598
|
+
Delta_FER[sub_member.j_node.ID*6 + 5, 0]]).reshape(12, 1)
|
|
599
|
+
|
|
600
|
+
# Convert the global displacement and fixed end reaction vectors to local member vectors
|
|
601
|
+
Delta_d = sub_member.T() @ Delta_D_member
|
|
602
|
+
Delta_fer = sub_member.T() @ Delta_FER_member
|
|
603
|
+
|
|
604
|
+
# Calculate change in the local end force vector for this load step
|
|
605
|
+
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]
|
|
437
619
|
|
|
438
|
-
|
|
439
|
-
|
|
620
|
+
else:
|
|
621
|
+
|
|
622
|
+
tc_changed_last = tc_changed
|
|
623
|
+
reversal_locations_last = list(dict.fromkeys(reversal_locations))
|
|
624
|
+
|
|
625
|
+
# Undo the tentative load step
|
|
626
|
+
_sum_displacements(model, -Delta_D1, D2, D1_indices, D2_indices, model.load_combos[combo_name])
|
|
627
|
+
if log:
|
|
628
|
+
if tc_changed and reversal:
|
|
629
|
+
print('- Restarting load step due to tension/compression-only status change and plastic load reversal')
|
|
630
|
+
elif tc_changed:
|
|
631
|
+
print('- Restarting load step due to tension/compression-only status change')
|
|
632
|
+
else:
|
|
633
|
+
print('- Restarting load step due to plastic load reversal')
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _unpartition(model: FEModel3D, V1: NDArray[float64], V2: NDArray[float64], V1_indices: List[int], V2_indices: List[int]) -> NDArray[float64]:
|
|
637
|
+
"""Unpartitions a vector and returns it as a global vector including all dofs.
|
|
440
638
|
|
|
441
639
|
:param model: The finite element model being evaluated
|
|
442
640
|
:type model: FEModel3D
|
|
443
|
-
:param
|
|
444
|
-
:type
|
|
445
|
-
:param
|
|
446
|
-
:type
|
|
447
|
-
:param
|
|
448
|
-
:type
|
|
449
|
-
:param
|
|
450
|
-
:type
|
|
451
|
-
:return:
|
|
641
|
+
:param V1: An array of vector terms corresponding to the unrestrained degrees of freedom.
|
|
642
|
+
:type V1: array
|
|
643
|
+
:param V2: An array of vector terms corresponding to the degrees of freedom with boundary conditions.
|
|
644
|
+
:type V2: array
|
|
645
|
+
:param V1_indices: A list of the degree of freedom indices for each term in V1
|
|
646
|
+
:type V1_indices: list
|
|
647
|
+
:param V2_indices: A list of the degree of freedom indices for each term in V2
|
|
648
|
+
:type V2_indices: list
|
|
649
|
+
:return: Expanded vector
|
|
452
650
|
:rtype: array
|
|
453
651
|
"""
|
|
454
652
|
|
|
455
|
-
|
|
653
|
+
V = zeros((len(model.nodes)*6, 1))
|
|
456
654
|
|
|
457
655
|
# Step through each node in the model
|
|
458
656
|
for node in model.nodes.values():
|
|
@@ -460,16 +658,16 @@ def _unpartition_disp(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float6
|
|
|
460
658
|
# Step through each degree of freedom at the node
|
|
461
659
|
for i in range(6):
|
|
462
660
|
|
|
463
|
-
# Check if the dof is in the list of
|
|
464
|
-
if node.ID*6 + i in
|
|
465
|
-
# Get the
|
|
466
|
-
|
|
661
|
+
# Check if the dof is in the list of known vector terms
|
|
662
|
+
if node.ID*6 + i in V2_indices:
|
|
663
|
+
# Get the vector term for the bounded dof
|
|
664
|
+
V[(node.ID*6 + i, 0)] = V2[V2_indices.index(node.ID*6 + i), 0]
|
|
467
665
|
else:
|
|
468
|
-
# Get the
|
|
469
|
-
|
|
666
|
+
# Get the vector term for the unbounded dof
|
|
667
|
+
V[(node.ID*6 + i, 0)] = V1[V1_indices.index(node.ID*6 + i), 0]
|
|
470
668
|
|
|
471
|
-
# Return the
|
|
472
|
-
return
|
|
669
|
+
# Return the vector
|
|
670
|
+
return V
|
|
473
671
|
|
|
474
672
|
|
|
475
673
|
def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], combo: LoadCombo) -> None:
|
|
@@ -490,7 +688,7 @@ def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[flo
|
|
|
490
688
|
"""
|
|
491
689
|
|
|
492
690
|
# The raw results from the solver are partitioned. Unpartition them.
|
|
493
|
-
D =
|
|
691
|
+
D = _unpartition(model, D1, D2, D1_indices, D2_indices)
|
|
494
692
|
|
|
495
693
|
if combo != None:
|
|
496
694
|
|
|
@@ -526,7 +724,7 @@ def _sum_displacements(model: FEModel3D, Delta_D1: NDArray[float64], Delta_D2: N
|
|
|
526
724
|
"""
|
|
527
725
|
|
|
528
726
|
# The raw results from the solver are partitioned. Unpartition them.
|
|
529
|
-
Delta_D =
|
|
727
|
+
Delta_D = _unpartition(model, Delta_D1, Delta_D2, D1_indices, D2_indices)
|
|
530
728
|
|
|
531
729
|
# Sum the load step's global displacement vector with the model's global displacement vector
|
|
532
730
|
model._D[combo.name] += Delta_D
|
|
@@ -541,6 +739,7 @@ def _sum_displacements(model: FEModel3D, Delta_D1: NDArray[float64], Delta_D2: N
|
|
|
541
739
|
node.RY[combo.name] += Delta_D[node.ID*6 + 4, 0]
|
|
542
740
|
node.RZ[combo.name] += Delta_D[node.ID*6 + 5, 0]
|
|
543
741
|
|
|
742
|
+
|
|
544
743
|
def _check_TC_convergence(model: FEModel3D, combo_name: str = "Combo 1", log: bool = True, spring_tolerance: float = 0, member_tolerance: float = 0) -> bool:
|
|
545
744
|
"""Checks for convergence in tension-only and compression-only analysis.
|
|
546
745
|
|
|
@@ -4,6 +4,9 @@ from Pynite.BeamSegZ import BeamSegZ
|
|
|
4
4
|
# %%
|
|
5
5
|
class BeamSegY(BeamSegZ):
|
|
6
6
|
|
|
7
|
+
def __repr__(self) -> str:
|
|
8
|
+
return f"BeamSegY(x1={self.x1}, x2={self.x2})"
|
|
9
|
+
|
|
7
10
|
# Returns the moment at a location on the segment
|
|
8
11
|
def moment(self, x: float, P_delta: bool = False) -> float:
|
|
9
12
|
'''
|
|
@@ -20,15 +23,16 @@ class BeamSegY(BeamSegZ):
|
|
|
20
23
|
P1 = self.P1
|
|
21
24
|
w1 = self.w1
|
|
22
25
|
w2 = self.w2
|
|
23
|
-
L = self.
|
|
26
|
+
L = self.length()
|
|
24
27
|
|
|
25
28
|
# M = M1 + V1*x + w1*x**2/2 + x**3*(-w1 + w2)/(6*L)
|
|
26
29
|
M = -M1 - V1*x - w1*x**2/2 - x**3*(-w1 + w2)/(6*L)
|
|
27
30
|
|
|
28
31
|
if P_delta == True:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
delta_1 = self.delta1
|
|
34
|
+
delta_x = self.deflection(x, P_delta)
|
|
35
|
+
M += P1*(delta_x - delta_1)
|
|
32
36
|
|
|
33
37
|
return M
|
|
34
38
|
|
|
@@ -50,7 +54,7 @@ class BeamSegY(BeamSegZ):
|
|
|
50
54
|
w2 = self.w2
|
|
51
55
|
theta_1 = self.theta1
|
|
52
56
|
|
|
53
|
-
L = self.
|
|
57
|
+
L = self.length()
|
|
54
58
|
EI = self.EI
|
|
55
59
|
|
|
56
60
|
if P_delta == True:
|
|
@@ -70,7 +74,7 @@ class BeamSegY(BeamSegZ):
|
|
|
70
74
|
w2 = self.w2
|
|
71
75
|
theta_1 = self.theta1
|
|
72
76
|
delta_1 = self.delta1
|
|
73
|
-
L = self.
|
|
77
|
+
L = self.length()
|
|
74
78
|
EI = self.EI
|
|
75
79
|
|
|
76
80
|
# Check if a P-delta solution is requested
|
|
@@ -90,7 +94,7 @@ class BeamSegY(BeamSegZ):
|
|
|
90
94
|
w1 = self.w1
|
|
91
95
|
w2 = self.w2
|
|
92
96
|
V1 = self.V1
|
|
93
|
-
L = self.
|
|
97
|
+
L = self.length()
|
|
94
98
|
|
|
95
99
|
# Find the quadratic equation parameters
|
|
96
100
|
a = (w2-w1)/(2*L)
|
|
@@ -135,7 +139,7 @@ class BeamSegY(BeamSegZ):
|
|
|
135
139
|
w1 = self.w1
|
|
136
140
|
w2 = self.w2
|
|
137
141
|
V1 = self.V1
|
|
138
|
-
L = self.
|
|
142
|
+
L = self.length()
|
|
139
143
|
|
|
140
144
|
# Find the quadratic equation parameters
|
|
141
145
|
a = (w2-w1)/(2*L)
|