PyNiteFEA 2.0.0__tar.gz → 2.0.2__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.0.0 → pynitefea-2.0.2}/PKG-INFO +10 -5
  2. {pynitefea-2.0.0 → pynitefea-2.0.2}/PyNiteFEA.egg-info/PKG-INFO +10 -5
  3. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Rendering.py +65 -9
  4. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Visualization.py +250 -86
  5. {pynitefea-2.0.0 → pynitefea-2.0.2}/README.md +9 -4
  6. {pynitefea-2.0.0 → pynitefea-2.0.2}/setup.py +1 -1
  7. {pynitefea-2.0.0 → pynitefea-2.0.2}/LICENSE +0 -0
  8. {pynitefea-2.0.0 → pynitefea-2.0.2}/PyNiteFEA.egg-info/SOURCES.txt +0 -0
  9. {pynitefea-2.0.0 → pynitefea-2.0.2}/PyNiteFEA.egg-info/dependency_links.txt +0 -0
  10. {pynitefea-2.0.0 → pynitefea-2.0.2}/PyNiteFEA.egg-info/requires.txt +0 -0
  11. {pynitefea-2.0.0 → pynitefea-2.0.2}/PyNiteFEA.egg-info/top_level.txt +0 -0
  12. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Analysis.py +0 -0
  13. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/BeamSegY.py +0 -0
  14. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/BeamSegZ.py +0 -0
  15. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/FEModel3D.py +0 -0
  16. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/FixedEndReactions.py +0 -0
  17. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/LoadCombo.py +0 -0
  18. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/MainStyleSheet.css +0 -0
  19. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/MatFoundation.py +0 -0
  20. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Material.py +0 -0
  21. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Member3D.py +0 -0
  22. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Mesh.py +0 -0
  23. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Node3D.py +0 -0
  24. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/PhysMember.py +0 -0
  25. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Plate3D.py +0 -0
  26. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Quad3D.py +0 -0
  27. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Report_Template.html +0 -0
  28. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Reporting.py +0 -0
  29. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Section.py +0 -0
  30. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/ShearWall.py +0 -0
  31. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Spring3D.py +0 -0
  32. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/Tri3D.py +0 -0
  33. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/VTKWriter.py +0 -0
  34. {pynitefea-2.0.0 → pynitefea-2.0.2}/Pynite/__init__.py +0 -0
  35. {pynitefea-2.0.0 → pynitefea-2.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyNiteFEA
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -117,6 +117,15 @@ Here's a list of projects that use Pynite:
117
117
  * Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
118
118
 
119
119
  # What's New?
120
+ v2.0.2
121
+ * Added docstrings to the VTK `Renderer` class to help the user.
122
+ * Enforced use of properties instead of attributes in the VTK `Renderer` and added docstrings to properties help the user make decisions.
123
+ * Removed the VTK `Renderer`'s properties that began with "set_". These were redundant and caused confusion. The prefix "set_" is no longer used to access or set any of the `Renderer`'s properties.
124
+
125
+ v2.0.1
126
+ * Pynite no longer struggles rendering load cases and combinations when no loads are present. This is especially helpful for rendering modal load combinations, which don't have loads (only masses).
127
+ * The pyvista plotter's "X" button now works just like pressing "q". This was an annoying pyvista-ism that led to error messages when closing the window with the "X" button.
128
+
120
129
  v2.0.0
121
130
  * Added modal analysis. You can now find the natural frequencies and mode shapes of frame-type structures (plates not yet supported). A special thanks to @boustrephon for heading up this feature.
122
131
  * Dropping support for python < 3.10.
@@ -195,7 +204,3 @@ v1.0.0
195
204
  * Added a new `ShearWall` class that assists you in constructing and analyzing shear walls. This tool automatically detects piers and coupling beams, and finds the forces inside them and calculates their ascpect ratios, which can be handy for seismic design. It reports stiffness of multi-story shear walls at each story to help with rigid diaphragm analysis. It allows for modeling walls with openings, steps, and partial depth diaphragm loading.
196
205
  * `vtk` and `pyvista` are now optional dependencies. This change streamlines installation for users who don't rely on `Pynite's` built-in visualization tools. From now on, `Pynite` should be installed using `$ pip install PyNiteFEA[all]` for most users.
197
206
  * `Pynite` no longer uses auxiliary nodes to define member cross-section rotation. You can now directly specify the rotation (in degrees) when you define a member using the `rotation` argument.
198
-
199
- v0.0.98-100
200
- * Bug fix for `FEModel3D.add_section`. It was throwing exceptions and had not been updated to match the examples.
201
- * Improvements to spring rendering in `pyvista`. Up until this point spring elements were being rendered as lines. They now render as zigzag lines in `pyvista`. There is still more work for improvement on spring rendering, but this is a good start.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyNiteFEA
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -117,6 +117,15 @@ Here's a list of projects that use Pynite:
117
117
  * Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
118
118
 
119
119
  # What's New?
120
+ v2.0.2
121
+ * Added docstrings to the VTK `Renderer` class to help the user.
122
+ * Enforced use of properties instead of attributes in the VTK `Renderer` and added docstrings to properties help the user make decisions.
123
+ * Removed the VTK `Renderer`'s properties that began with "set_". These were redundant and caused confusion. The prefix "set_" is no longer used to access or set any of the `Renderer`'s properties.
124
+
125
+ v2.0.1
126
+ * Pynite no longer struggles rendering load cases and combinations when no loads are present. This is especially helpful for rendering modal load combinations, which don't have loads (only masses).
127
+ * The pyvista plotter's "X" button now works just like pressing "q". This was an annoying pyvista-ism that led to error messages when closing the window with the "X" button.
128
+
120
129
  v2.0.0
121
130
  * Added modal analysis. You can now find the natural frequencies and mode shapes of frame-type structures (plates not yet supported). A special thanks to @boustrephon for heading up this feature.
122
131
  * Dropping support for python < 3.10.
@@ -195,7 +204,3 @@ v1.0.0
195
204
  * Added a new `ShearWall` class that assists you in constructing and analyzing shear walls. This tool automatically detects piers and coupling beams, and finds the forces inside them and calculates their ascpect ratios, which can be handy for seismic design. It reports stiffness of multi-story shear walls at each story to help with rigid diaphragm analysis. It allows for modeling walls with openings, steps, and partial depth diaphragm loading.
196
205
  * `vtk` and `pyvista` are now optional dependencies. This change streamlines installation for users who don't rely on `Pynite's` built-in visualization tools. From now on, `Pynite` should be installed using `$ pip install PyNiteFEA[all]` for most users.
197
206
  * `Pynite` no longer uses auxiliary nodes to define member cross-section rotation. You can now directly specify the rotation (in degrees) when you define a member using the `rotation` argument.
198
-
199
- v0.0.98-100
200
- * Bug fix for `FEModel3D.add_section`. It was throwing exceptions and had not been updated to match the examples.
201
- * Improvements to spring rendering in `pyvista`. Up until this point spring elements were being rendered as lines. They now render as zigzag lines in `pyvista`. There is still more work for improvement on spring rendering, but this is a good start.
@@ -8,6 +8,10 @@ import numpy as np
8
8
  import pyvista as pv
9
9
  import math
10
10
 
11
+ # Suppress PyVista warnings for clean console output
12
+ warnings.filterwarnings('ignore', category=UserWarning, module='pyvista')
13
+ warnings.filterwarnings('ignore', message='.*Points is not a float type.*')
14
+
11
15
  # For type checking only - these imports are only used during type checking
12
16
  if TYPE_CHECKING:
13
17
  from typing import List, Union, Tuple, Optional
@@ -54,13 +58,27 @@ class Renderer:
54
58
  # (e.g., grid, axes) before render. Each func in this list must accept a `pyvista.Plotter` argument.
55
59
  self.post_update_callbacks: List[Callable[[pv.Plotter], None]] = []
56
60
 
57
- self.plotter: pv.Plotter = pv.Plotter()
61
+ # Create plotter with off_screen mode if pyvista is set globally to OFF_SCREEN
62
+ # This is important for headless CI/testing environments
63
+ self.plotter: pv.Plotter = pv.Plotter(off_screen=pv.OFF_SCREEN)
58
64
  self.plotter.set_background('white') # Setting background color
59
65
  # self.plotter.add_logo_widget('./Resources/Full Logo No Buffer - Transparent.png')
60
- # self.plotter.view_isometric()
61
- self.plotter.view_xy()
62
- self.plotter.show_axes()
63
- self.plotter.set_viewup((0, 1, 0)) # Set the Y axis to vertical for 3D plots
66
+
67
+ # Only set view and axes in interactive mode (renderer must exist for these calls)
68
+ # In off-screen/headless mode, these will be set when update() is called
69
+ if not pv.OFF_SCREEN:
70
+ # self.plotter.view_isometric()
71
+ self.plotter.view_xy()
72
+ self.plotter.show_axes()
73
+ self.plotter.set_viewup((0, 1, 0)) # Set the Y axis to vertical for 3D plots
74
+
75
+ # Make X button behave like 'q' key - properly exit without destroying plotter
76
+ # Why: By default, PyVista's X button forcefully destroys the render window,
77
+ # causing warnings and preventing clean shutdown. This observer intercepts
78
+ # the window close event.
79
+ # How: When ExitEvent fires (X button clicked), we call TerminateApp() on the
80
+ # interactor, which cleanly exits the event loop just like pressing 'q'.
81
+ self.plotter.iren.add_observer('ExitEvent', lambda obj, event: obj.TerminateApp())
64
82
 
65
83
  # Initialize load labels
66
84
  self._load_label_points: List[List[float]] = []
@@ -178,7 +196,7 @@ class Renderer:
178
196
  def scalar_bar_text_size(self, text_size: int) -> None:
179
197
  self._scalar_bar_text_size = text_size
180
198
 
181
- def render_model(self, reset_camera: bool = True) -> None:
199
+ def render_model(self, reset_camera: bool = True, off_screen: bool = False) -> None:
182
200
  """
183
201
  Renders the model in a window
184
202
 
@@ -186,19 +204,37 @@ class Renderer:
186
204
  ----------
187
205
  reset_camera : bool
188
206
  Resets the camera if set to `True`. Default is `True`.
207
+ off_screen : bool
208
+ Renders off-screen without displaying a window. Useful for testing or
209
+ generating images in headless environments. Default is `False`.
189
210
  """
190
211
 
212
+ # Set off-screen mode if requested (for testing/headless environments)
213
+ # This must be done BEFORE calling update() so the plotter is in off-screen mode
214
+ # when adding meshes, preventing PyVista camera initialization issues
215
+ if off_screen:
216
+ self.plotter.off_screen = True
217
+
191
218
  # Update the plotter with the latest geometry
192
219
  self.update(reset_camera)
193
220
 
194
221
  # Render the model (code execution will pause here until the user closes the window)
195
- self.plotter.show(title='Pynite - Simple Finite Element Analysis for Python', auto_close=False)
222
+ try:
223
+ self.plotter.show(title='Pynite - Simple Finite Element Analysis for Python', auto_close=False)
224
+ finally:
225
+ # Explicitly deep clean the plotter to prevent garbage collection issues
226
+ # Why: PyVista/VTK objects can have circular references that cause AttributeErrors
227
+ # during Python's garbage collection phase when the program exits.
228
+ # How: deep_clean() forces immediate cleanup of all VTK objects, meshes, and actors
229
+ # before Python's garbage collector runs, preventing the "'NoneType' object has
230
+ # no attribute 'check_attribute'" exception that would otherwise appear.
231
+ self.plotter.deep_clean()
196
232
 
197
233
  def screenshot(self, filepath: str = './Pynite_Image.png', interact: bool = True, reset_camera: bool = False) -> None:
198
234
  """
199
235
  Saves a screenshot of the rendered model.
200
236
 
201
- In non-Jupyter notebook environments, if `interact` is set to `True`, the plotter will show a window that allows you to set the scene prior to the screenshot. Press `q` when you are ready to capture the screenshot and close the window. Pressing the `X` button in the corner of the window will ignore your scene changes and will shut down the entire plotter. This is a default setting in pyvista that is a little annoying. Don't press the `X` button.
237
+ In non-Jupyter notebook environments, if `interact` is set to `True`, the plotter will show a window that allows you to set the scene prior to the screenshot. Press `q` or click the `X` button when you are ready to capture the screenshot and close the window.
202
238
 
203
239
  For Jupyter notebooks, the scene must be set using `render_model` prior to using `screenshot`.
204
240
 
@@ -246,6 +282,15 @@ class Renderer:
246
282
 
247
283
  # Clear out the old plot (if any)
248
284
  self.plotter.clear()
285
+
286
+ # Set up view and axes (works for both interactive and off-screen modes)
287
+ try:
288
+ self.plotter.view_xy()
289
+ self.plotter.show_axes()
290
+ self.plotter.set_viewup((0, 1, 0))
291
+ except:
292
+ # Silently fail if not supported in this context
293
+ pass
249
294
 
250
295
  # Clear out internally stored labels (if any)
251
296
  self._load_label_points = []
@@ -1063,9 +1108,20 @@ class Renderer:
1063
1108
  if abs(load[0]) > max_area_load:
1064
1109
  max_area_load = abs(load[0])
1065
1110
 
1111
+ # Prevent division by zero errors by ensuring max values are never zero
1112
+ # If a load type has no loads, set it to 1 to avoid crashes during normalization
1113
+ if max_pt_load == 0:
1114
+ max_pt_load = 1
1115
+ if max_moment == 0:
1116
+ max_moment = 1
1117
+ if max_dist_load == 0:
1118
+ max_dist_load = 1
1119
+ if max_area_load == 0:
1120
+ max_area_load = 1
1121
+
1066
1122
  # Return the maximum loads for the load combo or load case
1067
1123
  return max_pt_load, max_moment, max_dist_load, max_area_load
1068
-
1124
+
1069
1125
  def plot_loads(self):
1070
1126
 
1071
1127
  # Get the maximum load magnitudes that will be used to normalize the display scale
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations # Allows more recent type hints features
2
- from json import load
3
2
  import warnings
4
3
 
5
4
  from IPython.display import Image
@@ -8,28 +7,83 @@ from numpy.linalg import norm
8
7
  import vtk
9
8
 
10
9
  class Renderer():
11
- """Used to render finite element models.
10
+ """Renderer for visualizing finite element models using VTK.
11
+
12
+ This class provides a flexible interface for rendering 3D finite element models,
13
+ including nodes, members, springs, plates, and quads. It supports rendering of
14
+ undeformed and deformed shapes, load visualizations, and stress/force contours.
15
+
16
+ Parameters
17
+ ----------
18
+ model : FEModel3D
19
+ The finite element model to be rendered.
20
+
21
+ Attributes
22
+ ----------
23
+ annotation_size : float
24
+ Controls the size of text labels and visual elements (default: 5).
25
+ deformed_shape : bool
26
+ Whether to render the deformed shape of the model (default: False).
27
+ deformed_scale : float
28
+ Scale factor for deformation visualization (default: 30).
29
+ render_nodes : bool
30
+ Whether to render nodes in the visualization (default: True).
31
+ render_loads : bool
32
+ Whether to render applied loads (default: True).
33
+ color_map : str or None
34
+ Type of stress/force contour to display on plates/quads. Options:
35
+ 'Qx', 'Qy', 'Mx', 'My', 'Mxy', 'Sx', 'Sy', 'Txy' (default: None).
36
+ combo_name : str or None
37
+ Name of load combination to visualize. Mutually exclusive with case.
38
+ case : str or None
39
+ Name of load case to visualize. Mutually exclusive with combo_name.
40
+ labels : bool
41
+ Whether to display text labels for elements (default: True).
42
+ scalar_bar : bool
43
+ Whether to display a scalar bar for contour plots (default: False).
44
+ scalar_bar_text_size : int
45
+ Font size for scalar bar text (default: 24).
46
+ theme : str
47
+ Visual theme: 'default' or 'print' (default: 'default').
48
+ window_size : tuple
49
+ Window dimensions as (width, height).
50
+
51
+ Examples
52
+ --------
53
+ >>> from PyNite import FEModel3D
54
+ >>> model = FEModel3D()
55
+ >>> # ... build model ...
56
+ >>> renderer = Renderer(model)
57
+ >>> renderer.annotation_size = 6
58
+ >>> renderer.combo_name = 'Load Combo 1'
59
+ >>> renderer.render_model()
12
60
  """
13
61
 
14
62
  scalar = None
15
63
 
16
64
  def __init__(self, model):
17
-
65
+ """Initialize the renderer with a finite element model.
66
+
67
+ Parameters
68
+ ----------
69
+ model : FEModel3D
70
+ The finite element model to render.
71
+ """
18
72
  self.model = model
19
73
 
20
74
  # Default settings for rendering
21
- self.annotation_size = 5
22
- self.deformed_shape = False
23
- self.deformed_scale = 30
24
- self.render_nodes = True
25
- self.render_loads = True
26
- self.color_map = None
27
- self.combo_name = 'Combo 1'
28
- self.case = None
29
- self.labels = True
30
- self.scalar_bar = False
31
- self.scalar_bar_text_size = 24
32
- self.theme = 'default'
75
+ self._annotation_size = 5
76
+ self._deformed_shape = False
77
+ self._deformed_scale = 30
78
+ self._render_nodes = True
79
+ self._render_loads = True
80
+ self._color_map = None
81
+ self._combo_name = 'Combo 1'
82
+ self._case = None
83
+ self._labels = True
84
+ self._scalar_bar = False
85
+ self._scalar_bar_text_size = 24
86
+ self._theme = 'default'
33
87
 
34
88
  # Initialize VTK objects
35
89
  self.renderer = vtk.vtkRenderer()
@@ -38,62 +92,134 @@ class Renderer():
38
92
  self.window.AddRenderer(self.renderer)
39
93
 
40
94
  @property
41
- def window_width(self):
42
- return self.window.GetSize()[0]
43
-
44
- @window_width.setter
45
- def window_width(self, width):
46
- height = self.window.GetSize()[1]
95
+ def window_size(self):
96
+ """Window size as (width, height) tuple."""
97
+ return self.window.GetSize()
98
+
99
+ @window_size.setter
100
+ def window_size(self, size):
101
+ """Set window size from tuple or list (width, height)."""
102
+ width, height = size
47
103
  self.window.SetSize(width, height)
48
104
 
49
105
  @property
50
- def window_height(self):
51
- return self.window.GetSize()[1]
52
-
53
- @window_height.setter
54
- def window_height(self, height):
55
- width = self.window.GetSize()[0]
56
- self.window.SetSize(width, height)
106
+ def combo_name(self):
107
+ """Load combination name. When set, case is automatically set to None."""
108
+ return self._combo_name
109
+
110
+ @combo_name.setter
111
+ def combo_name(self, value):
112
+ self._combo_name = value
113
+ if value is not None:
114
+ self._case = None
115
+
116
+ @property
117
+ def case(self):
118
+ """Load case name. When set, combo_name is automatically set to None."""
119
+ return self._case
120
+
121
+ @case.setter
122
+ def case(self, value):
123
+ self._case = value
124
+ if value is not None:
125
+ self._combo_name = None
57
126
 
58
- def set_annotation_size(self, size=5):
59
- self.annotation_size = size
127
+ @property
128
+ def annotation_size(self):
129
+ """Size of text annotations and visual elements in model units."""
130
+ return self._annotation_size
131
+
132
+ @annotation_size.setter
133
+ def annotation_size(self, value):
134
+ self._annotation_size = value
60
135
 
61
- def set_deformed_shape(self, deformed_shape=False):
62
- self.deformed_shape = deformed_shape
136
+ @property
137
+ def deformed_shape(self):
138
+ """Whether to render the deformed shape of the model."""
139
+ return self._deformed_shape
140
+
141
+ @deformed_shape.setter
142
+ def deformed_shape(self, value):
143
+ self._deformed_shape = value
63
144
 
64
- def set_deformed_scale(self, scale=30):
65
- self.deformed_scale = scale
145
+ @property
146
+ def deformed_scale(self):
147
+ """Scale factor for deformation visualization."""
148
+ return self._deformed_scale
149
+
150
+ @deformed_scale.setter
151
+ def deformed_scale(self, value):
152
+ self._deformed_scale = value
66
153
 
67
- def set_render_nodes(self, render_nodes=True):
68
- self.render_nodes = render_nodes
154
+ @property
155
+ def render_nodes(self):
156
+ """Whether to render nodes in the visualization."""
157
+ return self._render_nodes
158
+
159
+ @render_nodes.setter
160
+ def render_nodes(self, value):
161
+ self._render_nodes = value
69
162
 
70
- def set_render_loads(self, render_loads=True):
71
- self.render_loads = render_loads
163
+ @property
164
+ def render_loads(self):
165
+ """Whether to render applied loads."""
166
+ return self._render_loads
72
167
 
73
- def set_color_map(self, color_map=None):
74
- """Sets the color map for plate contours.
168
+ @render_loads.setter
169
+ def render_loads(self, value):
170
+ self._render_loads = value
75
171
 
76
- :param color_map: The color map to use: Valid options are 'Qx', Qy', 'Mx', 'My', 'Mxy', 'Sx', 'Sy' 'Txy'. Qx and Qy are out-of-plane shear forces. Mx, My, and Mxy are local out-of-plane bending moments. 'Sx' and 'Sy' are membrane forces, and 'Txy' is an in-plane shear force. Defaults to None.
77
- :type color_map: str, optional
172
+ @property
173
+ def color_map(self):
174
+ """Type of stress/force contour to display on plates/quads.
175
+
176
+ Valid options: 'Qx', 'Qy', 'Mx', 'My', 'Mxy', 'Sx', 'Sy', 'Txy'
177
+ - Qx, Qy: Out-of-plane shear forces
178
+ - Mx, My, Mxy: Local out-of-plane bending moments
179
+ - Sx, Sy: Membrane forces
180
+ - Txy: In-plane shear force
78
181
  """
79
- self.color_map = color_map
80
-
81
- def set_combo_name(self, combo_name='Combo 1'):
82
- self.case = None
83
- self.combo_name = combo_name
182
+ return self._color_map
183
+
184
+ @color_map.setter
185
+ def color_map(self, value):
186
+ self._color_map = value
84
187
 
85
- def set_case(self, case=None):
86
- self.combo_name = None
87
- self.case = case
188
+ @property
189
+ def labels(self):
190
+ """Whether to display text labels for elements."""
191
+ return self._labels
192
+
193
+ @labels.setter
194
+ def labels(self, value):
195
+ self._labels = value
88
196
 
89
- def set_show_labels(self, show_labels=True):
90
- self.labels = show_labels
197
+ @property
198
+ def scalar_bar(self):
199
+ """Whether to display a scalar bar legend for contour plots."""
200
+ return self._scalar_bar
201
+
202
+ @scalar_bar.setter
203
+ def scalar_bar(self, value):
204
+ self._scalar_bar = value
91
205
 
92
- def set_scalar_bar(self, scalar_bar=False):
93
- self.scalar_bar = scalar_bar
206
+ @property
207
+ def scalar_bar_text_size(self):
208
+ """Font size for scalar bar text."""
209
+ return self._scalar_bar_text_size
210
+
211
+ @scalar_bar_text_size.setter
212
+ def scalar_bar_text_size(self, value):
213
+ self._scalar_bar_text_size = value
94
214
 
95
- def set_scalar_bar_text_size(self, text_size=24):
96
- self.scalar_bar_text_size = text_size
215
+ @property
216
+ def theme(self):
217
+ """Visual theme: 'default' (dark background) or 'print' (white background)."""
218
+ return self._theme
219
+
220
+ @theme.setter
221
+ def theme(self, value):
222
+ self._theme = value
97
223
 
98
224
  def render_model(self, interact=True, reset_camera=True):
99
225
  """
@@ -198,11 +324,26 @@ class Renderer():
198
324
  return
199
325
 
200
326
  def update(self, reset_camera=True):
201
- """Builds (or rebuilds) the VTK renderer
202
-
203
- :param reset_camera: Resets the render window's camera position if set to True. Defaults to True.
204
- :type reset_camera: bool, optional
205
- :raises Exception: Visualization failed due to an unexpected error.
327
+ """Build or rebuild the VTK renderer with current settings.
328
+
329
+ This method updates the visualization based on the current renderer settings,
330
+ including deformed shape, loads, contours, and other visual elements. It is
331
+ automatically called by render_model() and screenshot().
332
+
333
+ Parameters
334
+ ----------
335
+ reset_camera : bool, optional
336
+ If True, resets the camera to view the entire model (default: True).
337
+
338
+ Raises
339
+ ------
340
+ Exception
341
+ If the model configuration is invalid for the requested visualization.
342
+
343
+ Notes
344
+ -----
345
+ This method validates that deformed_shape is not used with a load case,
346
+ and that render_loads requires either a load combination or load case.
206
347
  """
207
348
 
208
349
  # Input validation
@@ -604,8 +745,9 @@ class VisNode():
604
745
  # Set the mapper for the node's actor
605
746
  self.actor.SetMapper(mapper)
606
747
 
748
+
607
749
  class VisSpring():
608
-
750
+
609
751
  def __init__(self, spring, nodes, annotation_size=5, color=None):
610
752
 
611
753
  # Generate a line source for the spring
@@ -1320,6 +1462,9 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1320
1462
  polygon_points = vtk.vtkPoints()
1321
1463
  polygon_polydata = vtk.vtkPolyData()
1322
1464
 
1465
+ # Track whether any loads have been added to avoid VTK errors with empty append filters
1466
+ has_loads = False
1467
+
1323
1468
  # Get the maximum load magnitudes that will be used to normalize the display scale
1324
1469
  max_pt_load, max_moment, max_dist_load, max_area_load = _MaxLoads(model, combo_name, case)
1325
1470
 
@@ -1365,6 +1510,7 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1365
1510
  polydata.AddInputData(ptLoad.polydata.GetOutput())
1366
1511
  renderer.AddActor(ptLoad.lblActor)
1367
1512
  ptLoad.lblActor.SetCamera(renderer.GetActiveCamera())
1513
+ has_loads = True
1368
1514
 
1369
1515
  # Step through each member
1370
1516
  for member in model.members.values():
@@ -1418,6 +1564,7 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1418
1564
  polydata.AddInputData(ptLoad.polydata.GetOutput())
1419
1565
  renderer.AddActor(ptLoad.lblActor)
1420
1566
  ptLoad.lblActor.SetCamera(renderer.GetActiveCamera())
1567
+ has_loads = True
1421
1568
 
1422
1569
  # Step through each member distributed load
1423
1570
  for load in member.DistLoads:
@@ -1454,6 +1601,7 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1454
1601
  renderer.AddActor(distLoad.lblActors[1])
1455
1602
  distLoad.lblActors[0].SetCamera(renderer.GetActiveCamera())
1456
1603
  distLoad.lblActors[1].SetCamera(renderer.GetActiveCamera())
1604
+ has_loads = True
1457
1605
 
1458
1606
  # Step through each plate
1459
1607
  i = 0
@@ -1489,6 +1637,7 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1489
1637
 
1490
1638
  # Add the area load's arrows to the overall load polydata
1491
1639
  polydata.AddInputData(area_load.polydata.GetOutput())
1640
+ has_loads = True
1492
1641
 
1493
1642
  # Add the 4 points at the corners of this area load to the list of points
1494
1643
  polygon_points.InsertNextPoint(area_load.p0[0], area_load.p0[1], area_load.p0[2])
@@ -1523,35 +1672,39 @@ def _RenderLoads(model, renderer, annotation_size, combo_name, case, theme='defa
1523
1672
  polygon_polydata.SetPoints(polygon_points)
1524
1673
  polygon_polydata.SetPolys(polygons)
1525
1674
 
1526
- # Set up an actor and mapper for the loads
1527
- load_mapper = vtk.vtkPolyDataMapper()
1528
- load_mapper.SetInputConnection(polydata.GetOutputPort())
1529
- load_actor = vtk.vtkActor()
1530
- load_actor.SetMapper(load_mapper)
1675
+ # Only create and add actors if there are loads to render (avoids VTK errors with empty append filters)
1676
+ if has_loads:
1677
+ # Set up an actor and mapper for the loads
1678
+ load_mapper = vtk.vtkPolyDataMapper()
1679
+ load_mapper.SetInputConnection(polydata.GetOutputPort())
1680
+ load_actor = vtk.vtkActor()
1681
+ load_actor.SetMapper(load_mapper)
1531
1682
 
1532
- # Colorize the loads
1533
- if theme == 'default':
1534
- load_actor.GetProperty().SetColor(0, 1, 0) # Green
1535
- elif theme == 'print':
1536
- load_actor.GetProperty().SetColor(0, 0.75, 0) # Dark Green
1683
+ # Colorize the loads
1684
+ if theme == 'default':
1685
+ load_actor.GetProperty().SetColor(0, 1, 0) # Green
1686
+ elif theme == 'print':
1687
+ load_actor.GetProperty().SetColor(0, 0.75, 0) # Dark Green
1537
1688
 
1538
- # Add the load actor to the renderer
1539
- renderer.AddActor(load_actor)
1689
+ # Add the load actor to the renderer
1690
+ renderer.AddActor(load_actor)
1540
1691
 
1541
- # Set up an actor and a mapper for the area load polygons
1542
- polygon_mapper = vtk.vtkPolyDataMapper()
1543
- polygon_mapper.SetInputData(polygon_polydata)
1544
- polygon_actor = vtk.vtkActor()
1692
+ # Only create polygon actors if there are area loads (i > 0 means we added polygons)
1693
+ if i > 0:
1694
+ # Set up an actor and a mapper for the area load polygons
1695
+ polygon_mapper = vtk.vtkPolyDataMapper()
1696
+ polygon_mapper.SetInputData(polygon_polydata)
1697
+ polygon_actor = vtk.vtkActor()
1545
1698
 
1546
- # polygon_actor.GetProperty().SetOpacity(0.5) # 50% opacity
1547
- polygon_actor.SetMapper(polygon_mapper)
1548
- renderer.AddActor(polygon_actor)
1699
+ # polygon_actor.GetProperty().SetOpacity(0.5) # 50% opacity
1700
+ polygon_actor.SetMapper(polygon_mapper)
1701
+ renderer.AddActor(polygon_actor)
1549
1702
 
1550
- # Set the color of the area load polygons
1551
- if theme == 'default':
1552
- polygon_actor.GetProperty().SetColor(0, 1, 0) # Green
1553
- elif theme == 'print':
1554
- polygon_actor.GetProperty().SetColor(0, 0.75, 0) # Dark Green
1703
+ # Set the color of the area load polygons
1704
+ if theme == 'default':
1705
+ polygon_actor.GetProperty().SetColor(0, 1, 0) # Green
1706
+ elif theme == 'print':
1707
+ polygon_actor.GetProperty().SetColor(0, 0.75, 0) # Dark Green
1555
1708
 
1556
1709
  def _RenderContours(model, renderer, deformed_shape, deformed_scale, color_map, scalar_bar, scalar_bar_text_size, combo_name, theme='default'):
1557
1710
 
@@ -1847,5 +2000,16 @@ def _MaxLoads(model, combo_name=None, case=None):
1847
2000
  if abs(load[0]) > max_area_load:
1848
2001
  max_area_load = abs(load[0])
1849
2002
 
2003
+ # Prevent division by zero errors by ensuring max values are never zero
2004
+ # If a load type has no loads, set it to 1 to avoid crashes during normalization
2005
+ if max_pt_load == 0:
2006
+ max_pt_load = 1
2007
+ if max_moment == 0:
2008
+ max_moment = 1
2009
+ if max_dist_load == 0:
2010
+ max_dist_load = 1
2011
+ if max_area_load == 0:
2012
+ max_area_load = 1
2013
+
1850
2014
  # Return the maximum loads in the load combination or load case
1851
2015
  return max_pt_load, max_moment, max_dist_load, max_area_load
@@ -67,6 +67,15 @@ Here's a list of projects that use Pynite:
67
67
  * Phaenotyp (https://github.com/bewegende-Architektur/Phaenotyp) (https://youtu.be/shloSw9HjVI)
68
68
 
69
69
  # What's New?
70
+ v2.0.2
71
+ * Added docstrings to the VTK `Renderer` class to help the user.
72
+ * Enforced use of properties instead of attributes in the VTK `Renderer` and added docstrings to properties help the user make decisions.
73
+ * Removed the VTK `Renderer`'s properties that began with "set_". These were redundant and caused confusion. The prefix "set_" is no longer used to access or set any of the `Renderer`'s properties.
74
+
75
+ v2.0.1
76
+ * Pynite no longer struggles rendering load cases and combinations when no loads are present. This is especially helpful for rendering modal load combinations, which don't have loads (only masses).
77
+ * The pyvista plotter's "X" button now works just like pressing "q". This was an annoying pyvista-ism that led to error messages when closing the window with the "X" button.
78
+
70
79
  v2.0.0
71
80
  * Added modal analysis. You can now find the natural frequencies and mode shapes of frame-type structures (plates not yet supported). A special thanks to @boustrephon for heading up this feature.
72
81
  * Dropping support for python < 3.10.
@@ -145,7 +154,3 @@ v1.0.0
145
154
  * Added a new `ShearWall` class that assists you in constructing and analyzing shear walls. This tool automatically detects piers and coupling beams, and finds the forces inside them and calculates their ascpect ratios, which can be handy for seismic design. It reports stiffness of multi-story shear walls at each story to help with rigid diaphragm analysis. It allows for modeling walls with openings, steps, and partial depth diaphragm loading.
146
155
  * `vtk` and `pyvista` are now optional dependencies. This change streamlines installation for users who don't rely on `Pynite's` built-in visualization tools. From now on, `Pynite` should be installed using `$ pip install PyNiteFEA[all]` for most users.
147
156
  * `Pynite` no longer uses auxiliary nodes to define member cross-section rotation. You can now directly specify the rotation (in degrees) when you define a member using the `rotation` argument.
148
-
149
- v0.0.98-100
150
- * Bug fix for `FEModel3D.add_section`. It was throwing exceptions and had not been updated to match the examples.
151
- * Improvements to spring rendering in `pyvista`. Up until this point spring elements were being rendered as lines. They now render as zigzag lines in `pyvista`. There is still more work for improvement on spring rendering, but this is a good start.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="PyNiteFEA",
8
- version="2.0.0",
8
+ version="2.0.2",
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