Coverage for src/beamme/core/element_beam.py: 92%
78 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2018-2026 BeamMe Authors
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in
13# all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21# THE SOFTWARE.
22"""This file defines the base beam element."""
24from typing import Any as _Any
26import numpy as _np
27import pyvista as _pv
28import vtk as _vtk
30from beamme.core.conf import bme as _bme
31from beamme.core.element import Element as _Element
32from beamme.core.vtk_writer import add_point_data_node_sets as _add_point_data_node_sets
35class Beam(_Element):
36 """A base class for a beam element."""
38 # Cell type for representing this element in vtk.
39 vtk_cell_type = _pv.CellType.POLY_LINE
41 # An array that defines the parameter positions of the element nodes,
42 # in ascending order.
43 nodes_create: _Any = []
45 def __init__(self, material=None, nodes=None):
46 super().__init__(nodes=nodes, material=material)
48 @classmethod
49 def get_coupling_dict(cls, coupling_dof_type):
50 """Return the dict to couple this beam to another beam."""
52 match coupling_dof_type:
53 case _bme.coupling_dof.joint:
54 if cls.coupling_joint_dict is None:
55 raise ValueError(f"Joint coupling is not implemented for {cls}")
56 return cls.coupling_joint_dict
57 case _bme.coupling_dof.fix:
58 if cls.coupling_fix_dict is None:
59 raise ValueError("Fix coupling is not implemented for {cls}")
60 return cls.coupling_fix_dict
61 case _:
62 raise ValueError(
63 f'Coupling_dof_type "{coupling_dof_type}" is not implemented!'
64 )
66 def flip(self):
67 """Reverse the nodes of this element.
69 This is usually used when reflected.
70 """
71 self.nodes = [self.nodes[-1 - i] for i in range(len(self.nodes))]
73 def get_vtk(
74 self,
75 vtk_writer_beam,
76 vtk_writer_solid,
77 *,
78 beam_centerline_visualization_segments=1,
79 **kwargs,
80 ):
81 """Add the representation of this element to the VTK writer as a poly
82 line.
84 Args
85 ----
86 vtk_writer_beam:
87 VTK writer for the beams.
88 vtk_writer_solid:
89 VTK writer for solid elements, not used in this method.
90 beam_centerline_visualization_segments: int
91 Number of segments to be used for visualization of beam centerline between successive
92 nodes. Default is 1, which means a straight line is drawn between the beam nodes. For
93 Values greater than 1, a Hermite interpolation of the centerline is assumed for
94 visualization purposes.
95 """
97 n_nodes = len(self.nodes)
98 n_segments = n_nodes - 1
99 n_additional_points_per_segment = beam_centerline_visualization_segments - 1
100 # Number of points (in addition to the nodes) to be used for output
101 n_additional_points = n_segments * n_additional_points_per_segment
102 n_points = n_nodes + n_additional_points
104 # Dictionary with cell data.
105 cell_data = self.vtk_cell_data.copy()
106 if self.material.radius is not None:
107 cell_data["cross_section_radius"] = self.material.radius
109 # Dictionary with point data.
110 point_data = {}
111 point_data["node_value"] = _np.zeros(n_points)
112 point_data["base_vector_1"] = _np.zeros((n_points, 3))
113 point_data["base_vector_2"] = _np.zeros((n_points, 3))
114 point_data["base_vector_3"] = _np.zeros((n_points, 3))
116 coordinates = _np.zeros((n_points, 3))
117 nodal_rotation_matrices = [
118 node.rotation.get_rotation_matrix() for node in self.nodes
119 ]
121 for i_node, (node, rotation_matrix) in enumerate(
122 zip(self.nodes, nodal_rotation_matrices)
123 ):
124 coordinates[i_node, :] = node.coordinates
125 if node.is_middle_node:
126 point_data["node_value"][i_node] = 0.5
127 else:
128 point_data["node_value"][i_node] = 1.0
130 point_data["base_vector_1"][i_node] = rotation_matrix[:, 0]
131 point_data["base_vector_2"][i_node] = rotation_matrix[:, 1]
132 point_data["base_vector_3"][i_node] = rotation_matrix[:, 2]
134 # Check if we have everything we need to write output or if we need to calculate additional
135 # points for a smooth beam visualization.
136 if beam_centerline_visualization_segments == 1:
137 point_connectivity = _np.arange(n_nodes)
138 else:
139 # We need the centerline shape function matrices, so calculate them once and use for
140 # all segments that we need. Drop the first and last value, since they represent the
141 # nodes which we have already added above.
142 xi = _np.linspace(-1, 1, beam_centerline_visualization_segments + 1)[1:-1]
143 hermite_shape_functions_pos = _np.array(
144 [
145 0.25 * (2.0 + xi) * (1.0 - xi) ** 2,
146 0.25 * (2.0 - xi) * (1.0 + xi) ** 2,
147 ]
148 ).transpose()
149 hermite_shape_functions_tan = _np.array(
150 [
151 0.125 * (1.0 + xi) * (1.0 - xi) ** 2,
152 -0.125 * (1.0 - xi) * (1.0 + xi) ** 2,
153 ]
154 ).transpose()
156 point_connectivity = _np.zeros(n_points, dtype=int)
158 for i_segment in range(n_segments):
159 positions = _np.array(
160 [
161 self.nodes[i_node].coordinates
162 for i_node in [i_segment, i_segment + 1]
163 ]
164 )
165 tangents = _np.array(
166 [
167 nodal_rotation_matrices[i_node][:, 0]
168 for i_node in [i_segment, i_segment + 1]
169 ]
170 )
171 length_factor = _np.linalg.norm(positions[1] - positions[0])
172 interpolated_coordinates = _np.dot(
173 hermite_shape_functions_pos, positions
174 ) + length_factor * _np.dot(hermite_shape_functions_tan, tangents)
176 index_first_point = (
177 n_nodes + i_segment * n_additional_points_per_segment
178 )
179 index_last_point = (
180 n_nodes + (i_segment + 1) * n_additional_points_per_segment
181 )
183 coordinates[index_first_point:index_last_point] = (
184 interpolated_coordinates
185 )
186 point_connectivity[
187 i_segment * beam_centerline_visualization_segments
188 ] = i_segment
189 point_connectivity[
190 (i_segment + 1) * beam_centerline_visualization_segments
191 ] = i_segment + 1
192 point_connectivity[
193 i_segment * beam_centerline_visualization_segments + 1 : (
194 i_segment + 1
195 )
196 * beam_centerline_visualization_segments
197 ] = _np.arange(index_first_point, index_last_point)
199 # Get the point data sets and add everything to the output file.
200 _add_point_data_node_sets(
201 point_data, self.nodes, extra_points=n_additional_points
202 )
203 indices = vtk_writer_beam.add_points(coordinates, point_data=point_data)
204 vtk_writer_beam.add_cell(
205 _vtk.vtkPolyLine, indices[point_connectivity], cell_data=cell_data
206 )
209def generate_beam_class(n_nodes: int):
210 """Return a class representing a general beam with n_nodes in BeamMe.
212 Args:
213 n_nodes: Number of equally spaced nodes along the beam centerline.
215 Returns:
216 A beam object that has n_nodes along the centerline.
217 """
219 # Define the class variable responsible for creating the nodes.
220 nodes_create = _np.linspace(-1, 1, num=n_nodes)
222 # Create the beam class which inherits from the base beam class.
223 return type(f"Beam{n_nodes}", (Beam,), {"nodes_create": nodes_create})
226Beam2 = generate_beam_class(2)
227Beam3 = generate_beam_class(3)
228Beam4 = generate_beam_class(4)
229Beam5 = generate_beam_class(5)