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.
Files changed (35) hide show
  1. {pynitefea-2.4.0 → pynitefea-3.0.0}/PKG-INFO +14 -3
  2. {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/PKG-INFO +14 -3
  3. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Analysis.py +263 -64
  4. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/BeamSegY.py +12 -8
  5. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/BeamSegZ.py +38 -28
  6. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/FEModel3D.py +296 -149
  7. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/LoadCombo.py +3 -0
  8. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/MatFoundation.py +3 -0
  9. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Material.py +4 -1
  10. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Member3D.py +274 -147
  11. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Mesh.py +27 -0
  12. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Node3D.py +3 -0
  13. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/PhysMember.py +66 -43
  14. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Plate3D.py +27 -24
  15. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Quad3D.py +32 -29
  16. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Rendering.py +128 -20
  17. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Section.py +28 -17
  18. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/ShearWall.py +38 -12
  19. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Spring3D.py +12 -8
  20. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Tri3D.py +27 -24
  21. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/VTKWriter.py +3 -0
  22. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Visualization.py +133 -0
  23. {pynitefea-2.4.0 → pynitefea-3.0.0}/README.md +13 -2
  24. {pynitefea-2.4.0 → pynitefea-3.0.0}/setup.py +1 -1
  25. {pynitefea-2.4.0 → pynitefea-3.0.0}/LICENSE +0 -0
  26. {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
  27. {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
  28. {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/requires.txt +0 -0
  29. {pynitefea-2.4.0 → pynitefea-3.0.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
  30. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/FixedEndReactions.py +0 -0
  31. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/MainStyleSheet.css +0 -0
  32. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Report_Template.html +0 -0
  33. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/Reporting.py +0 -0
  34. {pynitefea-2.4.0 → pynitefea-3.0.0}/Pynite/__init__.py +0 -0
  35. {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: 2.4.0
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
  ![Build Status](https://github.com/JWock82/Pynite/actions/workflows/build-and-test.yml/badge.svg)
61
61
  [![codecov](https://codecov.io/gh/JWock82/Pynite/branch/main/graph/badge.svg?token=ZH18US3A7P)](https://codecov.io/gh/JWock82/Pynite)
62
62
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyniteFEA)
63
- ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyNiteFEA?cacheSeconds=86400)
63
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyNiteFEA?cacheSeconds=172800)
64
64
  <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JWock82/Pynite">
65
65
  ![GitHub last commit](https://img.shields.io/github/last-commit/JWock82/Pynite)
66
66
  ![GitHub](https://img.shields.io/github/license/JWock82/Pynite)
@@ -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-&Delta; 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 `K`/`M` builds in our targeted benchmarks, while sparse solver runs see ~30% faster stiffness assembly in our benchmarks.
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: 2.4.0
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
  ![Build Status](https://github.com/JWock82/Pynite/actions/workflows/build-and-test.yml/badge.svg)
61
61
  [![codecov](https://codecov.io/gh/JWock82/Pynite/branch/main/graph/badge.svg?token=ZH18US3A7P)](https://codecov.io/gh/JWock82/Pynite)
62
62
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyniteFEA)
63
- ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyNiteFEA?cacheSeconds=86400)
63
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyNiteFEA?cacheSeconds=172800)
64
64
  <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JWock82/Pynite">
65
65
  ![GitHub last commit](https://img.shields.io/github/last-commit/JWock82/Pynite)
66
66
  ![GitHub](https://img.shields.io/github/license/JWock82/Pynite)
@@ -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-&Delta; 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 `K`/`M` builds in our targeted benchmarks, while sparse solver runs see ~30% faster stiffness assembly in our benchmarks.
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.K(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
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.K(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
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, 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) -> None:
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 initial stiffness matrix
439
+ # Calculate the elastic stiffness matrix
329
440
  if log: print('- Calculating elastic stiffness matrix [Ke]')
330
- K11, K12, K21, K22 = _partition(model, model.K(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
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 = K11.tocsr() + Kg11.tocsr() + Km11.tocsr()
345
- K12 = K12.tocsr() + Kg12.tocsr() + Km12.tocsr()
346
- K21 = K21.tocsr() + Kg21.tocsr() + Km21.tocsr()
347
- K22 = K22.tocsr() + Kg22.tocsr() + Km22.tocsr()
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
- K11, K12, K21, K22 = _partition(model, model.K(combo_name, log, check_stability, sparse), D1_indices, D2_indices)
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 = K11 + Kg11 + Km11
366
- K12 = K12 + Kg12 + Km12
367
- K21 = K21 + Kg21 + Km21
368
- K22 = K22 + Kg22 + Km22
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(P1, FER1), K12.tocsr() @ D2))
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(P1, FER1), matmul(K12, D2)))
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
- # Unpartition the displacement results from the analysis step
393
- Delta_D = _unpartition_disp(model, Delta_D1, D2, D1_indices, D2_indices)
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 due to plastic load reversal until we prove it otherwise
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
- # print(f'Member {sub_member.name} lambda = {sub_member.lamb(Delta_D, combo_name, push_combo, step_num)}')
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 sub_member.lamb(Delta_D, combo_name, push_combo, step_num)[0, 0] < 0:
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
- print(f'-Plastic load reversal encountered at member {sub_member.name} i-node')
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 sub_member.lamb(Delta_D, combo_name, push_combo, step_num)[1, 0] < 0:
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
- print(f'-Plastic load reversal encountered at member {sub_member.name} j-node')
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
- # Undo the last loadstep if plastic load reversal was discovered. We'll rerun it with the corresponding gradients set to zero vectors.
430
- if run_step == True:
431
- _sum_displacements(model, -Delta_D1, D2, D1_indices, D2_indices, model.load_combos[combo_name])
432
- if log: print('- Restarting load step due to plastic load reversal')
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
- # Sum the calculated displacements
435
- _sum_displacements(model, Delta_D1, D2, D1_indices, D2_indices, model.load_combos[combo_name])
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
- def _unpartition_disp(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int]) -> NDArray[float64]:
439
- """Unpartitions displacements from the solver and returns them as a global displacement vector
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 D1: An array of calculated displacements
444
- :type D1: array
445
- :param D2: An array of enforced displacements
446
- :type D2: array
447
- :param D1_indices: A list of the degree of freedom indices for each displacement in D1
448
- :type D1_indices: list
449
- :param D2_indices: A list of the degree of freedom indices for each displacement in D2
450
- :type D2_indices: list
451
- :return: Global displacement matrix
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
- D = zeros((len(model.nodes)*6, 1))
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 enforced displacements
464
- if node.ID*6 + i in D2_indices:
465
- # Get the enforced displacement
466
- D[(node.ID*6 + i, 0)] = D2[D2_indices.index(node.ID*6 + i), 0]
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 calculated displacement
469
- D[(node.ID*6 + i, 0)] = D1[D1_indices.index(node.ID*6 + i), 0]
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 displacement vector
472
- return D
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 = _unpartition_disp(model, D1, D2, D1_indices, D2_indices)
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 = _unpartition_disp(model, Delta_D1, Delta_D2, D1_indices, D2_indices)
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.Length()
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
- delta1 = self.delta1
30
- delta = self.deflection(x)
31
- M += P1*(delta - delta1)
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.Length()
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.Length()
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.Length()
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.Length()
142
+ L = self.length()
139
143
 
140
144
  # Find the quadratic equation parameters
141
145
  a = (w2-w1)/(2*L)