PyNiteFEA 2.2.0__tar.gz → 2.3.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.2.0 → pynitefea-2.3.0}/PKG-INFO +16 -1
- {pynitefea-2.2.0 → pynitefea-2.3.0}/PyNiteFEA.egg-info/PKG-INFO +16 -1
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Analysis.py +6 -6
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/FEModel3D.py +20 -1
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/MatFoundation.py +5 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Member3D.py +178 -79
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Mesh.py +15 -2
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/PhysMember.py +158 -68
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Rendering.py +165 -26
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/ShearWall.py +41 -2
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/VTKWriter.py +28 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Visualization.py +195 -22
- {pynitefea-2.2.0 → pynitefea-2.3.0}/README.md +15 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/setup.py +1 -1
- {pynitefea-2.2.0 → pynitefea-2.3.0}/LICENSE +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/PyNiteFEA.egg-info/requires.txt +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/BeamSegY.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/BeamSegZ.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/FixedEndReactions.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/LoadCombo.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/MainStyleSheet.css +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Material.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Node3D.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Plate3D.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Quad3D.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Report_Template.html +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Reporting.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Section.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Spring3D.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/Tri3D.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/Pynite/__init__.py +0 -0
- {pynitefea-2.2.0 → pynitefea-2.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -121,6 +121,21 @@ Here's a list of projects that use Pynite:
|
|
|
121
121
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
122
122
|
|
|
123
123
|
# What's New?
|
|
124
|
+
v2.3.0
|
|
125
|
+
* Added enveloping to member diagrams. Set `combo_name` to a list of combo tags and the member diagram will show an envelope of all the combos that have any of those tags.
|
|
126
|
+
* Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
|
|
127
|
+
* Bug fix: Added shear wall and mat foundation load cases to `FEModel3D.load_cases()`. The list of load cases was sometimes incomplete if these meshes had not yet been generated.
|
|
128
|
+
* Added bending moment end releases to visualizations.
|
|
129
|
+
* Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
|
|
130
|
+
* Rectangular meshes now filter out user defined control points that erroneously lie outside their boudaries.
|
|
131
|
+
|
|
132
|
+
v2.2.1
|
|
133
|
+
* Normalized member force diagrams across the entire model for visual clarity.
|
|
134
|
+
* Made mesh auto-regeneration smarter and more efficient, targeting only meshes that have been altered since the last run.
|
|
135
|
+
* Added `ShearWall` mesh auto-regeneration. Whenever a model is solved any old or outdated `ShearWall` meshes are automatically removed and replaced by a rebuild.
|
|
136
|
+
* Optimized auto-calculated annotation sizing: The new automatic annotation size calculation feature now uses vectorized NumPy operations and caching for efficient rendering of large models with hundreds or thousands of elements.
|
|
137
|
+
* Adjusted member diagram label offsets for clarity.
|
|
138
|
+
|
|
124
139
|
v2.2.0
|
|
125
140
|
* Added member diagrams feature for easy visualization of shear, moment, axial, and torsion diagrams along members.
|
|
126
141
|
* Updated documentation for rendering.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNiteFEA
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -121,6 +121,21 @@ Here's a list of projects that use Pynite:
|
|
|
121
121
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
122
122
|
|
|
123
123
|
# What's New?
|
|
124
|
+
v2.3.0
|
|
125
|
+
* Added enveloping to member diagrams. Set `combo_name` to a list of combo tags and the member diagram will show an envelope of all the combos that have any of those tags.
|
|
126
|
+
* Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
|
|
127
|
+
* Bug fix: Added shear wall and mat foundation load cases to `FEModel3D.load_cases()`. The list of load cases was sometimes incomplete if these meshes had not yet been generated.
|
|
128
|
+
* Added bending moment end releases to visualizations.
|
|
129
|
+
* Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
|
|
130
|
+
* Rectangular meshes now filter out user defined control points that erroneously lie outside their boudaries.
|
|
131
|
+
|
|
132
|
+
v2.2.1
|
|
133
|
+
* Normalized member force diagrams across the entire model for visual clarity.
|
|
134
|
+
* Made mesh auto-regeneration smarter and more efficient, targeting only meshes that have been altered since the last run.
|
|
135
|
+
* Added `ShearWall` mesh auto-regeneration. Whenever a model is solved any old or outdated `ShearWall` meshes are automatically removed and replaced by a rebuild.
|
|
136
|
+
* Optimized auto-calculated annotation sizing: The new automatic annotation size calculation feature now uses vectorized NumPy operations and caching for efficient rendering of large models with hundreds or thousands of elements.
|
|
137
|
+
* Adjusted member diagram label offsets for clarity.
|
|
138
|
+
|
|
124
139
|
v2.2.0
|
|
125
140
|
* Added member diagrams feature for easy visualization of shear, moment, axial, and torsion diagrams along members.
|
|
126
141
|
* Updated documentation for rendering.
|
|
@@ -48,19 +48,19 @@ def _prepare_model(model: FEModel3D, n_modes: int = 0) -> None:
|
|
|
48
48
|
for i in range(n_modes):
|
|
49
49
|
model.add_load_combo(f'Mode {i + 1}', {}, ['modal'])
|
|
50
50
|
|
|
51
|
-
# Generate all basic meshes that haven't been generated yet
|
|
51
|
+
# Generate all basic meshes that haven't been generated yet or need updating
|
|
52
52
|
for mesh in model.meshes.values():
|
|
53
|
-
if not mesh.is_generated:
|
|
53
|
+
if not mesh.is_generated or mesh.needs_update:
|
|
54
54
|
mesh.generate()
|
|
55
55
|
|
|
56
|
-
# Generate all shear wall meshes that haven't been generated yet
|
|
56
|
+
# Generate all shear wall meshes that haven't been generated yet or need updating
|
|
57
57
|
for shear_wall in model.shear_walls.values():
|
|
58
|
-
if not shear_wall.is_generated:
|
|
58
|
+
if not shear_wall.is_generated or shear_wall.needs_update:
|
|
59
59
|
shear_wall.generate()
|
|
60
60
|
|
|
61
|
-
# Generate all mat foundation meshes that haven't been generated yet
|
|
61
|
+
# Generate all mat foundation meshes that haven't been generated yet or need updating
|
|
62
62
|
for mat in model.mats.values():
|
|
63
|
-
if not mat.is_generated:
|
|
63
|
+
if not mat.is_generated or mat.needs_update:
|
|
64
64
|
mat.generate()
|
|
65
65
|
|
|
66
66
|
# Activate all springs and members for all load combinations
|
|
@@ -194,6 +194,25 @@ class FEModel3D():
|
|
|
194
194
|
# Get the load case for each plate/quad pressure
|
|
195
195
|
cases.append(load[1])
|
|
196
196
|
|
|
197
|
+
# Step through each shear wall helper
|
|
198
|
+
for shear_wall in self.shear_walls.values():
|
|
199
|
+
# Step through each shear wall shear load
|
|
200
|
+
for load in shear_wall._shears:
|
|
201
|
+
# Get the load case for each shear wall shear
|
|
202
|
+
cases.append(load[2])
|
|
203
|
+
|
|
204
|
+
# Step through each shear wall axial load
|
|
205
|
+
for load in shear_wall._axials:
|
|
206
|
+
# Get the load case for each shear wall axial
|
|
207
|
+
cases.append(load[2])
|
|
208
|
+
|
|
209
|
+
# Step through each mat foundation helper
|
|
210
|
+
for mat in self.mats.values():
|
|
211
|
+
# Step through each mat point load
|
|
212
|
+
for load in mat.pt_loads:
|
|
213
|
+
# Get the load case for each mat point load
|
|
214
|
+
cases.append(load[3])
|
|
215
|
+
|
|
197
216
|
# Remove duplicates and return the list (sorted ascending)
|
|
198
217
|
return sorted(list(dict.fromkeys(cases)))
|
|
199
218
|
|
|
@@ -2415,7 +2434,7 @@ class FEModel3D():
|
|
|
2415
2434
|
# The partitioned stiffness matrix originates as `coo` and is converted to `csr`
|
|
2416
2435
|
# format for mathematical operations. The `@` operator performs matrix multiplication
|
|
2417
2436
|
# on sparse matrices.
|
|
2418
|
-
Delta_D1 = spsolve(K11
|
|
2437
|
+
Delta_D1 = spsolve(K11, np.subtract(np.subtract(Delta_P1, Delta_FER1), K12 @ Delta_D2))
|
|
2419
2438
|
Delta_D1 = Delta_D1.reshape(len(Delta_D1), 1)
|
|
2420
2439
|
else:
|
|
2421
2440
|
Delta_D1 = solve(K11, np.subtract(np.subtract(Delta_P1, Delta_FER1), np.matmul(K12, Delta_D2)))
|
|
@@ -51,6 +51,7 @@ class MatFoundation(RectangleMesh):
|
|
|
51
51
|
self.name = name
|
|
52
52
|
self.ks = ks
|
|
53
53
|
self.pt_loads = [] # [XZ_coord, direction, magnitude, case]
|
|
54
|
+
self.needs_update = False # Flag indicating regeneration is needed due to changes
|
|
54
55
|
|
|
55
56
|
def add_rect_opening(self, name, X_min, Z_min, X_max, Z_max):
|
|
56
57
|
"""Add a rectangular opening to the mat by corner coordinates.
|
|
@@ -67,6 +68,8 @@ class MatFoundation(RectangleMesh):
|
|
|
67
68
|
"""
|
|
68
69
|
|
|
69
70
|
super().add_rect_opening(name, X_min, Z_min, X_max - X_min, Z_max - Z_min)
|
|
71
|
+
if self.is_generated:
|
|
72
|
+
self.needs_update = True
|
|
70
73
|
|
|
71
74
|
def add_mat_pt_load(self, XZ_coord, direction, magnitude, case='Case 1'):
|
|
72
75
|
"""Register a concentrated load at an X-Z coordinate on the mat.
|
|
@@ -84,6 +87,8 @@ class MatFoundation(RectangleMesh):
|
|
|
84
87
|
self.x_control.append(XZ_coord[0])
|
|
85
88
|
self.y_control.append(XZ_coord[1])
|
|
86
89
|
self.pt_loads.append([XZ_coord, direction, magnitude, case])
|
|
90
|
+
if self.is_generated:
|
|
91
|
+
self.needs_update = True
|
|
87
92
|
|
|
88
93
|
def generate(self):
|
|
89
94
|
"""Generate the mesh, apply point loads, and define soil springs.
|
|
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Literal, Union, List
|
|
|
3
3
|
from math import isclose
|
|
4
4
|
|
|
5
5
|
from numpy import array, zeros, add, subtract, matmul, insert, dot, cross, divide, count_nonzero, concatenate
|
|
6
|
-
from numpy import linspace, vstack, hstack, allclose, radians, sin, cos
|
|
6
|
+
from numpy import linspace, vstack, hstack, allclose, radians, sin, cos, maximum, minimum
|
|
7
7
|
from numpy.linalg import inv, pinv, norm
|
|
8
8
|
|
|
9
9
|
import Pynite.FixedEndReactions
|
|
@@ -1292,42 +1292,62 @@ class Member3D():
|
|
|
1292
1292
|
# Return 0 if no valid combos were found
|
|
1293
1293
|
return Vmin_global if Vmin_global is not None else 0
|
|
1294
1294
|
|
|
1295
|
-
def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
1295
|
+
def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
1296
1296
|
"""
|
|
1297
|
-
Plots the shear diagram for the member
|
|
1297
|
+
Plots the shear diagram for the member.
|
|
1298
1298
|
|
|
1299
1299
|
Parameters
|
|
1300
1300
|
----------
|
|
1301
1301
|
Direction : string
|
|
1302
|
-
The direction in which to
|
|
1302
|
+
The direction in which to plot the shear force. Must be one of the following:
|
|
1303
1303
|
'Fy' = Shear acting on the local y-axis.
|
|
1304
1304
|
'Fz' = Shear acting on the local z-axis.
|
|
1305
|
-
combo_name : string
|
|
1306
|
-
|
|
1305
|
+
combo_name : string or list of strings
|
|
1306
|
+
A single load combination name, or a list of combo tags. When a
|
|
1307
|
+
list of tags is provided, each matching combo is plotted and a
|
|
1308
|
+
max/min envelope is shown.
|
|
1307
1309
|
n_points: int
|
|
1308
1310
|
The number of points used to generate the plot
|
|
1309
1311
|
"""
|
|
1310
1312
|
|
|
1311
|
-
# Segment the member if necessary
|
|
1312
|
-
if self._solved_combo is None or combo_name != self._solved_combo.name:
|
|
1313
|
-
self._segment_member(combo_name)
|
|
1314
|
-
self._solved_combo = self.model.load_combos[combo_name]
|
|
1315
|
-
|
|
1316
1313
|
# Import 'pyplot' if not already done
|
|
1317
1314
|
if Member3D.__plt is None:
|
|
1318
1315
|
from matplotlib import pyplot as plt
|
|
1319
1316
|
Member3D.__plt = plt
|
|
1320
1317
|
|
|
1318
|
+
if isinstance(combo_name, str):
|
|
1319
|
+
combo_names = [combo_name]
|
|
1320
|
+
else:
|
|
1321
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
1322
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
1323
|
+
|
|
1321
1324
|
fig, ax = Member3D.__plt.subplots()
|
|
1322
1325
|
ax.axhline(0, color='black', lw=1)
|
|
1323
1326
|
ax.grid()
|
|
1324
1327
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1328
|
+
if len(combo_names) == 1:
|
|
1329
|
+
# Segment the member if necessary
|
|
1330
|
+
if self._solved_combo is None or combo_names[0] != self._solved_combo.name:
|
|
1331
|
+
self._segment_member(combo_names[0])
|
|
1332
|
+
self._solved_combo = self.model.load_combos[combo_names[0]]
|
|
1333
|
+
x, V = self.shear_array(Direction, n_points, combo_names[0])
|
|
1334
|
+
ax.plot(x, V)
|
|
1335
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
1336
|
+
else:
|
|
1337
|
+
env_max = None
|
|
1338
|
+
env_min = None
|
|
1339
|
+
for name in combo_names:
|
|
1340
|
+
x, V = self.shear_array(Direction, n_points, name)
|
|
1341
|
+
ax.plot(x, V, label=name)
|
|
1342
|
+
env_max = V if env_max is None else maximum(env_max, V)
|
|
1343
|
+
env_min = V if env_min is None else minimum(env_min, V)
|
|
1344
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
1345
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
1346
|
+
ax.legend(fontsize='small')
|
|
1347
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
1348
|
+
|
|
1349
|
+
ax.set_ylabel('Shear')
|
|
1350
|
+
ax.set_xlabel('Location')
|
|
1331
1351
|
Member3D.__plt.show()
|
|
1332
1352
|
|
|
1333
1353
|
def shear_array(self, Direction: Literal['Fy', 'Fz'], n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -1581,9 +1601,9 @@ class Member3D():
|
|
|
1581
1601
|
return Mmin_global if Mmin_global is not None else 0
|
|
1582
1602
|
|
|
1583
1603
|
|
|
1584
|
-
def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
1604
|
+
def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
1585
1605
|
"""
|
|
1586
|
-
Plots the moment diagram for the member
|
|
1606
|
+
Plots the moment diagram for the member.
|
|
1587
1607
|
|
|
1588
1608
|
Parameters
|
|
1589
1609
|
----------
|
|
@@ -1591,33 +1611,52 @@ class Member3D():
|
|
|
1591
1611
|
The direction in which to plot the moment. Must be one of the following:
|
|
1592
1612
|
'My' = Moment about the local y-axis.
|
|
1593
1613
|
'Mz' = moment about the local z-axis.
|
|
1594
|
-
combo_name : string
|
|
1595
|
-
|
|
1614
|
+
combo_name : string or list of strings
|
|
1615
|
+
A single load combination name, or a list of combo tags. When a
|
|
1616
|
+
list of tags is provided, each matching combo is plotted and a
|
|
1617
|
+
max/min envelope is shown.
|
|
1596
1618
|
n_points: int
|
|
1597
1619
|
The number of points used to generate the plot
|
|
1598
1620
|
"""
|
|
1599
1621
|
|
|
1600
|
-
# Segment the member if necessary
|
|
1601
|
-
if self._solved_combo is None or combo_name != self._solved_combo.name:
|
|
1602
|
-
self._segment_member(combo_name)
|
|
1603
|
-
self._solved_combo = self.model.load_combos[combo_name]
|
|
1604
|
-
|
|
1605
1622
|
# Import 'pyplot' if not already done
|
|
1606
1623
|
if Member3D.__plt is None:
|
|
1607
1624
|
from matplotlib import pyplot as plt
|
|
1608
1625
|
Member3D.__plt = plt
|
|
1609
1626
|
|
|
1627
|
+
if isinstance(combo_name, str):
|
|
1628
|
+
combo_names = [combo_name]
|
|
1629
|
+
else:
|
|
1630
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
1631
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
1632
|
+
|
|
1610
1633
|
fig, ax = Member3D.__plt.subplots()
|
|
1611
1634
|
ax.axhline(0, color='black', lw=1)
|
|
1612
1635
|
ax.grid()
|
|
1613
1636
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1637
|
+
if len(combo_names) == 1:
|
|
1638
|
+
# Segment the member if necessary
|
|
1639
|
+
if self._solved_combo is None or combo_names[0] != self._solved_combo.name:
|
|
1640
|
+
self._segment_member(combo_names[0])
|
|
1641
|
+
self._solved_combo = self.model.load_combos[combo_names[0]]
|
|
1642
|
+
x, M = self.moment_array(Direction, n_points, combo_names[0])
|
|
1643
|
+
ax.plot(x, M)
|
|
1644
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
1645
|
+
else:
|
|
1646
|
+
env_max = None
|
|
1647
|
+
env_min = None
|
|
1648
|
+
for name in combo_names:
|
|
1649
|
+
x, M = self.moment_array(Direction, n_points, name)
|
|
1650
|
+
ax.plot(x, M, label=name)
|
|
1651
|
+
env_max = M if env_max is None else maximum(env_max, M)
|
|
1652
|
+
env_min = M if env_min is None else minimum(env_min, M)
|
|
1653
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
1654
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
1655
|
+
ax.legend(fontsize='small')
|
|
1656
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
1657
|
+
|
|
1658
|
+
ax.set_ylabel('Moment')
|
|
1659
|
+
ax.set_xlabel('Location')
|
|
1621
1660
|
Member3D.__plt.show()
|
|
1622
1661
|
|
|
1623
1662
|
def moment_array(self, Direction: Literal['My', 'Mz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
|
|
@@ -1825,38 +1864,58 @@ class Member3D():
|
|
|
1825
1864
|
# Return global minimum or 0 if nothing found
|
|
1826
1865
|
return Tmin_global if Tmin_global is not None else 0
|
|
1827
1866
|
|
|
1828
|
-
def plot_torque(self, combo_name='Combo 1', n_points=20):
|
|
1867
|
+
def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
1829
1868
|
"""
|
|
1830
1869
|
Plots the torque diagram for the member.
|
|
1831
1870
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
combo_name : string
|
|
1835
|
-
|
|
1871
|
+
Parameters
|
|
1872
|
+
----------
|
|
1873
|
+
combo_name : string or list of strings
|
|
1874
|
+
A single load combination name, or a list of combo tags. When a
|
|
1875
|
+
list of tags is provided, each matching combo is plotted and a
|
|
1876
|
+
max/min envelope is shown.
|
|
1836
1877
|
n_points: int
|
|
1837
1878
|
The number of points used to generate the plot
|
|
1838
1879
|
"""
|
|
1839
1880
|
|
|
1840
|
-
# Segment the member if necessary
|
|
1841
|
-
if self._solved_combo is None or combo_name != self._solved_combo.name:
|
|
1842
|
-
self._segment_member(combo_name)
|
|
1843
|
-
self._solved_combo = self.model.load_combos[combo_name]
|
|
1844
|
-
|
|
1845
1881
|
# Import 'pyplot' if not already done
|
|
1846
1882
|
if Member3D.__plt is None:
|
|
1847
1883
|
from matplotlib import pyplot as plt
|
|
1848
1884
|
Member3D.__plt = plt
|
|
1849
1885
|
|
|
1886
|
+
if isinstance(combo_name, str):
|
|
1887
|
+
combo_names = [combo_name]
|
|
1888
|
+
else:
|
|
1889
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
1890
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
1891
|
+
|
|
1850
1892
|
fig, ax = Member3D.__plt.subplots()
|
|
1851
1893
|
ax.axhline(0, color='black', lw=1)
|
|
1852
1894
|
ax.grid()
|
|
1853
1895
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1896
|
+
if len(combo_names) == 1:
|
|
1897
|
+
# Segment the member if necessary
|
|
1898
|
+
if self._solved_combo is None or combo_names[0] != self._solved_combo.name:
|
|
1899
|
+
self._segment_member(combo_names[0])
|
|
1900
|
+
self._solved_combo = self.model.load_combos[combo_names[0]]
|
|
1901
|
+
x, T = self.torque_array(n_points, combo_names[0])
|
|
1902
|
+
ax.plot(x, T)
|
|
1903
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
1904
|
+
else:
|
|
1905
|
+
env_max = None
|
|
1906
|
+
env_min = None
|
|
1907
|
+
for name in combo_names:
|
|
1908
|
+
x, T = self.torque_array(n_points, name)
|
|
1909
|
+
ax.plot(x, T, label=name)
|
|
1910
|
+
env_max = T if env_max is None else maximum(env_max, T)
|
|
1911
|
+
env_min = T if env_min is None else minimum(env_min, T)
|
|
1912
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
1913
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
1914
|
+
ax.legend(fontsize='small')
|
|
1915
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
1916
|
+
|
|
1917
|
+
ax.set_ylabel('Torsional Moment (Warping Torsion Not Included)')
|
|
1918
|
+
ax.set_xlabel('Location')
|
|
1860
1919
|
Member3D.__plt.show()
|
|
1861
1920
|
|
|
1862
1921
|
def torque_array(self, n_points, combo_name='Combo 1', x_array = None) -> NDArray[float64]:
|
|
@@ -2039,38 +2098,58 @@ class Member3D():
|
|
|
2039
2098
|
# Return the global minimum, or 0 if nothing was found
|
|
2040
2099
|
return Pmin_global if Pmin_global is not None else 0
|
|
2041
2100
|
|
|
2042
|
-
def plot_axial(self, combo_name: str = 'Combo 1', n_points=20) -> None:
|
|
2101
|
+
def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
2043
2102
|
"""
|
|
2044
2103
|
Plots the axial force diagram for the member.
|
|
2045
2104
|
|
|
2046
2105
|
Parameters
|
|
2047
2106
|
----------
|
|
2048
|
-
combo_name : string
|
|
2049
|
-
|
|
2107
|
+
combo_name : string or list of strings
|
|
2108
|
+
A single load combination name, or a list of combo tags. When a
|
|
2109
|
+
list of tags is provided, each matching combo is plotted and a
|
|
2110
|
+
max/min envelope is shown.
|
|
2050
2111
|
n_points: int
|
|
2051
2112
|
The number of points used to generate the plot
|
|
2052
2113
|
"""
|
|
2053
2114
|
|
|
2054
|
-
# Segment the member if necessary
|
|
2055
|
-
if self._solved_combo is None or combo_name != self._solved_combo.name:
|
|
2056
|
-
self._segment_member(combo_name)
|
|
2057
|
-
self._solved_combo = self.model.load_combos[combo_name]
|
|
2058
|
-
|
|
2059
2115
|
# Import 'pyplot' if not already done
|
|
2060
2116
|
if Member3D.__plt is None:
|
|
2061
2117
|
from matplotlib import pyplot as plt
|
|
2062
2118
|
Member3D.__plt = plt
|
|
2063
2119
|
|
|
2120
|
+
if isinstance(combo_name, str):
|
|
2121
|
+
combo_names = [combo_name]
|
|
2122
|
+
else:
|
|
2123
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
2124
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
2125
|
+
|
|
2064
2126
|
fig, ax = Member3D.__plt.subplots()
|
|
2065
2127
|
ax.axhline(0, color='black', lw=1)
|
|
2066
2128
|
ax.grid()
|
|
2067
2129
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2130
|
+
if len(combo_names) == 1:
|
|
2131
|
+
# Segment the member if necessary
|
|
2132
|
+
if self._solved_combo is None or combo_names[0] != self._solved_combo.name:
|
|
2133
|
+
self._segment_member(combo_names[0])
|
|
2134
|
+
self._solved_combo = self.model.load_combos[combo_names[0]]
|
|
2135
|
+
x, P = self.axial_array(n_points, combo_names[0])
|
|
2136
|
+
ax.plot(x, P)
|
|
2137
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
2138
|
+
else:
|
|
2139
|
+
env_max = None
|
|
2140
|
+
env_min = None
|
|
2141
|
+
for name in combo_names:
|
|
2142
|
+
x, P = self.axial_array(n_points, name)
|
|
2143
|
+
ax.plot(x, P, label=name)
|
|
2144
|
+
env_max = P if env_max is None else maximum(env_max, P)
|
|
2145
|
+
env_min = P if env_min is None else minimum(env_min, P)
|
|
2146
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
2147
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
2148
|
+
ax.legend(fontsize='small')
|
|
2149
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
2150
|
+
|
|
2151
|
+
ax.set_ylabel('Axial Force')
|
|
2152
|
+
ax.set_xlabel('Location')
|
|
2074
2153
|
Member3D.__plt.show()
|
|
2075
2154
|
|
|
2076
2155
|
def axial_array(self, n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
|
|
@@ -2309,43 +2388,63 @@ class Member3D():
|
|
|
2309
2388
|
# Return global minimum or 0 if nothing found
|
|
2310
2389
|
return dmin_global if dmin_global is not None else 0
|
|
2311
2390
|
|
|
2312
|
-
def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
2391
|
+
def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
2313
2392
|
"""
|
|
2314
|
-
Plots the deflection diagram for the member
|
|
2393
|
+
Plots the deflection diagram for the member.
|
|
2315
2394
|
|
|
2316
2395
|
Parameters
|
|
2317
2396
|
----------
|
|
2318
2397
|
Direction : string
|
|
2319
|
-
The direction in which to
|
|
2398
|
+
The direction in which to plot the deflection. Must be one of the following:
|
|
2320
2399
|
'dx' = Deflection in the local x-axis.
|
|
2321
2400
|
'dy' = Deflection in the local y-axis.
|
|
2322
2401
|
'dz' = Deflection in the local z-axis.
|
|
2323
|
-
combo_name : string
|
|
2324
|
-
|
|
2402
|
+
combo_name : string or list of strings
|
|
2403
|
+
A single load combination name, or a list of combo tags. When a
|
|
2404
|
+
list of tags is provided, each matching combo is plotted and a
|
|
2405
|
+
max/min envelope is shown.
|
|
2325
2406
|
n_points: int
|
|
2326
2407
|
The number of points used to generate the plot
|
|
2327
2408
|
"""
|
|
2328
2409
|
|
|
2329
|
-
# Segment the member if necessary
|
|
2330
|
-
if self._solved_combo is None or combo_name != self._solved_combo.name:
|
|
2331
|
-
self._segment_member(combo_name)
|
|
2332
|
-
self._solved_combo = self.model.load_combos[combo_name]
|
|
2333
|
-
|
|
2334
2410
|
# Import 'pyplot' if not already done
|
|
2335
2411
|
if Member3D.__plt is None:
|
|
2336
2412
|
from matplotlib import pyplot as plt
|
|
2337
2413
|
Member3D.__plt = plt
|
|
2338
2414
|
|
|
2415
|
+
if isinstance(combo_name, str):
|
|
2416
|
+
combo_names = [combo_name]
|
|
2417
|
+
else:
|
|
2418
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
2419
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
2420
|
+
|
|
2339
2421
|
fig, ax = Member3D.__plt.subplots()
|
|
2340
2422
|
ax.axhline(0, color='black', lw=1)
|
|
2341
2423
|
ax.grid()
|
|
2342
2424
|
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2425
|
+
if len(combo_names) == 1:
|
|
2426
|
+
# Segment the member if necessary
|
|
2427
|
+
if self._solved_combo is None or combo_names[0] != self._solved_combo.name:
|
|
2428
|
+
self._segment_member(combo_names[0])
|
|
2429
|
+
self._solved_combo = self.model.load_combos[combo_names[0]]
|
|
2430
|
+
x, d = self.deflection_array(Direction, n_points, combo_names[0])
|
|
2431
|
+
ax.plot(x, d)
|
|
2432
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
2433
|
+
else:
|
|
2434
|
+
env_max = None
|
|
2435
|
+
env_min = None
|
|
2436
|
+
for name in combo_names:
|
|
2437
|
+
x, d = self.deflection_array(Direction, n_points, name)
|
|
2438
|
+
ax.plot(x, d, label=name)
|
|
2439
|
+
env_max = d if env_max is None else maximum(env_max, d)
|
|
2440
|
+
env_min = d if env_min is None else minimum(env_min, d)
|
|
2441
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
2442
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
2443
|
+
ax.legend(fontsize='small')
|
|
2444
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
2445
|
+
|
|
2446
|
+
ax.set_ylabel('Deflection')
|
|
2447
|
+
ax.set_xlabel('Location')
|
|
2349
2448
|
Member3D.__plt.show()
|
|
2350
2449
|
|
|
2351
2450
|
def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
|
|
@@ -48,6 +48,7 @@ class Mesh():
|
|
|
48
48
|
self.elements: Dict[str, Union[Quad3D, Plate3D]] = {} # A dictionary containing the elements in the mesh
|
|
49
49
|
self.element_type = 'Quad' # The type of element used in the mesh
|
|
50
50
|
self.is_generated = False # A flag indicating whether the mesh has been generated
|
|
51
|
+
self.needs_update = False # A flag indicating whether the mesh needs regeneration due to changes
|
|
51
52
|
|
|
52
53
|
def _remove_from_model(self) -> None:
|
|
53
54
|
"""Removes the mesh's nodes and elements from the model in preparation for regeneration.
|
|
@@ -815,6 +816,11 @@ class RectangleMesh(Mesh):
|
|
|
815
816
|
unique_list.append(y_control[i])
|
|
816
817
|
unique_list.append(y_control[-1])
|
|
817
818
|
y_control = unique_list
|
|
819
|
+
|
|
820
|
+
# Remove any control points that fall outside the mesh boundaries
|
|
821
|
+
# Control points outside [0, width] or [0, height] would create elements outside the intended mesh
|
|
822
|
+
x_control = [val for val in x_control if val >= -1e-10 and val <= width + 1e-10]
|
|
823
|
+
y_control = [val for val in y_control if val >= -1e-10 and val <= height + 1e-10]
|
|
818
824
|
|
|
819
825
|
# Each node number will be increased by the offset calculated below
|
|
820
826
|
node_offset = int(self.start_node[1:]) - 1
|
|
@@ -1033,6 +1039,7 @@ class RectangleMesh(Mesh):
|
|
|
1033
1039
|
|
|
1034
1040
|
# Flag the mesh as generated
|
|
1035
1041
|
self.is_generated = True
|
|
1042
|
+
self.needs_update = False
|
|
1036
1043
|
|
|
1037
1044
|
def node_local_coords(self, node: Node3D) -> tuple[float, float]:
|
|
1038
1045
|
"""Calculates a node's position in the mesh's local x/y coordinate system.
|
|
@@ -1078,8 +1085,9 @@ class RectangleMesh(Mesh):
|
|
|
1078
1085
|
self.x_control.append(x_left + width)
|
|
1079
1086
|
self.y_control.append(y_bott + height)
|
|
1080
1087
|
|
|
1081
|
-
# Flag
|
|
1082
|
-
self.is_generated
|
|
1088
|
+
# Flag that regeneration is needed if already generated
|
|
1089
|
+
if self.is_generated:
|
|
1090
|
+
self.needs_update = True
|
|
1083
1091
|
|
|
1084
1092
|
|
|
1085
1093
|
class RectOpening():
|
|
@@ -1233,6 +1241,7 @@ class AnnulusMesh(Mesh):
|
|
|
1233
1241
|
|
|
1234
1242
|
# Flag the mesh as generated
|
|
1235
1243
|
self.is_generated = True
|
|
1244
|
+
self.needs_update = False
|
|
1236
1245
|
|
|
1237
1246
|
#%%
|
|
1238
1247
|
class AnnulusRingMesh(Mesh):
|
|
@@ -1390,6 +1399,7 @@ class AnnulusRingMesh(Mesh):
|
|
|
1390
1399
|
|
|
1391
1400
|
# Flag the mesh as generated
|
|
1392
1401
|
self.is_generated = True
|
|
1402
|
+
self.needs_update = False
|
|
1393
1403
|
|
|
1394
1404
|
|
|
1395
1405
|
class AnnulusTransRingMesh(Mesh):
|
|
@@ -1589,6 +1599,7 @@ class AnnulusTransRingMesh(Mesh):
|
|
|
1589
1599
|
|
|
1590
1600
|
# Flag the mesh as generated
|
|
1591
1601
|
self.is_generated = True
|
|
1602
|
+
self.needs_update = False
|
|
1592
1603
|
|
|
1593
1604
|
|
|
1594
1605
|
class FrustrumMesh(AnnulusMesh):
|
|
@@ -1806,6 +1817,7 @@ class CylinderMesh(Mesh):
|
|
|
1806
1817
|
|
|
1807
1818
|
# Flag the mesh as generated
|
|
1808
1819
|
self.is_generated = True
|
|
1820
|
+
self.needs_update = False
|
|
1809
1821
|
|
|
1810
1822
|
#%%
|
|
1811
1823
|
class CylinderRingMesh(Mesh):
|
|
@@ -1994,6 +2006,7 @@ class CylinderRingMesh(Mesh):
|
|
|
1994
2006
|
|
|
1995
2007
|
# Flag the mesh as generated
|
|
1996
2008
|
self.is_generated = True
|
|
2009
|
+
self.needs_update = False
|
|
1997
2010
|
|
|
1998
2011
|
def check_mesh_integrity(mesh: Mesh, console_log: bool = True) -> Union[str, List[str], None]:
|
|
1999
2012
|
"""Runs basic integrity checks to ensure the mesh is in sync with its model. Usually you don't
|