Coverage for src/beamme/utils/nodes.py: 93%
83 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-11 12:17 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-11 12:17 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2018-2025 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"""Helper functions to find, filter and interact with nodes."""
24from typing import Union as _Union
26import numpy as _np
27from numpy.typing import NDArray as _NDArray
29from beamme.core.conf import bme as _bme
30from beamme.core.geometry_set import GeometryName as _GeometryName
31from beamme.core.geometry_set import GeometrySet as _GeometrySet
32from beamme.core.geometry_set import GeometrySetBase as _GeometrySetBase
33from beamme.core.node import Node as _Node
34from beamme.core.node import NodeCosserat as _NodeCosserat
35from beamme.geometric_search.find_close_points import (
36 find_close_points as _find_close_points,
37)
38from beamme.geometric_search.find_close_points import (
39 point_partners_to_partner_indices as _point_partners_to_partner_indices,
40)
43def find_close_nodes(nodes, **kwargs):
44 """Find nodes in a point cloud that are within a certain tolerance of each
45 other.
47 Args
48 ----
49 nodes: list(Node)
50 Nodes who are part of the point cloud.
51 **kwargs:
52 Arguments passed on to geometric_search.find_close_points
54 Return
55 ----
56 partner_nodes: list(list(Node))
57 A list of lists of nodes that are close to each other, i.e.,
58 each element in the returned list contains nodes that are close
59 to each other.
60 """
62 coords = _np.zeros([len(nodes), 3])
63 for i, node in enumerate(nodes):
64 coords[i, :] = node.coordinates
65 partner_indices = _point_partners_to_partner_indices(
66 *_find_close_points(coords, **kwargs)
67 )
68 return [[nodes[i] for i in partners] for partners in partner_indices]
71def check_node_by_coordinate(node, axis, value, eps=_bme.eps_pos):
72 """Check if the node is at a certain coordinate value.
74 Args
75 ----
76 node: Node
77 The node to be checked for its position.
78 axis: int
79 Coordinate axis to check.
80 0 -> x, 1 -> y, 2 -> z
81 value: float
82 Value for the coordinate that the node should have.
83 eps: float
84 Tolerance to check for equality.
85 """
86 return _np.abs(node.coordinates[axis] - value) < eps
89def get_min_max_coordinates(nodes):
90 """Return an array with the minimal and maximal coordinates of the given
91 nodes.
93 Return
94 ----
95 min_max_coordinates:
96 [min_x, min_y, min_z, max_x, max_y, max_z]
97 """
98 coordinates = _np.zeros([len(nodes), 3])
99 for i, node in enumerate(nodes):
100 coordinates[i, :] = node.coordinates
101 min_max = _np.zeros(6)
102 min_max[:3] = _np.min(coordinates, axis=0)
103 min_max[3:] = _np.max(coordinates, axis=0)
104 return min_max
107def get_single_node(item: _Union[_Node, _GeometrySetBase]) -> _NodeCosserat:
108 """Function to get a single node from the input item.
110 Args:
111 item: This can be a GeometrySet with exactly one node or a single node object.
113 Returns:
114 If a single node, or a Geometry set (point set) containing a single node
115 is given, that node is returned, otherwise an error is raised.
116 """
117 if isinstance(item, _Node):
118 node = item
119 elif isinstance(item, _GeometrySetBase):
120 # Check if there is only one node in the set
121 nodes = item.get_points()
122 if len(nodes) == 1:
123 node = nodes[0]
124 else:
125 raise ValueError("GeometrySet does not have exactly one node!")
126 else:
127 raise TypeError(
128 f'The given object can be node or GeometrySet got "{type(item)}"!'
129 )
131 if not isinstance(node, _NodeCosserat):
132 raise TypeError("Expected a NodeCosserat object.")
134 return node
137def filter_nodes(nodes, *, middle_nodes=True):
138 """Filter the list of the given nodes. Be aware that if no filters are
139 enabled the original list will be returned.
141 Args
142 ----
143 nodes: list(Nodes)
144 If this list is given it will be returned as is.
145 middle_nodes: bool
146 If middle nodes should be returned or not.
147 """
149 if not middle_nodes:
150 return [node for node in nodes if middle_nodes or not node.is_middle_node]
151 else:
152 return nodes
155def get_nodal_coordinates(nodes):
156 """Return an array with the coordinates of the given nodes.
158 Args
159 ----
160 kwargs:
161 Will be passed to self.get_global_nodes.
163 Return
164 ----
165 pos: _np.array
166 Numpy array with all the positions of the nodes.
167 """
168 coordinates = _np.zeros([len(nodes), 3])
169 for i, node in enumerate(nodes):
170 coordinates[i, :] = node.coordinates
171 return coordinates
174def get_nodal_quaternions(nodes: list[_Node]) -> _NDArray:
175 """Return an array with the quaternions of the given nodes.
177 Args:
178 nodes: List of nodes where we want the quaternion array.
179 Returns:
180 A numpy array containing the quaternions (the length is the number of
181 nodes and the dtype is a numpy quaternion). For nodes which don't
182 contain a rotation, we set the dummy quaternion (2, 0, 0, 0).
183 """
184 quaternions = _np.zeros([len(nodes), 4])
185 for i, node in enumerate(nodes):
186 if isinstance(node, _NodeCosserat):
187 quaternions[i, :] = node.rotation.get_quaternion()
188 else:
189 # For the case of nodes that belong to solid elements,
190 # we define the following default value:
191 quaternions[i, :] = [2.0, 0.0, 0.0, 0.0]
192 return quaternions
195def get_nodes_by_function(nodes, function, *args, middle_nodes=False, **kwargs):
196 """Return all nodes for which the function evaluates to true.
198 Args
199 ----
200 nodes: [Node]
201 Nodes that should be filtered.
202 function: function(node, *args, **kwargs)
203 Nodes for which this function is true are returned.
204 middle_nodes: bool
205 If this is true, middle nodes of a beam are also returned.
206 """
207 node_list = filter_nodes(nodes, middle_nodes=middle_nodes)
208 return [node for node in node_list if function(node, *args, **kwargs)]
211def get_min_max_nodes(nodes, *, middle_nodes=False):
212 """Return a geometry set with the max and min nodes in all directions.
214 Args
215 ----
216 nodes: list(Nodes)
217 If this one is given return an array with the coordinates of the
218 nodes in list, otherwise of all nodes in the mesh.
219 middle_nodes: bool
220 If this is true, middle nodes of a beam are also returned.
221 """
223 node_list = filter_nodes(nodes, middle_nodes=middle_nodes)
224 geometry = _GeometryName()
226 pos = get_nodal_coordinates(node_list)
227 for i, direction in enumerate(["x", "y", "z"]):
228 # Check if there is more than one value in dimension.
229 min_max = [_np.min(pos[:, i]), _np.max(pos[:, i])]
230 if _np.abs(min_max[1] - min_max[0]) >= _bme.eps_pos:
231 for j, text in enumerate(["min", "max"]):
232 # get all nodes with the min / max coordinate
233 min_max_nodes = []
234 for index, value in enumerate(
235 _np.abs(pos[:, i] - min_max[j]) < _bme.eps_pos
236 ):
237 if value:
238 min_max_nodes.append(node_list[index])
239 geometry[f"{direction}_{text}"] = _GeometrySet(min_max_nodes)
240 return geometry
243def is_node_on_plane(
244 node, *, normal=None, origin_distance=None, point_on_plane=None, tol=_bme.eps_pos
245):
246 """Query if a node lies on a plane defined by a point_on_plane or the
247 origin distance.
249 Args
250 ----
251 node:
252 Check if this node coincides with the defined plane.
253 normal: _np.array, list
254 Normal vector of defined plane.
255 origin_distance: float
256 Distance between origin and defined plane. Mutually exclusive with
257 point_on_plane.
258 point_on_plane: _np.array, list
259 Point on defined plane. Mutually exclusive with origin_distance.
260 tol: float
261 Tolerance of evaluation if point coincides with plane
263 Return
264 ----
265 True if the point lies on the plane, False otherwise.
266 """
268 if origin_distance is None and point_on_plane is None:
269 raise ValueError("Either provide origin_distance or point_on_plane!")
270 elif origin_distance is not None and point_on_plane is not None:
271 raise ValueError("Only provide origin_distance OR point_on_plane!")
273 if origin_distance is not None:
274 projection = _np.dot(node.coordinates, normal) / _np.linalg.norm(normal)
275 distance = _np.abs(projection - origin_distance)
276 elif point_on_plane is not None:
277 distance = _np.abs(
278 _np.dot(point_on_plane - node.coordinates, normal) / _np.linalg.norm(normal)
279 )
281 return distance < tol