PyNiteFEA 2.3.0__tar.gz → 2.4.1__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.1}/PKG-INFO +8 -2
  2. {pynitefea-2.3.0 → pynitefea-2.4.1}/PyNiteFEA.egg-info/PKG-INFO +8 -2
  3. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Member3D.py +145 -109
  4. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/PhysMember.py +315 -81
  5. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Visualization.py +3 -6
  6. {pynitefea-2.3.0 → pynitefea-2.4.1}/README.md +7 -1
  7. {pynitefea-2.3.0 → pynitefea-2.4.1}/setup.py +1 -1
  8. {pynitefea-2.3.0 → pynitefea-2.4.1}/LICENSE +0 -0
  9. {pynitefea-2.3.0 → pynitefea-2.4.1}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
  10. {pynitefea-2.3.0 → pynitefea-2.4.1}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
  11. {pynitefea-2.3.0 → pynitefea-2.4.1}/PyNiteFEA.egg-info/requires.txt +0 -0
  12. {pynitefea-2.3.0 → pynitefea-2.4.1}/PyNiteFEA.egg-info/top_level.txt +0 -0
  13. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Analysis.py +0 -0
  14. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/BeamSegY.py +0 -0
  15. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/BeamSegZ.py +0 -0
  16. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/FEModel3D.py +0 -0
  17. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/FixedEndReactions.py +0 -0
  18. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/LoadCombo.py +0 -0
  19. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/MainStyleSheet.css +0 -0
  20. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/MatFoundation.py +0 -0
  21. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Material.py +0 -0
  22. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Mesh.py +0 -0
  23. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Node3D.py +0 -0
  24. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Plate3D.py +0 -0
  25. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Quad3D.py +0 -0
  26. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Rendering.py +0 -0
  27. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Report_Template.html +0 -0
  28. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Reporting.py +0 -0
  29. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Section.py +0 -0
  30. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/ShearWall.py +0 -0
  31. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Spring3D.py +0 -0
  32. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/Tri3D.py +0 -0
  33. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/VTKWriter.py +0 -0
  34. {pynitefea-2.3.0 → pynitefea-2.4.1}/Pynite/__init__.py +0 -0
  35. {pynitefea-2.3.0 → pynitefea-2.4.1}/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.1
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,15 @@ 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.1
125
+ * Fixed a bug where enveloped results did not work if both tagged and untagged combos were in a model.
126
+
127
+ 2.4.0
128
+ * Member `max` and `min` methods now return governing load combination along with the member max/min forces.
129
+ * Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
130
+
124
131
  v2.3.0
125
132
  * 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
133
  * 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
134
  * Added bending moment end releases to visualizations.
129
135
  * 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.1
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,15 @@ 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.1
125
+ * Fixed a bug where enveloped results did not work if both tagged and untagged combos were in a model.
126
+
127
+ 2.4.0
128
+ * Member `max` and `min` methods now return governing load combination along with the member max/min forces.
129
+ * Bug fix: Quadrilateral element displacements had corners swapped during VTK visualization. This was a relic from the old MITC4 formulation that has been corrected.
130
+
124
131
  v2.3.0
125
132
  * 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
133
  * 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
134
  * Added bending moment end releases to visualizations.
129
135
  * 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):
@@ -1192,10 +1186,11 @@ class Member3D():
1192
1186
  # Filter combos that contain *any* of the given tags
1193
1187
  combo_names = [
1194
1188
  name for name, combo in self.model.load_combos.items()
1195
- if any(tag in combo.combo_tags for tag in combo_tags)
1189
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
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):
@@ -1261,10 +1255,11 @@ class Member3D():
1261
1255
  # Filter combos that contain *any* of the given tags
1262
1256
  combo_names = [
1263
1257
  name for name, combo in self.model.load_combos.items()
1264
- if any(tag in combo.combo_tags for tag in combo_tags)
1258
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
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
  """
@@ -1319,7 +1319,7 @@ class Member3D():
1319
1319
  combo_names = [combo_name]
1320
1320
  else:
1321
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)]
1322
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_name)]
1323
1323
 
1324
1324
  fig, ax = Member3D.__plt.subplots()
1325
1325
  ax.axhline(0, color='black', lw=1)
@@ -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):
@@ -1494,10 +1488,11 @@ class Member3D():
1494
1488
  # Filter combos that contain *any* of the given tags
1495
1489
  combo_names = [
1496
1490
  name for name, combo in self.model.load_combos.items()
1497
- if any(tag in combo.combo_tags for tag in combo_tags)
1491
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
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):
@@ -1566,10 +1560,11 @@ class Member3D():
1566
1560
  # Filter combos that contain *any* of the given tags
1567
1561
  combo_names = [
1568
1562
  name for name, combo in self.model.load_combos.items()
1569
- if any(tag in combo.combo_tags for tag in combo_tags)
1563
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
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:
@@ -1628,7 +1628,7 @@ class Member3D():
1628
1628
  combo_names = [combo_name]
1629
1629
  else:
1630
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)]
1631
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_name)]
1632
1632
 
1633
1633
  fig, ax = Member3D.__plt.subplots()
1634
1634
  ax.axhline(0, color='black', lw=1)
@@ -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):
@@ -1774,11 +1774,12 @@ class Member3D():
1774
1774
  else:
1775
1775
  combo_names = [
1776
1776
  name for name, combo in self.model.load_combos.items()
1777
- if any(tag in combo.combo_tags for tag in combo_tags)
1777
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
1778
1778
  ]
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):
@@ -1833,11 +1839,12 @@ class Member3D():
1833
1839
  else:
1834
1840
  combo_names = [
1835
1841
  name for name, combo in self.model.load_combos.items()
1836
- if any(tag in combo.combo_tags for tag in combo_tags)
1842
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
1837
1843
  ]
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
  """
@@ -1887,7 +1899,7 @@ class Member3D():
1887
1899
  combo_names = [combo_name]
1888
1900
  else:
1889
1901
  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)]
1902
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_name)]
1891
1903
 
1892
1904
  fig, ax = Member3D.__plt.subplots()
1893
1905
  ax.axhline(0, color='black', lw=1)
@@ -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):
@@ -2008,11 +2020,12 @@ class Member3D():
2008
2020
  # If tags are provided, gather all combos that match ANY of the tags
2009
2021
  combo_names = [
2010
2022
  name for name, combo in self.model.load_combos.items()
2011
- if any(tag in combo.combo_tags for tag in combo_tags)
2023
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
2012
2024
  ]
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):
@@ -2067,11 +2085,12 @@ class Member3D():
2067
2085
  # If tags are provided, gather all combos that match ANY of the tags
2068
2086
  combo_names = [
2069
2087
  name for name, combo in self.model.load_combos.items()
2070
- if any(tag in combo.combo_tags for tag in combo_tags)
2088
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
2071
2089
  ]
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
  """
@@ -2121,7 +2145,7 @@ class Member3D():
2121
2145
  combo_names = [combo_name]
2122
2146
  else:
2123
2147
  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)]
2148
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_name)]
2125
2149
 
2126
2150
  fig, ax = Member3D.__plt.subplots()
2127
2151
  ax.axhline(0, color='black', lw=1)
@@ -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):
@@ -2289,11 +2313,12 @@ class Member3D():
2289
2313
  else:
2290
2314
  combo_names = [
2291
2315
  name for name, combo in self.model.load_combos.items()
2292
- if any(tag in combo.combo_tags for tag in combo_tags)
2316
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
2293
2317
  ]
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):
@@ -2355,11 +2385,12 @@ class Member3D():
2355
2385
  else:
2356
2386
  combo_names = [
2357
2387
  name for name, combo in self.model.load_combos.items()
2358
- if any(tag in combo.combo_tags for tag in combo_tags)
2388
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags)
2359
2389
  ]
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
2418
+ governing_combo = combo_name
2387
2419
 
2388
- # Return global minimum or 0 if nothing found
2389
- return dmin_global if dmin_global is not None else 0
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
  """
@@ -2416,7 +2452,7 @@ class Member3D():
2416
2452
  combo_names = [combo_name]
2417
2453
  else:
2418
2454
  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)]
2455
+ if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_name)]
2420
2456
 
2421
2457
  fig, ax = Member3D.__plt.subplots()
2422
2458
  ax.axhline(0, color='black', lw=1)