PyNiteFEA 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {pynitefea-2.3.0 → pynitefea-2.4.0}/PKG-INFO +5 -2
  2. {pynitefea-2.3.0 → pynitefea-2.4.0}/PyNiteFEA.egg-info/PKG-INFO +5 -2
  3. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Member3D.py +131 -95
  4. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/PhysMember.py +310 -76
  5. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Visualization.py +3 -6
  6. {pynitefea-2.3.0 → pynitefea-2.4.0}/README.md +4 -1
  7. {pynitefea-2.3.0 → pynitefea-2.4.0}/setup.py +1 -1
  8. {pynitefea-2.3.0 → pynitefea-2.4.0}/LICENSE +0 -0
  9. {pynitefea-2.3.0 → pynitefea-2.4.0}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
  10. {pynitefea-2.3.0 → pynitefea-2.4.0}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
  11. {pynitefea-2.3.0 → pynitefea-2.4.0}/PyNiteFEA.egg-info/requires.txt +0 -0
  12. {pynitefea-2.3.0 → pynitefea-2.4.0}/PyNiteFEA.egg-info/top_level.txt +0 -0
  13. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Analysis.py +0 -0
  14. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/BeamSegY.py +0 -0
  15. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/BeamSegZ.py +0 -0
  16. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/FEModel3D.py +0 -0
  17. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/FixedEndReactions.py +0 -0
  18. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/LoadCombo.py +0 -0
  19. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/MainStyleSheet.css +0 -0
  20. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/MatFoundation.py +0 -0
  21. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Material.py +0 -0
  22. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Mesh.py +0 -0
  23. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Node3D.py +0 -0
  24. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Plate3D.py +0 -0
  25. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Quad3D.py +0 -0
  26. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Rendering.py +0 -0
  27. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Report_Template.html +0 -0
  28. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Reporting.py +0 -0
  29. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Section.py +0 -0
  30. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/ShearWall.py +0 -0
  31. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Spring3D.py +0 -0
  32. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/Tri3D.py +0 -0
  33. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/VTKWriter.py +0 -0
  34. {pynitefea-2.3.0 → pynitefea-2.4.0}/Pynite/__init__.py +0 -0
  35. {pynitefea-2.3.0 → pynitefea-2.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyNiteFEA
3
- Version: 2.3.0
3
+ Version: 2.4.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,9 +121,12 @@ 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
+ 2.4.0
125
+ * Member `max` and `min` methods now return governing load combination along with the member max/min forces.
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
+
124
128
  v2.3.0
125
129
  * 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
130
  * 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
131
  * Added bending moment end releases to visualizations.
129
132
  * Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyNiteFEA
3
- Version: 2.3.0
3
+ Version: 2.4.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,9 +121,12 @@ 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
+ 2.4.0
125
+ * Member `max` and `min` methods now return governing load combination along with the member max/min forces.
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
+
124
128
  v2.3.0
125
129
  * 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
130
  * 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
131
  * Added bending moment end releases to visualizations.
129
132
  * Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
@@ -1154,7 +1154,7 @@ class Member3D():
1154
1154
 
1155
1155
  return 0
1156
1156
 
1157
- def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1157
+ def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1158
1158
  """
1159
1159
  Returns the maximum shear force in the member for the specified direction
1160
1160
  and load combination(s).
@@ -1174,16 +1174,10 @@ class Member3D():
1174
1174
 
1175
1175
  Returns
1176
1176
  -------
1177
- float
1178
- The maximum shear value in the specified direction for the given
1179
- combination(s). Returns 0 if the member is inactive for all
1180
- specified combinations or if no segments are available.
1181
-
1182
- Notes
1183
- -----
1184
- - Unlike moments, shear does not depend on P-Delta effects.
1185
- - Automatically segments the member if it has not yet been segmented
1186
- for a load combination.
1177
+ float or tuple of (float, str)
1178
+ If a single combo name is provided, returns the maximum shear value.
1179
+ If a list of tags is provided, returns a tuple of the maximum shear
1180
+ value and the name of the governing load combination.
1187
1181
  """
1188
1182
  # Normalize input: single name vs list of tags
1189
1183
  if isinstance(combo_tags, str):
@@ -1196,6 +1190,7 @@ class Member3D():
1196
1190
  ]
1197
1191
 
1198
1192
  Vmax_global = None # will store the global maximum across combos
1193
+ governing_combo = None
1199
1194
 
1200
1195
  for combo_name in combo_names:
1201
1196
  # Skip inactive combos
@@ -1219,11 +1214,16 @@ class Member3D():
1219
1214
  # Update global maximum
1220
1215
  if Vmax_global is None or Vmax > Vmax_global:
1221
1216
  Vmax_global = Vmax
1217
+ governing_combo = combo_name
1222
1218
 
1223
- # Return 0 if no valid combos were found
1224
- return Vmax_global if Vmax_global is not None else 0
1219
+ # Return result
1220
+ if Vmax_global is None:
1221
+ Vmax_global = 0
1222
+ if isinstance(combo_tags, list):
1223
+ return (Vmax_global, governing_combo)
1224
+ return Vmax_global
1225
1225
 
1226
- def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1226
+ def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1227
1227
  """
1228
1228
  Returns the minimum shear force in the member for the specified direction
1229
1229
  and load combination(s).
@@ -1243,16 +1243,10 @@ class Member3D():
1243
1243
 
1244
1244
  Returns
1245
1245
  -------
1246
- float
1247
- The minimum shear value in the specified direction for the given
1248
- combination(s). Returns 0 if the member is inactive for all
1249
- specified combinations or if no segments are available.
1250
-
1251
- Notes
1252
- -----
1253
- - Unlike moments, shear does not depend on P-Delta effects.
1254
- - Automatically segments the member if it has not yet been segmented
1255
- for a load combination.
1246
+ float or tuple of (float, str)
1247
+ If a single combo name is provided, returns the minimum shear value.
1248
+ If a list of tags is provided, returns a tuple of the minimum shear
1249
+ value and the name of the governing load combination.
1256
1250
  """
1257
1251
  # Normalize input: single name vs list of tags
1258
1252
  if isinstance(combo_tags, str):
@@ -1265,6 +1259,7 @@ class Member3D():
1265
1259
  ]
1266
1260
 
1267
1261
  Vmin_global = None # will store the global minimum across combos
1262
+ governing_combo = None
1268
1263
 
1269
1264
  for combo_name in combo_names:
1270
1265
  # Skip inactive combos
@@ -1288,9 +1283,14 @@ class Member3D():
1288
1283
  # Update global minimum
1289
1284
  if Vmin_global is None or Vmin < Vmin_global:
1290
1285
  Vmin_global = Vmin
1286
+ governing_combo = combo_name
1291
1287
 
1292
- # Return 0 if no valid combos were found
1293
- return Vmin_global if Vmin_global is not None else 0
1288
+ # Return result
1289
+ if Vmin_global is None:
1290
+ Vmin_global = 0
1291
+ if isinstance(combo_tags, list):
1292
+ return (Vmin_global, governing_combo)
1293
+ return Vmin_global
1294
1294
 
1295
1295
  def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
1296
1296
  """
@@ -1456,7 +1456,7 @@ class Member3D():
1456
1456
 
1457
1457
  return 0
1458
1458
 
1459
- def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1459
+ def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1460
1460
  """
1461
1461
  Returns the maximum bending moment in the member for the specified direction
1462
1462
  and load combination(s).
@@ -1476,16 +1476,10 @@ class Member3D():
1476
1476
 
1477
1477
  Returns
1478
1478
  -------
1479
- float
1480
- The maximum moment value in the specified direction for the given
1481
- combination(s). Returns 0 if the member is inactive for all
1482
- specified combinations or if no segments are available.
1483
-
1484
- Notes
1485
- -----
1486
- - Includes P-Delta effects if the model's solution is 'P-Delta' or 'Pushover'.
1487
- - Automatically segments the member if it has not yet been segmented
1488
- for a load combination.
1479
+ float or tuple of (float, str)
1480
+ If a single combo name is provided, returns the maximum moment value.
1481
+ If a list of tags is provided, returns a tuple of the maximum moment
1482
+ value and the name of the governing load combination.
1489
1483
  """
1490
1484
  # Normalize input: single name vs list of tags
1491
1485
  if isinstance(combo_tags, str):
@@ -1498,6 +1492,7 @@ class Member3D():
1498
1492
  ]
1499
1493
 
1500
1494
  Mmax_global = None # will store the global maximum across combos
1495
+ governing_combo = None
1501
1496
 
1502
1497
  for combo_name in combo_names:
1503
1498
  # Skip inactive combos
@@ -1524,11 +1519,16 @@ class Member3D():
1524
1519
  # Update global maximum
1525
1520
  if Mmax_global is None or Mmax > Mmax_global:
1526
1521
  Mmax_global = Mmax
1522
+ governing_combo = combo_name
1527
1523
 
1528
- # Return 0 if no valid combos were found
1529
- return Mmax_global if Mmax_global is not None else 0
1524
+ # Return result
1525
+ if Mmax_global is None:
1526
+ Mmax_global = 0
1527
+ if isinstance(combo_tags, list):
1528
+ return (Mmax_global, governing_combo)
1529
+ return Mmax_global
1530
1530
 
1531
- def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1531
+ def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1532
1532
  """
1533
1533
  Returns the minimum bending moment in the member for the specified direction
1534
1534
  and load combination(s).
@@ -1548,16 +1548,10 @@ class Member3D():
1548
1548
 
1549
1549
  Returns
1550
1550
  -------
1551
- float
1552
- The minimum moment value in the specified direction for the given
1553
- combination(s). Returns 0 if the member is inactive for all
1554
- specified combinations or if no segments are available.
1555
-
1556
- Notes
1557
- -----
1558
- - Includes P-Delta effects if the model's solution is 'P-Delta' or 'Pushover'.
1559
- - Automatically segments the member if it has not yet been segmented
1560
- for a load combination.
1551
+ float or tuple of (float, str)
1552
+ If a single combo name is provided, returns the minimum moment value.
1553
+ If a list of tags is provided, returns a tuple of the minimum moment
1554
+ value and the name of the governing load combination.
1561
1555
  """
1562
1556
  # Normalize input: single name vs list of tags
1563
1557
  if isinstance(combo_tags, str):
@@ -1570,6 +1564,7 @@ class Member3D():
1570
1564
  ]
1571
1565
 
1572
1566
  Mmin_global = None # will store the global minimum across combos
1567
+ governing_combo = None
1573
1568
 
1574
1569
  for combo_name in combo_names:
1575
1570
  # Skip inactive combos
@@ -1596,9 +1591,14 @@ class Member3D():
1596
1591
  # Update global minimum
1597
1592
  if Mmin_global is None or Mmin < Mmin_global:
1598
1593
  Mmin_global = Mmin
1594
+ governing_combo = combo_name
1599
1595
 
1600
- # Return 0 if no valid combos were found
1601
- return Mmin_global if Mmin_global is not None else 0
1596
+ # Return result
1597
+ if Mmin_global is None:
1598
+ Mmin_global = 0
1599
+ if isinstance(combo_tags, list):
1600
+ return (Mmin_global, governing_combo)
1601
+ return Mmin_global
1602
1602
 
1603
1603
 
1604
1604
  def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
@@ -1747,7 +1747,7 @@ class Member3D():
1747
1747
 
1748
1748
  return 0
1749
1749
 
1750
- def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1750
+ def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1751
1751
  """
1752
1752
  Returns the maximum torsional moment in the member across the
1753
1753
  specified load combination(s).
@@ -1763,10 +1763,10 @@ class Member3D():
1763
1763
 
1764
1764
  Returns
1765
1765
  -------
1766
- float
1767
- The maximum torsional moment in the member for the specified
1768
- combination(s). Returns 0 if the member is inactive for all
1769
- specified combinations or if no segments are available.
1766
+ float or tuple of (float, str)
1767
+ If a single combo name is provided, returns the maximum torsion value.
1768
+ If a list of tags is provided, returns a tuple of the maximum torsion
1769
+ value and the name of the governing load combination.
1770
1770
  """
1771
1771
  # Normalize input: single combo name or list of tags
1772
1772
  if isinstance(combo_tags, str):
@@ -1779,6 +1779,7 @@ class Member3D():
1779
1779
 
1780
1780
  # Track global maximum torsion across all combos
1781
1781
  Tmax_global = None
1782
+ governing_combo = None
1782
1783
 
1783
1784
  # Loop through each candidate combo
1784
1785
  for combo_name in combo_names:
@@ -1801,12 +1802,17 @@ class Member3D():
1801
1802
  # Update global maximum
1802
1803
  if Tmax_global is None or Tmax > Tmax_global:
1803
1804
  Tmax_global = Tmax
1805
+ governing_combo = combo_name
1804
1806
 
1805
- # Return global maximum or 0 if nothing found
1806
- return Tmax_global if Tmax_global is not None else 0
1807
+ # Return result
1808
+ if Tmax_global is None:
1809
+ Tmax_global = 0
1810
+ if isinstance(combo_tags, list):
1811
+ return (Tmax_global, governing_combo)
1812
+ return Tmax_global
1807
1813
 
1808
1814
 
1809
- def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1815
+ def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1810
1816
  """
1811
1817
  Returns the minimum torsional moment in the member across the
1812
1818
  specified load combination(s).
@@ -1822,10 +1828,10 @@ class Member3D():
1822
1828
 
1823
1829
  Returns
1824
1830
  -------
1825
- float
1826
- The minimum torsional moment in the member for the specified
1827
- combination(s). Returns 0 if the member is inactive for all
1828
- specified combinations or if no segments are available.
1831
+ float or tuple of (float, str)
1832
+ If a single combo name is provided, returns the minimum torsion value.
1833
+ If a list of tags is provided, returns a tuple of the minimum torsion
1834
+ value and the name of the governing load combination.
1829
1835
  """
1830
1836
  # Normalize input: single combo name or list of tags
1831
1837
  if isinstance(combo_tags, str):
@@ -1838,6 +1844,7 @@ class Member3D():
1838
1844
 
1839
1845
  # Track global minimum torsion across all combos
1840
1846
  Tmin_global = None
1847
+ governing_combo = None
1841
1848
 
1842
1849
  # Loop through each candidate combo
1843
1850
  for combo_name in combo_names:
@@ -1860,9 +1867,14 @@ class Member3D():
1860
1867
  # Update global minimum
1861
1868
  if Tmin_global is None or Tmin < Tmin_global:
1862
1869
  Tmin_global = Tmin
1870
+ governing_combo = combo_name
1863
1871
 
1864
- # Return global minimum or 0 if nothing found
1865
- return Tmin_global if Tmin_global is not None else 0
1872
+ # Return result
1873
+ if Tmin_global is None:
1874
+ Tmin_global = 0
1875
+ if isinstance(combo_tags, list):
1876
+ return (Tmin_global, governing_combo)
1877
+ return Tmin_global
1866
1878
 
1867
1879
  def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
1868
1880
  """
@@ -1980,7 +1992,7 @@ class Member3D():
1980
1992
 
1981
1993
  return 0
1982
1994
 
1983
- def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
1995
+ def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
1984
1996
  """
1985
1997
  Returns the maximum axial force in the member across the specified
1986
1998
  load combination(s).
@@ -1996,10 +2008,10 @@ class Member3D():
1996
2008
 
1997
2009
  Returns
1998
2010
  -------
1999
- float
2000
- The maximum axial force in the member for the specified
2001
- combination(s). Returns 0 if the member is inactive for all
2002
- specified combinations or if no segments are available.
2011
+ float or tuple of (float, str)
2012
+ If a single combo name is provided, returns the maximum axial value.
2013
+ If a list of tags is provided, returns a tuple of the maximum axial
2014
+ value and the name of the governing load combination.
2003
2015
  """
2004
2016
  # Normalize input: if a string is passed, treat it as a single combo name
2005
2017
  if isinstance(combo_tags, str):
@@ -2013,6 +2025,7 @@ class Member3D():
2013
2025
 
2014
2026
  # Track the global maximum axial force across all combos
2015
2027
  Pmax_global = None
2028
+ governing_combo = None
2016
2029
 
2017
2030
  # Loop through each candidate combo
2018
2031
  for combo_name in combo_names:
@@ -2035,11 +2048,16 @@ class Member3D():
2035
2048
  # Update the global maximum
2036
2049
  if Pmax_global is None or Pmax > Pmax_global:
2037
2050
  Pmax_global = Pmax
2051
+ governing_combo = combo_name
2038
2052
 
2039
- # Return the global maximum, or 0 if nothing was found
2040
- return Pmax_global if Pmax_global is not None else 0
2053
+ # Return result
2054
+ if Pmax_global is None:
2055
+ Pmax_global = 0
2056
+ if isinstance(combo_tags, list):
2057
+ return (Pmax_global, governing_combo)
2058
+ return Pmax_global
2041
2059
 
2042
- def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
2060
+ def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
2043
2061
  """
2044
2062
  Returns the minimum axial force in the member across the specified
2045
2063
  load combination(s).
@@ -2055,10 +2073,10 @@ class Member3D():
2055
2073
 
2056
2074
  Returns
2057
2075
  -------
2058
- float
2059
- The minimum axial force in the member for the specified
2060
- combination(s). Returns 0 if the member is inactive for all
2061
- specified combinations or if no segments are available.
2076
+ float or tuple of (float, str)
2077
+ If a single combo name is provided, returns the minimum axial value.
2078
+ If a list of tags is provided, returns a tuple of the minimum axial
2079
+ value and the name of the governing load combination.
2062
2080
  """
2063
2081
  # Normalize input: if a string is passed, treat it as a single combo name
2064
2082
  if isinstance(combo_tags, str):
@@ -2072,6 +2090,7 @@ class Member3D():
2072
2090
 
2073
2091
  # Track the global minimum axial force across all combos
2074
2092
  Pmin_global = None
2093
+ governing_combo = None
2075
2094
 
2076
2095
  # Loop through each candidate combo
2077
2096
  for combo_name in combo_names:
@@ -2094,9 +2113,14 @@ class Member3D():
2094
2113
  # Update the global minimum
2095
2114
  if Pmin_global is None or Pmin < Pmin_global:
2096
2115
  Pmin_global = Pmin
2116
+ governing_combo = combo_name
2097
2117
 
2098
- # Return the global minimum, or 0 if nothing was found
2099
- return Pmin_global if Pmin_global is not None else 0
2118
+ # Return result
2119
+ if Pmin_global is None:
2120
+ Pmin_global = 0
2121
+ if isinstance(combo_tags, list):
2122
+ return (Pmin_global, governing_combo)
2123
+ return Pmin_global
2100
2124
 
2101
2125
  def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
2102
2126
  """
@@ -2257,7 +2281,7 @@ class Member3D():
2257
2281
 
2258
2282
  return 0
2259
2283
 
2260
- def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
2284
+ def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
2261
2285
  """
2262
2286
  Returns the maximum deflection in the member across the specified
2263
2287
  load combination(s).
@@ -2278,10 +2302,10 @@ class Member3D():
2278
2302
 
2279
2303
  Returns
2280
2304
  -------
2281
- float
2282
- The maximum deflection in the member for the specified
2283
- combination(s). Returns 0 if the member is inactive for all
2284
- specified combinations.
2305
+ float or tuple of (float, str)
2306
+ If a single combo name is provided, returns the maximum deflection.
2307
+ If a list of tags is provided, returns a tuple of the maximum
2308
+ deflection and the name of the governing load combination.
2285
2309
  """
2286
2310
  # Normalize input: single name or list of tags
2287
2311
  if isinstance(combo_tags, str):
@@ -2294,6 +2318,7 @@ class Member3D():
2294
2318
 
2295
2319
  # Track global maximum deflection across all combos
2296
2320
  dmax_global = None
2321
+ governing_combo = None
2297
2322
 
2298
2323
  # Loop through each candidate combo
2299
2324
  for combo_name in combo_names:
@@ -2318,12 +2343,17 @@ class Member3D():
2318
2343
  # Update global maximum
2319
2344
  if dmax_global is None or dmax > dmax_global:
2320
2345
  dmax_global = dmax
2346
+ governing_combo = combo_name
2321
2347
 
2322
- # Return global maximum or 0 if nothing found
2323
- return dmax_global if dmax_global is not None else 0
2348
+ # Return result
2349
+ if dmax_global is None:
2350
+ dmax_global = 0
2351
+ if isinstance(combo_tags, list):
2352
+ return (dmax_global, governing_combo)
2353
+ return dmax_global
2324
2354
 
2325
2355
 
2326
- def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> float:
2356
+ def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
2327
2357
  """
2328
2358
  Returns the minimum deflection in the member across the specified
2329
2359
  load combination(s).
@@ -2344,10 +2374,10 @@ class Member3D():
2344
2374
 
2345
2375
  Returns
2346
2376
  -------
2347
- float
2348
- The minimum deflection in the member for the specified
2349
- combination(s). Returns 0 if the member is inactive for all
2350
- specified combinations.
2377
+ float or tuple of (float, str)
2378
+ If a single combo name is provided, returns the minimum deflection.
2379
+ If a list of tags is provided, returns a tuple of the minimum
2380
+ deflection and the name of the governing load combination.
2351
2381
  """
2352
2382
  # Normalize input: single name or list of tags
2353
2383
  if isinstance(combo_tags, str):
@@ -2360,6 +2390,7 @@ class Member3D():
2360
2390
 
2361
2391
  # Track global minimum deflection across all combos
2362
2392
  dmin_global = None
2393
+ governing_combo = None
2363
2394
 
2364
2395
  # Loop through each candidate combo
2365
2396
  for combo_name in combo_names:
@@ -2384,9 +2415,14 @@ class Member3D():
2384
2415
  # Update global minimum
2385
2416
  if dmin_global is None or dmin < dmin_global:
2386
2417
  dmin_global = dmin
2387
-
2388
- # Return global minimum or 0 if nothing found
2389
- return dmin_global if dmin_global is not None else 0
2418
+ governing_combo = combo_name
2419
+
2420
+ # Return result
2421
+ if dmin_global is None:
2422
+ dmin_global = 0
2423
+ if isinstance(combo_tags, list):
2424
+ return (dmin_global, governing_combo)
2425
+ return dmin_global
2390
2426
 
2391
2427
  def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
2392
2428
  """
@@ -215,9 +215,9 @@ class PhysMember(Member3D):
215
215
  member, x_mod = self.find_member(x)
216
216
  return member.shear(Direction, x_mod, combo_name)
217
217
 
218
- def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
218
+ def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
219
219
  """
220
- Returns the maximum shear in the member for the given direction
220
+ Returns the maximum shear in the member for the given direction.
221
221
 
222
222
  Parameters
223
223
  ----------
@@ -225,20 +225,41 @@ class PhysMember(Member3D):
225
225
  The direction in which to find the maximum shear. Must be one of the following:
226
226
  'Fy' = Shear acting on the local y-axis
227
227
  'Fz' = Shear acting on the local z-axis
228
- combo_name : string
229
- The name of the load combination to get the results for (not the combination itself).
228
+ combo_tags : str or list of str
229
+ A single load combination name, or a list of combo tags.
230
+
231
+ Returns
232
+ -------
233
+ float or tuple of (float, str)
234
+ If a single combo name is provided, returns the maximum shear value.
235
+ If a list of tags is provided, returns a tuple of the maximum shear
236
+ value and the name of the governing load combination.
230
237
  """
231
238
 
239
+ if isinstance(combo_tags, str):
240
+ combo_names = [combo_tags]
241
+ else:
242
+ combo_names = [name for name, combo in self.model.load_combos.items()
243
+ if any(tag in combo.combo_tags for tag in combo_tags)]
244
+
232
245
  Vmax = None
233
- for member in self.sub_members.values():
234
- V = member.max_shear(Direction, combo_name)
235
- if Vmax is None or V > Vmax:
236
- Vmax = V
246
+ governing_combo = None
247
+ for name in combo_names:
248
+ for member in self.sub_members.values():
249
+ V = member.max_shear(Direction, name)
250
+ if Vmax is None or V > Vmax:
251
+ Vmax = V
252
+ governing_combo = name
253
+
254
+ if Vmax is None:
255
+ Vmax = 0
256
+ if isinstance(combo_tags, list):
257
+ return (Vmax, governing_combo)
237
258
  return Vmax
238
259
 
239
- def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
260
+ def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
240
261
  """
241
- Returns the minimum shear in the member for the given direction
262
+ Returns the minimum shear in the member for the given direction.
242
263
 
243
264
  Parameters
244
265
  ----------
@@ -246,15 +267,36 @@ class PhysMember(Member3D):
246
267
  The direction in which to find the minimum shear. Must be one of the following:
247
268
  'Fy' = Shear acting on the local y-axis
248
269
  'Fz' = Shear acting on the local z-axis
249
- combo_name : string
250
- The name of the load combination to get the results for (not the load combination itself).
270
+ combo_tags : str or list of str
271
+ A single load combination name, or a list of combo tags.
272
+
273
+ Returns
274
+ -------
275
+ float or tuple of (float, str)
276
+ If a single combo name is provided, returns the minimum shear value.
277
+ If a list of tags is provided, returns a tuple of the minimum shear
278
+ value and the name of the governing load combination.
251
279
  """
252
280
 
281
+ if isinstance(combo_tags, str):
282
+ combo_names = [combo_tags]
283
+ else:
284
+ combo_names = [name for name, combo in self.model.load_combos.items()
285
+ if any(tag in combo.combo_tags for tag in combo_tags)]
286
+
253
287
  Vmin = None
254
- for member in self.sub_members.values():
255
- V = member.min_shear(Direction, combo_name)
256
- if Vmin is None or V < Vmin:
257
- Vmin = V
288
+ governing_combo = None
289
+ for name in combo_names:
290
+ for member in self.sub_members.values():
291
+ V = member.min_shear(Direction, name)
292
+ if Vmin is None or V < Vmin:
293
+ Vmin = V
294
+ governing_combo = name
295
+
296
+ if Vmin is None:
297
+ Vmin = 0
298
+ if isinstance(combo_tags, list):
299
+ return (Vmin, governing_combo)
258
300
  return Vmin
259
301
 
260
302
  def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
@@ -405,46 +447,88 @@ class PhysMember(Member3D):
405
447
  member, x_mod = self.find_member(x)
406
448
  return member.moment(Direction, x_mod, combo_name)
407
449
 
408
- def max_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
450
+ def max_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
409
451
  """
410
452
  Returns the maximum moment in the member for the given direction.
411
-
453
+
412
454
  Parameters
413
455
  ----------
414
456
  Direction : string
415
457
  The direction in which to find the maximum moment. Must be one of the following:
416
458
  'My' = Moment about the local y-axis.
417
459
  'Mz' = Moment about the local z-axis.
418
- combo_name : string
419
- The name of the load combination to get the results for (not the combination itself).
460
+ combo_tags : str or list of str
461
+ A single load combination name, or a list of combo tags.
462
+
463
+ Returns
464
+ -------
465
+ float or tuple of (float, str)
466
+ If a single combo name is provided, returns the maximum moment value.
467
+ If a list of tags is provided, returns a tuple of the maximum moment
468
+ value and the name of the governing load combination.
420
469
  """
421
470
 
471
+ if isinstance(combo_tags, str):
472
+ combo_names = [combo_tags]
473
+ else:
474
+ combo_names = [name for name, combo in self.model.load_combos.items()
475
+ if any(tag in combo.combo_tags for tag in combo_tags)]
476
+
422
477
  Mmax = None
423
- for member in self.sub_members.values():
424
- M = member.max_moment(Direction, combo_name)
425
- if Mmax is None or M > Mmax:
426
- Mmax = M
478
+ governing_combo = None
479
+ for name in combo_names:
480
+ for member in self.sub_members.values():
481
+ M = member.max_moment(Direction, name)
482
+ if Mmax is None or M > Mmax:
483
+ Mmax = M
484
+ governing_combo = name
485
+
486
+ if Mmax is None:
487
+ Mmax = 0
488
+ if isinstance(combo_tags, list):
489
+ return (Mmax, governing_combo)
427
490
  return Mmax
428
491
 
429
- def min_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
492
+ def min_moment(self, Direction: Literal['My', 'Mz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
430
493
  """
431
- Returns the minimum moment in the member for the given direction
432
-
494
+ Returns the minimum moment in the member for the given direction.
495
+
433
496
  Parameters
434
497
  ----------
435
498
  Direction : string
436
499
  The direction in which to find the minimum moment. Must be one of the following:
437
500
  'My' = Moment about the local y-axis.
438
501
  'Mz' = Moment about the local z-axis.
439
- combo_name : string
440
- The name of the load combination to get the results for (not the load combination itself).
502
+ combo_tags : str or list of str
503
+ A single load combination name, or a list of combo tags.
504
+
505
+ Returns
506
+ -------
507
+ float or tuple of (float, str)
508
+ If a single combo name is provided, returns the minimum moment value.
509
+ If a list of tags is provided, returns a tuple of the minimum moment
510
+ value and the name of the governing load combination.
441
511
  """
442
-
512
+
513
+ if isinstance(combo_tags, str):
514
+ combo_names = [combo_tags]
515
+ else:
516
+ combo_names = [name for name, combo in self.model.load_combos.items()
517
+ if any(tag in combo.combo_tags for tag in combo_tags)]
518
+
443
519
  Mmin = None
444
- for member in self.sub_members.values():
445
- M = member.min_moment(Direction, combo_name)
446
- if Mmin is None or M < Mmin:
447
- Mmin = M
520
+ governing_combo = None
521
+ for name in combo_names:
522
+ for member in self.sub_members.values():
523
+ M = member.min_moment(Direction, name)
524
+ if Mmin is None or M < Mmin:
525
+ Mmin = M
526
+ governing_combo = name
527
+
528
+ if Mmin is None:
529
+ Mmin = 0
530
+ if isinstance(combo_tags, list):
531
+ return (Mmin, governing_combo)
448
532
  return Mmin
449
533
 
450
534
  def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
@@ -598,30 +682,80 @@ class PhysMember(Member3D):
598
682
  member, x_mod = self.find_member(x)
599
683
  return member.torque(x_mod, combo_name)
600
684
 
601
- def max_torque(self, combo_name: str = 'Combo 1') -> float:
602
-
685
+ def max_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
686
+ """
687
+ Returns the maximum torsional moment in the member.
688
+
689
+ Parameters
690
+ ----------
691
+ combo_tags : str or list of str
692
+ A single load combination name, or a list of combo tags.
693
+
694
+ Returns
695
+ -------
696
+ float or tuple of (float, str)
697
+ If a single combo name is provided, returns the maximum torque value.
698
+ If a list of tags is provided, returns a tuple of the maximum torque
699
+ value and the name of the governing load combination.
700
+ """
701
+
702
+ if isinstance(combo_tags, str):
703
+ combo_names = [combo_tags]
704
+ else:
705
+ combo_names = [name for name, combo in self.model.load_combos.items()
706
+ if any(tag in combo.combo_tags for tag in combo_tags)]
707
+
603
708
  Tmax = None
604
- for member in self.sub_members.values():
605
- T = member.max_torque(combo_name)
606
- if Tmax is None or T > Tmax:
607
- Tmax = T
709
+ governing_combo = None
710
+ for name in combo_names:
711
+ for member in self.sub_members.values():
712
+ T = member.max_torque(name)
713
+ if Tmax is None or T > Tmax:
714
+ Tmax = T
715
+ governing_combo = name
716
+
717
+ if Tmax is None:
718
+ Tmax = 0
719
+ if isinstance(combo_tags, list):
720
+ return (Tmax, governing_combo)
608
721
  return Tmax
609
722
 
610
- def min_torque(self, combo_name: str = 'Combo 1') -> float:
723
+ def min_torque(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
611
724
  """
612
725
  Returns the minimum torsional moment in the member.
613
726
 
614
727
  Parameters
615
728
  ----------
616
- combo_name : string
617
- The name of the load combination to get the results for (not the load combination itself).
729
+ combo_tags : str or list of str
730
+ A single load combination name, or a list of combo tags.
731
+
732
+ Returns
733
+ -------
734
+ float or tuple of (float, str)
735
+ If a single combo name is provided, returns the minimum torque value.
736
+ If a list of tags is provided, returns a tuple of the minimum torque
737
+ value and the name of the governing load combination.
618
738
  """
619
739
 
740
+ if isinstance(combo_tags, str):
741
+ combo_names = [combo_tags]
742
+ else:
743
+ combo_names = [name for name, combo in self.model.load_combos.items()
744
+ if any(tag in combo.combo_tags for tag in combo_tags)]
745
+
620
746
  Tmin = None
621
- for member in self.sub_members.values():
622
- T = member.min_torque(combo_name)
623
- if Tmin is None or T < Tmin:
624
- Tmin = T
747
+ governing_combo = None
748
+ for name in combo_names:
749
+ for member in self.sub_members.values():
750
+ T = member.min_torque(name)
751
+ if Tmin is None or T < Tmin:
752
+ Tmin = T
753
+ governing_combo = name
754
+
755
+ if Tmin is None:
756
+ Tmin = 0
757
+ if isinstance(combo_tags, list):
758
+ return (Tmin, governing_combo)
625
759
  return Tmin
626
760
 
627
761
  def plot_torque(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
@@ -756,22 +890,80 @@ class PhysMember(Member3D):
756
890
  member, x_mod = self.find_member(x)
757
891
  return member.axial(x_mod, combo_name)
758
892
 
759
- def max_axial(self, combo_name: str = 'Combo 1') -> float:
760
-
893
+ def max_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
894
+ """
895
+ Returns the maximum axial force in the member.
896
+
897
+ Parameters
898
+ ----------
899
+ combo_tags : str or list of str
900
+ A single load combination name, or a list of combo tags.
901
+
902
+ Returns
903
+ -------
904
+ float or tuple of (float, str)
905
+ If a single combo name is provided, returns the maximum axial value.
906
+ If a list of tags is provided, returns a tuple of the maximum axial
907
+ value and the name of the governing load combination.
908
+ """
909
+
910
+ if isinstance(combo_tags, str):
911
+ combo_names = [combo_tags]
912
+ else:
913
+ combo_names = [name for name, combo in self.model.load_combos.items()
914
+ if any(tag in combo.combo_tags for tag in combo_tags)]
915
+
761
916
  Pmax = None
762
- for member in self.sub_members.values():
763
- P = member.max_axial(combo_name)
764
- if Pmax is None or P > Pmax:
765
- Pmax = P
917
+ governing_combo = None
918
+ for name in combo_names:
919
+ for member in self.sub_members.values():
920
+ P = member.max_axial(name)
921
+ if Pmax is None or P > Pmax:
922
+ Pmax = P
923
+ governing_combo = name
924
+
925
+ if Pmax is None:
926
+ Pmax = 0
927
+ if isinstance(combo_tags, list):
928
+ return (Pmax, governing_combo)
766
929
  return Pmax
767
930
 
768
- def min_axial(self, combo_name: str = 'Combo 1') -> float:
931
+ def min_axial(self, combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
932
+ """
933
+ Returns the minimum axial force in the member.
934
+
935
+ Parameters
936
+ ----------
937
+ combo_tags : str or list of str
938
+ A single load combination name, or a list of combo tags.
939
+
940
+ Returns
941
+ -------
942
+ float or tuple of (float, str)
943
+ If a single combo name is provided, returns the minimum axial value.
944
+ If a list of tags is provided, returns a tuple of the minimum axial
945
+ value and the name of the governing load combination.
946
+ """
947
+
948
+ if isinstance(combo_tags, str):
949
+ combo_names = [combo_tags]
950
+ else:
951
+ combo_names = [name for name, combo in self.model.load_combos.items()
952
+ if any(tag in combo.combo_tags for tag in combo_tags)]
769
953
 
770
954
  Pmin = None
771
- for member in self.sub_members.values():
772
- P = member.min_axial(combo_name)
773
- if Pmin is None or P < Pmin:
774
- Pmin = P
955
+ governing_combo = None
956
+ for name in combo_names:
957
+ for member in self.sub_members.values():
958
+ P = member.min_axial(name)
959
+ if Pmin is None or P < Pmin:
960
+ Pmin = P
961
+ governing_combo = name
962
+
963
+ if Pmin is None:
964
+ Pmin = 0
965
+ if isinstance(combo_tags, list):
966
+ return (Pmin, governing_combo)
775
967
  return Pmin
776
968
 
777
969
  def plot_axial(self, combo_name: Union[str, List[str]] = 'Combo 1', n_points: int = 20) -> None:
@@ -910,42 +1102,84 @@ class PhysMember(Member3D):
910
1102
  member, x_mod = self.find_member(x)
911
1103
  return member.deflection(Direction, x_mod, combo_name)
912
1104
 
913
- def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
1105
+ def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
914
1106
  """
915
1107
  Returns the maximum deflection in the member.
916
-
1108
+
917
1109
  Parameters
918
1110
  ----------
919
- Direction : {'dy', 'dz'}
1111
+ Direction : {'dx', 'dy', 'dz'}
920
1112
  The direction in which to find the maximum deflection.
921
- combo_name : string
922
- The name of the load combination to get the results for (not the load combination itself).
1113
+ combo_tags : str or list of str
1114
+ A single load combination name, or a list of combo tags.
1115
+
1116
+ Returns
1117
+ -------
1118
+ float or tuple of (float, str)
1119
+ If a single combo name is provided, returns the maximum deflection.
1120
+ If a list of tags is provided, returns a tuple of the maximum
1121
+ deflection and the name of the governing load combination.
923
1122
  """
924
1123
 
1124
+ if isinstance(combo_tags, str):
1125
+ combo_names = [combo_tags]
1126
+ else:
1127
+ combo_names = [name for name, combo in self.model.load_combos.items()
1128
+ if any(tag in combo.combo_tags for tag in combo_tags)]
1129
+
925
1130
  dmax = None
926
- for member in self.sub_members.values():
927
- d = member.max_deflection(Direction, combo_name)
928
- if dmax is None or d > dmax:
929
- dmax = d
1131
+ governing_combo = None
1132
+ for name in combo_names:
1133
+ for member in self.sub_members.values():
1134
+ d = member.max_deflection(Direction, name)
1135
+ if dmax is None or d > dmax:
1136
+ dmax = d
1137
+ governing_combo = name
1138
+
1139
+ if dmax is None:
1140
+ dmax = 0
1141
+ if isinstance(combo_tags, list):
1142
+ return (dmax, governing_combo)
930
1143
  return dmax
931
1144
 
932
- def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
1145
+ def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_tags: Union[str, List[str]] = 'Combo 1') -> Union[float, tuple[float, str]]:
933
1146
  """
934
1147
  Returns the minimum deflection in the member.
935
-
1148
+
936
1149
  Parameters
937
1150
  ----------
938
- Direction : {'dy', 'dz'}
1151
+ Direction : {'dx', 'dy', 'dz'}
939
1152
  The direction in which to find the minimum deflection.
940
- combo_name : string
941
- The name of the load combination to get the results for (not the load combination itself).
1153
+ combo_tags : str or list of str
1154
+ A single load combination name, or a list of combo tags.
1155
+
1156
+ Returns
1157
+ -------
1158
+ float or tuple of (float, str)
1159
+ If a single combo name is provided, returns the minimum deflection.
1160
+ If a list of tags is provided, returns a tuple of the minimum
1161
+ deflection and the name of the governing load combination.
942
1162
  """
943
1163
 
1164
+ if isinstance(combo_tags, str):
1165
+ combo_names = [combo_tags]
1166
+ else:
1167
+ combo_names = [name for name, combo in self.model.load_combos.items()
1168
+ if any(tag in combo.combo_tags for tag in combo_tags)]
1169
+
944
1170
  dmin = None
945
- for member in self.sub_members.values():
946
- d = member.min_deflection(Direction, combo_name)
947
- if dmin is None or d < dmin:
948
- dmin = d
1171
+ governing_combo = None
1172
+ for name in combo_names:
1173
+ for member in self.sub_members.values():
1174
+ d = member.min_deflection(Direction, name)
1175
+ if dmin is None or d < dmin:
1176
+ dmin = d
1177
+ governing_combo = name
1178
+
1179
+ if dmin is None:
1180
+ dmin = 0
1181
+ if isinstance(combo_tags, list):
1182
+ return (dmin, governing_combo)
949
1183
  return dmin
950
1184
 
951
1185
  def rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: str = 'Combo 1') -> float:
@@ -1599,12 +1599,9 @@ def _PrepContour(model, stress_type='Mx', combo_name='Combo 1'):
1599
1599
 
1600
1600
  # Determine which stress result has been requested by the user
1601
1601
  if stress_type == 'dz':
1602
- # Internally Pynite defines the nodes for a rectangular element in the order (i, j, m, n),
1603
- # while it defines the nodes for a quadrilateral element in the order (m, n, i, j)
1604
- if element.type == 'Rect':
1605
- i, j, m, n = element.d(combo_name)[[2, 8, 14, 20], :]
1606
- else:
1607
- i, j, m, n = element.d(combo_name)[[14, 20, 2, 8], :]
1602
+ # Both rectangular and quadrilateral elements store local displacements in the
1603
+ # order (i, j, m, n) in their d() vectors. Local DZ is at indices 2, 8, 14, 20.
1604
+ i, j, m, n = element.d(combo_name)[[2, 8, 14, 20], :]
1608
1605
  element.i_node.contour.append(i)
1609
1606
  element.j_node.contour.append(j)
1610
1607
  element.m_node.contour.append(m)
@@ -68,9 +68,12 @@ 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
+ 2.4.0
72
+ * Member `max` and `min` methods now return governing load combination along with the member max/min forces.
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
+
71
75
  v2.3.0
72
76
  * 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
77
  * 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
78
  * Added bending moment end releases to visualizations.
76
79
  * Bug fix for shear walls with flanges having an origin with their origin at Y != 0.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="PyNiteFEA",
8
- version="2.3.0",
8
+ version="2.4.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