Coverage for src/beamme/utils/nodes.py: 93%

88 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 15:20 +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"""Helper functions to find, filter and interact with nodes.""" 

23 

24import numpy as _np 

25from numpy.typing import NDArray as _NDArray 

26 

27from beamme.core.conf import bme as _bme 

28from beamme.core.geometry_set import GeometryName as _GeometryName 

29from beamme.core.geometry_set import GeometrySet as _GeometrySet 

30from beamme.core.geometry_set import GeometrySetBase as _GeometrySetBase 

31from beamme.core.node import Node as _Node 

32from beamme.core.node import NodeCosserat as _NodeCosserat 

33from beamme.geometric_search.find_close_points import ( 

34 find_close_points as _find_close_points, 

35) 

36from beamme.geometric_search.find_close_points import ( 

37 point_partners_to_partner_indices as _point_partners_to_partner_indices, 

38) 

39 

40 

41def find_close_nodes(nodes: list[_Node], **kwargs) -> list[list[_Node]]: 

42 """Find nodes in a point cloud that are within a certain tolerance of each other. 

43 

44 Args: 

45 nodes: Nodes who are part of the point cloud. 

46 **kwargs: Arguments passed on to geometric_search.find_close_points 

47 

48 Returns: 

49 A list of lists of nodes that are close to each other, i.e., 

50 each element in the returned list contains nodes that are close 

51 to each other. 

52 """ 

53 coords = _np.zeros([len(nodes), 3]) 

54 for i, node in enumerate(nodes): 

55 coords[i, :] = node.coordinates 

56 partner_indices = _point_partners_to_partner_indices( 

57 *_find_close_points(coords, **kwargs) 

58 ) 

59 return [[nodes[i] for i in partners] for partners in partner_indices] 

60 

61 

62def adjust_close_nodes(nodes: list[_Node], *, tol=_bme.eps_pos) -> None: 

63 """Adjust the coordinates of nodes that are within the given tolerance by setting 

64 all involved coordinates of the nodes to their common mean. 

65 

66 Args: 

67 nodes: List of nodes whose coordinates need adjustment. 

68 tol: Distance tolerance used to detect partner nodes. 

69 """ 

70 partner_nodes = find_close_nodes(nodes, tol=tol) 

71 for close_nodes in partner_nodes: 

72 average_coords = _np.mean([node.coordinates for node in close_nodes], axis=0) 

73 for node in close_nodes: 

74 node.coordinates = average_coords.copy() 

75 

76 

77def check_node_by_coordinate(node, axis, value, eps=_bme.eps_pos): 

78 """Check if the node is at a certain coordinate value. 

79 

80 Args 

81 ---- 

82 node: Node 

83 The node to be checked for its position. 

84 axis: int 

85 Coordinate axis to check. 

86 0 -> x, 1 -> y, 2 -> z 

87 value: float 

88 Value for the coordinate that the node should have. 

89 eps: float 

90 Tolerance to check for equality. 

91 """ 

92 return _np.abs(node.coordinates[axis] - value) < eps 

93 

94 

95def get_min_max_coordinates(nodes): 

96 """Return an array with the minimal and maximal coordinates of the given nodes. 

97 

98 Return 

99 ---- 

100 min_max_coordinates: 

101 [min_x, min_y, min_z, max_x, max_y, max_z] 

102 """ 

103 coordinates = _np.zeros([len(nodes), 3]) 

104 for i, node in enumerate(nodes): 

105 coordinates[i, :] = node.coordinates 

106 min_max = _np.zeros(6) 

107 min_max[:3] = _np.min(coordinates, axis=0) 

108 min_max[3:] = _np.max(coordinates, axis=0) 

109 return min_max 

110 

111 

112def get_single_node(item: _Node | _GeometrySetBase) -> _NodeCosserat: 

113 """Function to get a single node from the input item. 

114 

115 Args: 

116 item: This can be a GeometrySet with exactly one node or a single node object. 

117 

118 Returns: 

119 If a single node, or a Geometry set (point set) containing a single node 

120 is given, that node is returned, otherwise an error is raised. 

121 """ 

122 if isinstance(item, _Node): 

123 node = item 

124 elif isinstance(item, _GeometrySetBase): 

125 # Check if there is only one node in the set 

126 nodes = item.get_points() 

127 if len(nodes) == 1: 

128 node = nodes[0] 

129 else: 

130 raise ValueError("GeometrySet does not have exactly one node!") 

131 else: 

132 raise TypeError( 

133 f'The given object can be node or GeometrySet got "{type(item)}"!' 

134 ) 

135 

136 if not isinstance(node, _NodeCosserat): 

137 raise TypeError("Expected a NodeCosserat object.") 

138 

139 return node 

140 

141 

142def filter_nodes(nodes, *, middle_nodes=True) -> list[_Node]: 

143 """Filter the list of the given nodes. 

144 

145 Be aware that if no filters are enabled the original list will be returned. 

146 

147 Args 

148 ---- 

149 nodes: list(Nodes) 

150 If this list is given it will be returned as is. 

151 middle_nodes: bool 

152 If middle nodes should be returned or not. 

153 """ 

154 if not middle_nodes: 

155 return [node for node in nodes if middle_nodes or not node.is_middle_node] 

156 else: 

157 return nodes 

158 

159 

160def get_nodal_coordinates(nodes: list[_Node]) -> _NDArray: 

161 """Return an array with the coordinates of the given nodes. 

162 

163 Args: 

164 nodes: Nodes for which the coordinates should be returned. 

165 

166 Returns: 

167 Numpy array with all the positions of the nodes. 

168 """ 

169 coordinates = _np.zeros([len(nodes), 3]) 

170 for i, node in enumerate(nodes): 

171 coordinates[i, :] = node.coordinates 

172 return coordinates 

173 

174 

175def get_nodal_quaternions(nodes: list[_Node]) -> _NDArray: 

176 """Return an array with the quaternions of the given nodes. 

177 

178 Args: 

179 nodes: List of nodes where we want the quaternion array. 

180 Returns: 

181 A numpy array containing the quaternions (the length is the number of 

182 nodes and the dtype is a numpy quaternion). For nodes which don't 

183 contain a rotation, we set the dummy quaternion (2, 0, 0, 0). 

184 """ 

185 quaternions = _np.zeros([len(nodes), 4]) 

186 for i, node in enumerate(nodes): 

187 if isinstance(node, _NodeCosserat): 

188 quaternions[i, :] = node.rotation.get_quaternion() 

189 else: 

190 # For the case of nodes that belong to solid elements, 

191 # we define the following default value: 

192 quaternions[i, :] = [2.0, 0.0, 0.0, 0.0] 

193 return quaternions 

194 

195 

196def get_nodes_by_function(nodes, function, *args, middle_nodes=False, **kwargs): 

197 """Return all nodes for which the function evaluates to true. 

198 

199 Args 

200 ---- 

201 nodes: [Node] 

202 Nodes that should be filtered. 

203 function: function(node, *args, **kwargs) 

204 Nodes for which this function is true are returned. 

205 middle_nodes: bool 

206 If this is true, middle nodes of a beam are also returned. 

207 """ 

208 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

209 return [node for node in node_list if function(node, *args, **kwargs)] 

210 

211 

212def get_min_max_nodes(nodes, *, middle_nodes=False): 

213 """Return a geometry set with the max and min nodes in all directions. 

214 

215 Args 

216 ---- 

217 nodes: list(Nodes) 

218 If this one is given return an array with the coordinates of the 

219 nodes in list, otherwise of all nodes in the mesh. 

220 middle_nodes: bool 

221 If this is true, middle nodes of a beam are also returned. 

222 """ 

223 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

224 geometry = _GeometryName() 

225 

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 

241 

242 

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 origin 

247 distance. 

248 

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 

262 

263 Return 

264 ---- 

265 True if the point lies on the plane, False otherwise. 

266 """ 

267 if origin_distance is None and point_on_plane is None: 

268 raise ValueError("Either provide origin_distance or point_on_plane!") 

269 elif origin_distance is not None and point_on_plane is not None: 

270 raise ValueError("Only provide origin_distance OR point_on_plane!") 

271 

272 if origin_distance is not None: 

273 projection = _np.dot(node.coordinates, normal) / _np.linalg.norm(normal) 

274 distance = _np.abs(projection - origin_distance) 

275 elif point_on_plane is not None: 

276 distance = _np.abs( 

277 _np.dot(point_on_plane - node.coordinates, normal) / _np.linalg.norm(normal) 

278 ) 

279 

280 return distance < tol