PyNiteFEA 2.2.1__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.1 → pynitefea-2.3.0}/PKG-INFO +9 -1
- {pynitefea-2.2.1 → pynitefea-2.3.0}/PyNiteFEA.egg-info/PKG-INFO +9 -1
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/FEModel3D.py +20 -1
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Member3D.py +178 -79
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Mesh.py +5 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/PhysMember.py +158 -68
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Rendering.py +72 -6
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/ShearWall.py +1 -1
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/VTKWriter.py +28 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Visualization.py +97 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/README.md +8 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/setup.py +1 -1
- {pynitefea-2.2.1 → pynitefea-2.3.0}/LICENSE +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/PyNiteFEA.egg-info/requires.txt +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Analysis.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/BeamSegY.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/BeamSegZ.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/FixedEndReactions.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/LoadCombo.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/MainStyleSheet.css +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/MatFoundation.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Material.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Node3D.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Plate3D.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Quad3D.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Report_Template.html +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Reporting.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Section.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Spring3D.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/Tri3D.py +0 -0
- {pynitefea-2.2.1 → pynitefea-2.3.0}/Pynite/__init__.py +0 -0
- {pynitefea-2.2.1 → 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,14 @@ 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
|
+
|
|
124
132
|
v2.2.1
|
|
125
133
|
* Normalized member force diagrams across the entire model for visual clarity.
|
|
126
134
|
* Made mesh auto-regeneration smarter and more efficient, targeting only meshes that have been altered since the last run.
|
|
@@ -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,14 @@ 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
|
+
|
|
124
132
|
v2.2.1
|
|
125
133
|
* Normalized member force diagrams across the entire model for visual clarity.
|
|
126
134
|
* Made mesh auto-regeneration smarter and more efficient, targeting only meshes that have been altered since the last run.
|
|
@@ -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)))
|
|
@@ -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]:
|
|
@@ -816,6 +816,11 @@ class RectangleMesh(Mesh):
|
|
|
816
816
|
unique_list.append(y_control[i])
|
|
817
817
|
unique_list.append(y_control[-1])
|
|
818
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]
|
|
819
824
|
|
|
820
825
|
# Each node number will be increased by the offset calculated below
|
|
821
826
|
node_offset = int(self.start_node[1:]) - 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations # Allows more recent type hints features
|
|
2
|
-
from typing import Dict, List, Literal, Tuple, TYPE_CHECKING
|
|
2
|
+
from typing import Dict, List, Literal, Tuple, Union, TYPE_CHECKING
|
|
3
3
|
from Pynite.Member3D import Member3D
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from numpy import float64
|
|
11
11
|
from numpy.typing import NDArray
|
|
12
12
|
|
|
13
|
-
from numpy import array, dot, linspace, hstack, empty
|
|
13
|
+
from numpy import array, dot, linspace, hstack, empty, maximum, minimum
|
|
14
14
|
from numpy.linalg import norm
|
|
15
15
|
from math import isclose, acos
|
|
16
16
|
|
|
@@ -257,9 +257,9 @@ class PhysMember(Member3D):
|
|
|
257
257
|
Vmin = V
|
|
258
258
|
return Vmin
|
|
259
259
|
|
|
260
|
-
def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
260
|
+
def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
261
261
|
"""
|
|
262
|
-
Plots the shear diagram for the member
|
|
262
|
+
Plots the shear diagram for the member.
|
|
263
263
|
|
|
264
264
|
Parameters
|
|
265
265
|
----------
|
|
@@ -267,8 +267,10 @@ class PhysMember(Member3D):
|
|
|
267
267
|
The direction in which to plot the shear force. Must be one of the following:
|
|
268
268
|
'Fy' = Shear in the local y-axis.
|
|
269
269
|
'Fz' = Shear in the local z-axis.
|
|
270
|
-
combo_name : string
|
|
271
|
-
|
|
270
|
+
combo_name : string or list of strings
|
|
271
|
+
A single load combination name, or a list of combo tags. When a
|
|
272
|
+
list of tags is provided, each matching combo is plotted and a
|
|
273
|
+
max/min envelope is shown.
|
|
272
274
|
n_points: int
|
|
273
275
|
The number of points used to generate the plot
|
|
274
276
|
"""
|
|
@@ -278,19 +280,35 @@ class PhysMember(Member3D):
|
|
|
278
280
|
from matplotlib import pyplot as plt
|
|
279
281
|
PhysMember.__plt = plt
|
|
280
282
|
|
|
283
|
+
if isinstance(combo_name, str):
|
|
284
|
+
combo_names = [combo_name]
|
|
285
|
+
else:
|
|
286
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
287
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
288
|
+
|
|
281
289
|
fig, ax = PhysMember.__plt.subplots()
|
|
282
290
|
ax.axhline(0, color='black', lw=1)
|
|
283
291
|
ax.grid()
|
|
284
292
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
293
|
+
if len(combo_names) == 1:
|
|
294
|
+
x, V = self.shear_array(Direction, n_points, combo_names[0])
|
|
295
|
+
ax.plot(x, V)
|
|
296
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
297
|
+
else:
|
|
298
|
+
env_max = None
|
|
299
|
+
env_min = None
|
|
300
|
+
for name in combo_names:
|
|
301
|
+
x, V = self.shear_array(Direction, n_points, name)
|
|
302
|
+
ax.plot(x, V, label=name)
|
|
303
|
+
env_max = V if env_max is None else maximum(env_max, V)
|
|
304
|
+
env_min = V if env_min is None else minimum(env_min, V)
|
|
305
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
306
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
307
|
+
ax.legend(fontsize='small')
|
|
308
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
309
|
+
|
|
310
|
+
ax.set_ylabel('Shear')
|
|
311
|
+
ax.set_xlabel('Location')
|
|
294
312
|
PhysMember.__plt.show()
|
|
295
313
|
|
|
296
314
|
def shear_array(self, Direction: Literal['Fy', 'Fz'], n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -429,19 +447,20 @@ class PhysMember(Member3D):
|
|
|
429
447
|
Mmin = M
|
|
430
448
|
return Mmin
|
|
431
449
|
|
|
432
|
-
def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
450
|
+
def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
433
451
|
"""
|
|
434
|
-
Plots the moment diagram for the member
|
|
452
|
+
Plots the moment diagram for the member.
|
|
435
453
|
|
|
436
454
|
Parameters
|
|
437
455
|
----------
|
|
438
|
-
|
|
439
456
|
Direction : string
|
|
440
457
|
The direction in which to plot the moment. Must be one of the following:
|
|
441
458
|
'My' = Moment about the local y-axis.
|
|
442
|
-
'Mz' =
|
|
443
|
-
combo_name : string
|
|
444
|
-
|
|
459
|
+
'Mz' = Moment about the local z-axis.
|
|
460
|
+
combo_name : string or list of strings
|
|
461
|
+
A single load combination name, or a list of combo tags. When a
|
|
462
|
+
list of tags is provided, each matching combo is plotted and a
|
|
463
|
+
max/min envelope is shown.
|
|
445
464
|
n_points: int
|
|
446
465
|
The number of points used to generate the plot
|
|
447
466
|
"""
|
|
@@ -451,19 +470,35 @@ class PhysMember(Member3D):
|
|
|
451
470
|
from matplotlib import pyplot as plt
|
|
452
471
|
PhysMember.__plt = plt
|
|
453
472
|
|
|
473
|
+
if isinstance(combo_name, str):
|
|
474
|
+
combo_names = [combo_name]
|
|
475
|
+
else:
|
|
476
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
477
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
478
|
+
|
|
454
479
|
fig, ax = PhysMember.__plt.subplots()
|
|
455
480
|
ax.axhline(0, color='black', lw=1)
|
|
456
481
|
ax.grid()
|
|
457
482
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
483
|
+
if len(combo_names) == 1:
|
|
484
|
+
x, M = self.moment_array(Direction, n_points, combo_names[0])
|
|
485
|
+
ax.plot(x, M)
|
|
486
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
487
|
+
else:
|
|
488
|
+
env_max = None
|
|
489
|
+
env_min = None
|
|
490
|
+
for name in combo_names:
|
|
491
|
+
x, M = self.moment_array(Direction, n_points, name)
|
|
492
|
+
ax.plot(x, M, label=name)
|
|
493
|
+
env_max = M if env_max is None else maximum(env_max, M)
|
|
494
|
+
env_min = M if env_min is None else minimum(env_min, M)
|
|
495
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
496
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
497
|
+
ax.legend(fontsize='small')
|
|
498
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
499
|
+
|
|
500
|
+
ax.set_ylabel('Moment')
|
|
501
|
+
ax.set_xlabel('Location')
|
|
467
502
|
PhysMember.__plt.show()
|
|
468
503
|
|
|
469
504
|
def moment_array(self, Direction: Literal['My', 'Mz'], n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -589,14 +624,16 @@ class PhysMember(Member3D):
|
|
|
589
624
|
Tmin = T
|
|
590
625
|
return Tmin
|
|
591
626
|
|
|
592
|
-
def plot_torque(self, combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
627
|
+
def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
593
628
|
"""
|
|
594
|
-
Plots the torque diagram for the member
|
|
629
|
+
Plots the torque diagram for the member.
|
|
595
630
|
|
|
596
631
|
Parameters
|
|
597
632
|
----------
|
|
598
|
-
combo_name : string
|
|
599
|
-
|
|
633
|
+
combo_name : string or list of strings
|
|
634
|
+
A single load combination name, or a list of combo tags. When a
|
|
635
|
+
list of tags is provided, each matching combo is plotted and a
|
|
636
|
+
max/min envelope is shown.
|
|
600
637
|
n_points: int
|
|
601
638
|
The number of points used to generate the plot
|
|
602
639
|
"""
|
|
@@ -606,19 +643,35 @@ class PhysMember(Member3D):
|
|
|
606
643
|
from matplotlib import pyplot as plt
|
|
607
644
|
PhysMember.__plt = plt
|
|
608
645
|
|
|
646
|
+
if isinstance(combo_name, str):
|
|
647
|
+
combo_names = [combo_name]
|
|
648
|
+
else:
|
|
649
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
650
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
651
|
+
|
|
609
652
|
fig, ax = PhysMember.__plt.subplots()
|
|
610
653
|
ax.axhline(0, color='black', lw=1)
|
|
611
654
|
ax.grid()
|
|
612
655
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
656
|
+
if len(combo_names) == 1:
|
|
657
|
+
x, T = self.torque_array(n_points, combo_names[0])
|
|
658
|
+
ax.plot(x, T)
|
|
659
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
660
|
+
else:
|
|
661
|
+
env_max = None
|
|
662
|
+
env_min = None
|
|
663
|
+
for name in combo_names:
|
|
664
|
+
x, T = self.torque_array(n_points, name)
|
|
665
|
+
ax.plot(x, T, label=name)
|
|
666
|
+
env_max = T if env_max is None else maximum(env_max, T)
|
|
667
|
+
env_min = T if env_min is None else minimum(env_min, T)
|
|
668
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
669
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
670
|
+
ax.legend(fontsize='small')
|
|
671
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
672
|
+
|
|
673
|
+
ax.set_ylabel('Torque')
|
|
674
|
+
ax.set_xlabel('Location')
|
|
622
675
|
PhysMember.__plt.show()
|
|
623
676
|
|
|
624
677
|
def torque_array(self, n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -721,14 +774,16 @@ class PhysMember(Member3D):
|
|
|
721
774
|
Pmin = P
|
|
722
775
|
return Pmin
|
|
723
776
|
|
|
724
|
-
def plot_axial(self, combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
777
|
+
def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
725
778
|
"""
|
|
726
|
-
Plots the axial force diagram for the member
|
|
779
|
+
Plots the axial force diagram for the member.
|
|
727
780
|
|
|
728
781
|
Parameters
|
|
729
782
|
----------
|
|
730
|
-
combo_name : string
|
|
731
|
-
|
|
783
|
+
combo_name : string or list of strings
|
|
784
|
+
A single load combination name, or a list of combo tags. When a
|
|
785
|
+
list of tags is provided, each matching combo is plotted and a
|
|
786
|
+
max/min envelope is shown.
|
|
732
787
|
n_points: int
|
|
733
788
|
The number of points used to generate the plot
|
|
734
789
|
"""
|
|
@@ -738,19 +793,35 @@ class PhysMember(Member3D):
|
|
|
738
793
|
from matplotlib import pyplot as plt
|
|
739
794
|
PhysMember.__plt = plt
|
|
740
795
|
|
|
796
|
+
if isinstance(combo_name, str):
|
|
797
|
+
combo_names = [combo_name]
|
|
798
|
+
else:
|
|
799
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
800
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
801
|
+
|
|
741
802
|
fig, ax = PhysMember.__plt.subplots()
|
|
742
803
|
ax.axhline(0, color='black', lw=1)
|
|
743
804
|
ax.grid()
|
|
744
805
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
806
|
+
if len(combo_names) == 1:
|
|
807
|
+
x, P = self.axial_array(n_points, combo_names[0])
|
|
808
|
+
ax.plot(x, P)
|
|
809
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
810
|
+
else:
|
|
811
|
+
env_max = None
|
|
812
|
+
env_min = None
|
|
813
|
+
for name in combo_names:
|
|
814
|
+
x, P = self.axial_array(n_points, name)
|
|
815
|
+
ax.plot(x, P, label=name)
|
|
816
|
+
env_max = P if env_max is None else maximum(env_max, P)
|
|
817
|
+
env_min = P if env_min is None else minimum(env_min, P)
|
|
818
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
819
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
820
|
+
ax.legend(fontsize='small')
|
|
821
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
822
|
+
|
|
823
|
+
ax.set_ylabel('Axial')
|
|
824
|
+
ax.set_xlabel('Location')
|
|
754
825
|
PhysMember.__plt.show()
|
|
755
826
|
|
|
756
827
|
def axial_array(self, n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -897,9 +968,9 @@ class PhysMember(Member3D):
|
|
|
897
968
|
member, x_mod = self.find_member(x)
|
|
898
969
|
return member.rel_deflection(Direction, x_mod, combo_name)
|
|
899
970
|
|
|
900
|
-
def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
|
|
971
|
+
def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
|
|
901
972
|
"""
|
|
902
|
-
Plots the deflection diagram for the member
|
|
973
|
+
Plots the deflection diagram for the member.
|
|
903
974
|
|
|
904
975
|
Parameters
|
|
905
976
|
----------
|
|
@@ -907,8 +978,10 @@ class PhysMember(Member3D):
|
|
|
907
978
|
The direction in which to plot the deflection. Must be one of the following:
|
|
908
979
|
'dy' = Deflection in the local y-axis.
|
|
909
980
|
'dz' = Deflection in the local z-axis.
|
|
910
|
-
combo_name : string
|
|
911
|
-
|
|
981
|
+
combo_name : string or list of strings
|
|
982
|
+
A single load combination name, or a list of combo tags. When a
|
|
983
|
+
list of tags is provided, each matching combo is plotted and a
|
|
984
|
+
max/min envelope is shown.
|
|
912
985
|
n_points: int
|
|
913
986
|
The number of points used to generate the plot
|
|
914
987
|
"""
|
|
@@ -918,18 +991,35 @@ class PhysMember(Member3D):
|
|
|
918
991
|
from matplotlib import pyplot as plt
|
|
919
992
|
PhysMember.__plt = plt
|
|
920
993
|
|
|
994
|
+
if isinstance(combo_name, str):
|
|
995
|
+
combo_names = [combo_name]
|
|
996
|
+
else:
|
|
997
|
+
combo_names = [name for name, combo in self.model.load_combos.items()
|
|
998
|
+
if any(tag in combo.combo_tags for tag in combo_name)]
|
|
999
|
+
|
|
921
1000
|
fig, ax = PhysMember.__plt.subplots()
|
|
922
1001
|
ax.axhline(0, color='black', lw=1)
|
|
923
1002
|
ax.grid()
|
|
924
1003
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1004
|
+
if len(combo_names) == 1:
|
|
1005
|
+
x, d = self.deflection_array(Direction, n_points, combo_names[0])
|
|
1006
|
+
ax.plot(x, d)
|
|
1007
|
+
ax.set_title('Member ' + self.name + '\n' + combo_names[0])
|
|
1008
|
+
else:
|
|
1009
|
+
env_max = None
|
|
1010
|
+
env_min = None
|
|
1011
|
+
for name in combo_names:
|
|
1012
|
+
x, d = self.deflection_array(Direction, n_points, name)
|
|
1013
|
+
ax.plot(x, d, label=name)
|
|
1014
|
+
env_max = d if env_max is None else maximum(env_max, d)
|
|
1015
|
+
env_min = d if env_min is None else minimum(env_min, d)
|
|
1016
|
+
ax.plot(x, env_max, color='green', alpha=0.4, lw=3, label='Max Envelope')
|
|
1017
|
+
ax.plot(x, env_min, color='red', alpha=0.4, lw=3, label='Min Envelope')
|
|
1018
|
+
ax.legend(fontsize='small')
|
|
1019
|
+
ax.set_title('Member ' + self.name + '\nEnvelope')
|
|
1020
|
+
|
|
1021
|
+
ax.set_ylabel('Deflection')
|
|
1022
|
+
ax.set_xlabel('Location')
|
|
933
1023
|
PhysMember.__plt.show()
|
|
934
1024
|
|
|
935
1025
|
def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
|
|
@@ -405,7 +405,7 @@ class Renderer:
|
|
|
405
405
|
vis_spring.add_to_plotter(self.plotter)
|
|
406
406
|
|
|
407
407
|
if self.model.members:
|
|
408
|
-
vis_members = [VisMember(member, self.theme) for member in self.model.members.values()]
|
|
408
|
+
vis_members = [VisMember(member, self.theme, self.annotation_size) for member in self.model.members.values()]
|
|
409
409
|
for vis_member in vis_members:
|
|
410
410
|
vis_member.add_to_plotter(self.plotter)
|
|
411
411
|
|
|
@@ -425,16 +425,19 @@ class Renderer:
|
|
|
425
425
|
if self.show_labels and vis_nodes:
|
|
426
426
|
label_points = [vis_node.label_point for vis_node in vis_nodes]
|
|
427
427
|
labels = [vis_node.label for vis_node in vis_nodes]
|
|
428
|
+
label_points = np.asarray(label_points, dtype=float)
|
|
428
429
|
self.plotter.add_point_labels(label_points, labels, bold=False, text_color='black', show_points=True, point_color='grey', point_size=5, shape=None, render_points_as_spheres=True)
|
|
429
430
|
|
|
430
431
|
if self.show_labels and vis_springs:
|
|
431
432
|
self._spring_label_points = [vis_spring.label_point for vis_spring in vis_springs]
|
|
432
433
|
self._spring_labels = [vis_spring.label for vis_spring in vis_springs]
|
|
433
|
-
|
|
434
|
+
spring_label_points = np.asarray(self._spring_label_points, dtype=float)
|
|
435
|
+
self.plotter.add_point_labels(spring_label_points, self._spring_labels, text_color='black', bold=False, shape=None, render_points_as_spheres=False)
|
|
434
436
|
|
|
435
437
|
if self.show_labels and vis_members:
|
|
436
438
|
label_points = [vis_member.label_point for vis_member in vis_members]
|
|
437
439
|
labels = [vis_member.label for vis_member in vis_members]
|
|
440
|
+
label_points = np.asarray(label_points, dtype=float)
|
|
438
441
|
self.plotter.add_point_labels(label_points, labels, bold=False, text_color='black', show_points=False, shape=None, render_points_as_spheres=False)
|
|
439
442
|
|
|
440
443
|
# Render the loads if requested
|
|
@@ -444,7 +447,8 @@ class Renderer:
|
|
|
444
447
|
self.plot_loads()
|
|
445
448
|
|
|
446
449
|
# Plot the load labels
|
|
447
|
-
|
|
450
|
+
load_label_points = np.asarray(self._load_label_points, dtype=float)
|
|
451
|
+
self.plotter.add_point_labels(load_label_points, self._load_labels, bold=False, text_color='green', show_points=False, shape=None, render_points_as_spheres=False)
|
|
448
452
|
|
|
449
453
|
# Render the plates and quads, if present
|
|
450
454
|
if self.model.quads or self.model.plates:
|
|
@@ -829,7 +833,7 @@ class Renderer:
|
|
|
829
833
|
i+=1
|
|
830
834
|
|
|
831
835
|
# Add the vertices and the faces to our lists
|
|
832
|
-
plate_vertices = np.array(plate_vertices)
|
|
836
|
+
plate_vertices = np.array(plate_vertices, dtype=float)
|
|
833
837
|
plate_faces = np.array(plate_faces)
|
|
834
838
|
|
|
835
839
|
# Create a new PyVista dataset to store plate data
|
|
@@ -1058,7 +1062,7 @@ class Renderer:
|
|
|
1058
1062
|
v2 = _PerpVector(v1)
|
|
1059
1063
|
|
|
1060
1064
|
# Generate the arc for the moment
|
|
1061
|
-
arc = pv.CircularArcFromNormal(center, resolution=20, normal=v1, angle=215, polar=v2*radius)
|
|
1065
|
+
arc = pv.CircularArcFromNormal(center=center, resolution=20, normal=v1, angle=215, polar=v2*radius)
|
|
1062
1066
|
|
|
1063
1067
|
# Add the arc to the plot
|
|
1064
1068
|
self.plotter.add_mesh(arc, line_width=2, color=color)
|
|
@@ -1837,15 +1841,18 @@ class VisSpring:
|
|
|
1837
1841
|
class VisMember:
|
|
1838
1842
|
"""Visual wrapper for a Member3D as a simple line."""
|
|
1839
1843
|
|
|
1840
|
-
def __init__(self, member: 'Member3D', theme: str) -> None:
|
|
1844
|
+
def __init__(self, member: 'Member3D', theme: str, annotation_size: float) -> None:
|
|
1841
1845
|
"""Build visual elements for a member.
|
|
1842
1846
|
|
|
1843
1847
|
:param Member3D member: Member to visualize.
|
|
1844
1848
|
:param str theme: Rendering theme (``'default'`` or ``'print'``).
|
|
1849
|
+
:param float annotation_size: Base size for release symbols.
|
|
1845
1850
|
"""
|
|
1846
1851
|
self.member = member
|
|
1847
1852
|
self.theme = theme
|
|
1853
|
+
self.annotation_size = annotation_size
|
|
1848
1854
|
self.mesh: pv.PolyData = self._build_geometry()
|
|
1855
|
+
self.release_meshes: List[pv.PolyData] = self._build_release_meshes()
|
|
1849
1856
|
self.label = member.name
|
|
1850
1857
|
self.label_point = [(member.i_node.X + member.j_node.X) / 2,
|
|
1851
1858
|
(member.i_node.Y + member.j_node.Y) / 2,
|
|
@@ -1857,6 +1864,63 @@ class VisMember:
|
|
|
1857
1864
|
line.points[1] = [self.member.j_node.X, self.member.j_node.Y, self.member.j_node.Z]
|
|
1858
1865
|
return line
|
|
1859
1866
|
|
|
1867
|
+
def _build_release_meshes(self) -> List[pv.PolyData]:
|
|
1868
|
+
releases = self.member.Releases
|
|
1869
|
+
if not releases:
|
|
1870
|
+
return []
|
|
1871
|
+
|
|
1872
|
+
show_i_ry = releases[4]
|
|
1873
|
+
show_i_rz = releases[5]
|
|
1874
|
+
show_j_ry = releases[10]
|
|
1875
|
+
show_j_rz = releases[11]
|
|
1876
|
+
|
|
1877
|
+
if not (show_i_ry or show_i_rz or show_j_ry or show_j_rz):
|
|
1878
|
+
return []
|
|
1879
|
+
|
|
1880
|
+
T = self.member.T()[0:3, 0:3]
|
|
1881
|
+
local_x = T[0, 0:3]
|
|
1882
|
+
local_y = T[1, 0:3]
|
|
1883
|
+
local_z = T[2, 0:3]
|
|
1884
|
+
|
|
1885
|
+
local_x = local_x / np.linalg.norm(local_x)
|
|
1886
|
+
local_y = local_y / np.linalg.norm(local_y)
|
|
1887
|
+
local_z = local_z / np.linalg.norm(local_z)
|
|
1888
|
+
|
|
1889
|
+
radius = self.annotation_size * 0.3
|
|
1890
|
+
member_length = self.member.L()
|
|
1891
|
+
default_offset = self.annotation_size * 1.2
|
|
1892
|
+
short_offset = member_length * 0.05
|
|
1893
|
+
offset = short_offset if default_offset > short_offset else default_offset
|
|
1894
|
+
num_segments = 24
|
|
1895
|
+
|
|
1896
|
+
i_point = np.array([self.member.i_node.X, self.member.i_node.Y, self.member.i_node.Z])
|
|
1897
|
+
j_point = np.array([self.member.j_node.X, self.member.j_node.Y, self.member.j_node.Z])
|
|
1898
|
+
|
|
1899
|
+
release_meshes: List[pv.PolyData] = []
|
|
1900
|
+
|
|
1901
|
+
def add_circle(center: np.ndarray, axis1: np.ndarray, axis2: np.ndarray) -> None:
|
|
1902
|
+
angles = np.linspace(0.0, 2 * np.pi, num_segments, endpoint=False)
|
|
1903
|
+
points = [center + radius * (np.cos(a) * axis1 + np.sin(a) * axis2) for a in angles]
|
|
1904
|
+
release_meshes.append(pv.lines_from_points(np.array(points), close=True))
|
|
1905
|
+
|
|
1906
|
+
if show_i_ry:
|
|
1907
|
+
center = i_point + local_x * offset
|
|
1908
|
+
add_circle(center, local_x, local_z)
|
|
1909
|
+
|
|
1910
|
+
if show_i_rz:
|
|
1911
|
+
center = i_point + local_x * offset
|
|
1912
|
+
add_circle(center, local_x, local_y)
|
|
1913
|
+
|
|
1914
|
+
if show_j_ry:
|
|
1915
|
+
center = j_point - local_x * offset
|
|
1916
|
+
add_circle(center, local_x, local_z)
|
|
1917
|
+
|
|
1918
|
+
if show_j_rz:
|
|
1919
|
+
center = j_point - local_x * offset
|
|
1920
|
+
add_circle(center, local_x, local_y)
|
|
1921
|
+
|
|
1922
|
+
return release_meshes
|
|
1923
|
+
|
|
1860
1924
|
def add_to_plotter(self, plotter: pv.Plotter) -> None:
|
|
1861
1925
|
"""Add the member line to the plotter.
|
|
1862
1926
|
|
|
@@ -1864,6 +1928,8 @@ class VisMember:
|
|
|
1864
1928
|
"""
|
|
1865
1929
|
color = 'black' if self.theme == 'print' else 'black'
|
|
1866
1930
|
plotter.add_mesh(self.mesh, color=color, line_width=2)
|
|
1931
|
+
for release_mesh in self.release_meshes:
|
|
1932
|
+
plotter.add_mesh(release_mesh, color=color, line_width=2)
|
|
1867
1933
|
|
|
1868
1934
|
|
|
1869
1935
|
class VisDeformedMember:
|
|
@@ -214,7 +214,7 @@ class ShearWall():
|
|
|
214
214
|
# Identify the global origin for the flange mesh
|
|
215
215
|
if self.plane == 'XY':
|
|
216
216
|
Xof = self.origin[0] + x
|
|
217
|
-
Yof = self.origin[
|
|
217
|
+
Yof = self.origin[1] + y_start
|
|
218
218
|
Zof = self.origin[2] + z
|
|
219
219
|
flg_plane = 'YZ'
|
|
220
220
|
elif self.plane == 'XZ':
|
|
@@ -203,6 +203,34 @@ class VTKWriter:
|
|
|
203
203
|
ugrid_members.SetPoints(points)
|
|
204
204
|
ugrid_members.SetCells(vtk.VTK_LINE, lines)
|
|
205
205
|
|
|
206
|
+
#### MEMBER RELEASES DATA ####
|
|
207
|
+
# Add member end releases as cell data
|
|
208
|
+
member_releases = vtk.vtkIntArray()
|
|
209
|
+
member_releases.SetName("End Releases")
|
|
210
|
+
member_releases.SetNumberOfComponents(12) # 12 DOFs (Rx, Ry, Rz, Mx, My, Mz for each end)
|
|
211
|
+
|
|
212
|
+
for i, name in enumerate(["DXi", "DYi", "DZi", "RXi", "RYi", "RZi", "DXj", "DYj", "DZj", "RXj", "RYj", "RZj"]):
|
|
213
|
+
member_releases.SetComponentName(i, name)
|
|
214
|
+
|
|
215
|
+
cell_id = 0
|
|
216
|
+
for member in self.model.members.values():
|
|
217
|
+
if len(member.sub_members) == 0:
|
|
218
|
+
# Single uninformed element
|
|
219
|
+
member_releases.InsertTuple(cell_id, tuple(int(r) for r in member.Releases))
|
|
220
|
+
cell_id += 1
|
|
221
|
+
else:
|
|
222
|
+
# Member releases are defined once on the full member (i/j ends).
|
|
223
|
+
# They are NOT applied per sub-member segment. We only duplicate the
|
|
224
|
+
# same 12 release flags onto every exported line cell so the metadata is
|
|
225
|
+
# available regardless of which segment is selected in post-processing.
|
|
226
|
+
for subm in member.sub_members.values():
|
|
227
|
+
n = 11 # Number of segments
|
|
228
|
+
for _ in range(n - 1):
|
|
229
|
+
member_releases.InsertTuple(cell_id, tuple(int(r) for r in member.Releases))
|
|
230
|
+
cell_id += 1
|
|
231
|
+
|
|
232
|
+
ugrid_members.GetCellData().AddArray(member_releases)
|
|
233
|
+
|
|
206
234
|
#### MEMBER Data ####
|
|
207
235
|
for combo in self.model.load_combos.keys():
|
|
208
236
|
# Displacement
|
|
@@ -475,6 +475,10 @@ class Renderer():
|
|
|
475
475
|
# Add the actor for the member
|
|
476
476
|
renderer.AddActor(vis_member.actor)
|
|
477
477
|
|
|
478
|
+
# Add visualization for member end releases
|
|
479
|
+
for release_actor in vis_member.release_actors:
|
|
480
|
+
renderer.AddActor(release_actor)
|
|
481
|
+
|
|
478
482
|
if self.labels == True:
|
|
479
483
|
|
|
480
484
|
# Add the actor for the member label
|
|
@@ -950,6 +954,9 @@ class VisMember():
|
|
|
950
954
|
:param str theme: ``'default'`` or ``'print'`` to control colors.
|
|
951
955
|
"""
|
|
952
956
|
|
|
957
|
+
self.member = member
|
|
958
|
+
self.release_actors = []
|
|
959
|
+
|
|
953
960
|
# Generate a line for the member
|
|
954
961
|
line = vtk.vtkLineSource()
|
|
955
962
|
|
|
@@ -997,6 +1004,96 @@ class VisMember():
|
|
|
997
1004
|
self.actor.GetProperty().SetColor(0/255, 0/255, 0/255) # Black
|
|
998
1005
|
self.lblActor.GetProperty().SetColor(0/255, 0/255, 0/255) # Black
|
|
999
1006
|
|
|
1007
|
+
# Add visualization for member end releases
|
|
1008
|
+
self._create_release_actors(annotation_size)
|
|
1009
|
+
|
|
1010
|
+
def _create_release_actors(self, annotation_size):
|
|
1011
|
+
"""Create VTK actors for rotational end releases.
|
|
1012
|
+
|
|
1013
|
+
Only local y/z rotational releases are shown.
|
|
1014
|
+
|
|
1015
|
+
:param float annotation_size: Base size for symbol scaling.
|
|
1016
|
+
"""
|
|
1017
|
+
from numpy import array, cos, sin, linalg
|
|
1018
|
+
|
|
1019
|
+
releases = self.member.Releases
|
|
1020
|
+
if not releases:
|
|
1021
|
+
return
|
|
1022
|
+
|
|
1023
|
+
show_i_ry = releases[4]
|
|
1024
|
+
show_i_rz = releases[5]
|
|
1025
|
+
show_j_ry = releases[10]
|
|
1026
|
+
show_j_rz = releases[11]
|
|
1027
|
+
|
|
1028
|
+
if not (show_i_ry or show_i_rz or show_j_ry or show_j_rz):
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
T = self.member.T()[0:3, 0:3]
|
|
1032
|
+
local_x = T[0, 0:3]
|
|
1033
|
+
local_y = T[1, 0:3]
|
|
1034
|
+
local_z = T[2, 0:3]
|
|
1035
|
+
|
|
1036
|
+
local_x = local_x / linalg.norm(local_x)
|
|
1037
|
+
local_y = local_y / linalg.norm(local_y)
|
|
1038
|
+
local_z = local_z / linalg.norm(local_z)
|
|
1039
|
+
|
|
1040
|
+
radius = annotation_size * 0.3
|
|
1041
|
+
member_length = self.member.L()
|
|
1042
|
+
default_offset = annotation_size * 1.2
|
|
1043
|
+
short_offset = member_length * 0.05
|
|
1044
|
+
offset = short_offset if default_offset > short_offset else default_offset
|
|
1045
|
+
num_segments = 24
|
|
1046
|
+
|
|
1047
|
+
i_point = array([self.member.i_node.X, self.member.i_node.Y, self.member.i_node.Z])
|
|
1048
|
+
j_point = array([self.member.j_node.X, self.member.j_node.Y, self.member.j_node.Z])
|
|
1049
|
+
member_color = self.actor.GetProperty().GetColor()
|
|
1050
|
+
|
|
1051
|
+
def create_circle_actor(center, axis1, axis2):
|
|
1052
|
+
angles = linspace(0.0, 2 * 3.14159265, num_segments, endpoint=False)
|
|
1053
|
+
points = vtk.vtkPoints()
|
|
1054
|
+
lines = vtk.vtkCellArray()
|
|
1055
|
+
|
|
1056
|
+
for i, a in enumerate(angles):
|
|
1057
|
+
pt = center + radius * (cos(a) * axis1 + sin(a) * axis2)
|
|
1058
|
+
points.InsertNextPoint(pt[0], pt[1], pt[2])
|
|
1059
|
+
|
|
1060
|
+
# Close the circle
|
|
1061
|
+
for i in range(num_segments):
|
|
1062
|
+
line = vtk.vtkLine()
|
|
1063
|
+
line.GetPointIds().SetId(0, i)
|
|
1064
|
+
line.GetPointIds().SetId(1, (i + 1) % num_segments)
|
|
1065
|
+
lines.InsertNextCell(line)
|
|
1066
|
+
|
|
1067
|
+
polydata = vtk.vtkPolyData()
|
|
1068
|
+
polydata.SetPoints(points)
|
|
1069
|
+
polydata.SetLines(lines)
|
|
1070
|
+
|
|
1071
|
+
mapper = vtk.vtkPolyDataMapper()
|
|
1072
|
+
mapper.SetInputData(polydata)
|
|
1073
|
+
|
|
1074
|
+
actor = vtk.vtkActor()
|
|
1075
|
+
actor.SetMapper(mapper)
|
|
1076
|
+
actor.GetProperty().SetColor(*member_color)
|
|
1077
|
+
actor.GetProperty().SetLineWidth(2)
|
|
1078
|
+
|
|
1079
|
+
return actor
|
|
1080
|
+
|
|
1081
|
+
if show_i_ry:
|
|
1082
|
+
center = i_point + local_x * offset
|
|
1083
|
+
self.release_actors.append(create_circle_actor(center, local_x, local_z))
|
|
1084
|
+
|
|
1085
|
+
if show_i_rz:
|
|
1086
|
+
center = i_point + local_x * offset
|
|
1087
|
+
self.release_actors.append(create_circle_actor(center, local_x, local_y))
|
|
1088
|
+
|
|
1089
|
+
if show_j_ry:
|
|
1090
|
+
center = j_point - local_x * offset
|
|
1091
|
+
self.release_actors.append(create_circle_actor(center, local_x, local_z))
|
|
1092
|
+
|
|
1093
|
+
if show_j_rz:
|
|
1094
|
+
center = j_point - local_x * offset
|
|
1095
|
+
self.release_actors.append(create_circle_actor(center, local_x, local_y))
|
|
1096
|
+
|
|
1000
1097
|
# Converts a node object into a node in its deformed position for the viewer
|
|
1001
1098
|
class VisDeformedNode():
|
|
1002
1099
|
"""Sphere representing a node in its deformed position."""
|
|
@@ -68,6 +68,14 @@ Here's a list of projects that use Pynite:
|
|
|
68
68
|
* Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
|
|
69
69
|
|
|
70
70
|
# What's New?
|
|
71
|
+
v2.3.0
|
|
72
|
+
* 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.
|
|
73
|
+
* Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
|
|
74
|
+
* 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.
|
|
75
|
+
* Added bending moment end releases to visualizations.
|
|
76
|
+
* Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
|
|
77
|
+
* Rectangular meshes now filter out user defined control points that erroneously lie outside their boudaries.
|
|
78
|
+
|
|
71
79
|
v2.2.1
|
|
72
80
|
* Normalized member force diagrams across the entire model for visual clarity.
|
|
73
81
|
* Made mesh auto-regeneration smarter and more efficient, targeting only meshes that have been altered since the last run.
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="PyNiteFEA",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.3.0",
|
|
9
9
|
author="D. Craig Brinck, PE, SE",
|
|
10
10
|
author_email="Building.Code@outlook.com",
|
|
11
11
|
description="A simple elastic 3D structural finite element library for Python.",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|