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

89 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 12:57 +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.""" 

23 

24from typing import Union as _Union 

25 

26import numpy as _np 

27from numpy.typing import NDArray as _NDArray 

28 

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) 

41 

42 

43def find_close_nodes(nodes, **kwargs): 

44 """Find nodes in a point cloud that are within a certain tolerance of each 

45 other. 

46 

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 

53 

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 """ 

61 

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] 

69 

70 

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

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

73 setting all involved coordinates of the nodes to their common mean. 

74 

75 Args: 

76 nodes: List of nodes whose coordinates need adjustment. 

77 tol: Distance tolerance used to detect partner nodes. 

78 """ 

79 

80 partner_nodes = find_close_nodes(nodes, tol=tol) 

81 for close_nodes in partner_nodes: 

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

83 for node in close_nodes: 

84 node.coordinates = average_coords.copy() 

85 

86 

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

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

89 

90 Args 

91 ---- 

92 node: Node 

93 The node to be checked for its position. 

94 axis: int 

95 Coordinate axis to check. 

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

97 value: float 

98 Value for the coordinate that the node should have. 

99 eps: float 

100 Tolerance to check for equality. 

101 """ 

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

103 

104 

105def get_min_max_coordinates(nodes): 

106 """Return an array with the minimal and maximal coordinates of the given 

107 nodes. 

108 

109 Return 

110 ---- 

111 min_max_coordinates: 

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

113 """ 

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

115 for i, node in enumerate(nodes): 

116 coordinates[i, :] = node.coordinates 

117 min_max = _np.zeros(6) 

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

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

120 return min_max 

121 

122 

123def get_single_node(item: _Union[_Node, _GeometrySetBase]) -> _NodeCosserat: 

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

125 

126 Args: 

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

128 

129 Returns: 

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

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

132 """ 

133 if isinstance(item, _Node): 

134 node = item 

135 elif isinstance(item, _GeometrySetBase): 

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

137 nodes = item.get_points() 

138 if len(nodes) == 1: 

139 node = nodes[0] 

140 else: 

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

142 else: 

143 raise TypeError( 

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

145 ) 

146 

147 if not isinstance(node, _NodeCosserat): 

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

149 

150 return node 

151 

152 

153def filter_nodes(nodes, *, middle_nodes=True): 

154 """Filter the list of the given nodes. Be aware that if no filters are 

155 enabled the original list will be returned. 

156 

157 Args 

158 ---- 

159 nodes: list(Nodes) 

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

161 middle_nodes: bool 

162 If middle nodes should be returned or not. 

163 """ 

164 

165 if not middle_nodes: 

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

167 else: 

168 return nodes 

169 

170 

171def get_nodal_coordinates(nodes): 

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

173 

174 Args 

175 ---- 

176 kwargs: 

177 Will be passed to self.get_global_nodes. 

178 

179 Return 

180 ---- 

181 pos: _np.array 

182 Numpy array with all the positions of the nodes. 

183 """ 

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

185 for i, node in enumerate(nodes): 

186 coordinates[i, :] = node.coordinates 

187 return coordinates 

188 

189 

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

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

192 

193 Args: 

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

195 Returns: 

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

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

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

199 """ 

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

201 for i, node in enumerate(nodes): 

202 if isinstance(node, _NodeCosserat): 

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

204 else: 

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

206 # we define the following default value: 

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

208 return quaternions 

209 

210 

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

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

213 

214 Args 

215 ---- 

216 nodes: [Node] 

217 Nodes that should be filtered. 

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

219 Nodes for which this function is true are returned. 

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 return [node for node in node_list if function(node, *args, **kwargs)] 

225 

226 

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

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

229 

230 Args 

231 ---- 

232 nodes: list(Nodes) 

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

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

235 middle_nodes: bool 

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

237 """ 

238 

239 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

240 geometry = _GeometryName() 

241 

242 pos = get_nodal_coordinates(node_list) 

243 for i, direction in enumerate(["x", "y", "z"]): 

244 # Check if there is more than one value in dimension. 

245 min_max = [_np.min(pos[:, i]), _np.max(pos[:, i])] 

246 if _np.abs(min_max[1] - min_max[0]) >= _bme.eps_pos: 

247 for j, text in enumerate(["min", "max"]): 

248 # get all nodes with the min / max coordinate 

249 min_max_nodes = [] 

250 for index, value in enumerate( 

251 _np.abs(pos[:, i] - min_max[j]) < _bme.eps_pos 

252 ): 

253 if value: 

254 min_max_nodes.append(node_list[index]) 

255 geometry[f"{direction}_{text}"] = _GeometrySet(min_max_nodes) 

256 return geometry 

257 

258 

259def is_node_on_plane( 

260 node, *, normal=None, origin_distance=None, point_on_plane=None, tol=_bme.eps_pos 

261): 

262 """Query if a node lies on a plane defined by a point_on_plane or the 

263 origin distance. 

264 

265 Args 

266 ---- 

267 node: 

268 Check if this node coincides with the defined plane. 

269 normal: _np.array, list 

270 Normal vector of defined plane. 

271 origin_distance: float 

272 Distance between origin and defined plane. Mutually exclusive with 

273 point_on_plane. 

274 point_on_plane: _np.array, list 

275 Point on defined plane. Mutually exclusive with origin_distance. 

276 tol: float 

277 Tolerance of evaluation if point coincides with plane 

278 

279 Return 

280 ---- 

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

282 """ 

283 

284 if origin_distance is None and point_on_plane is None: 

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

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

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

288 

289 if origin_distance is not None: 

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

291 distance = _np.abs(projection - origin_distance) 

292 elif point_on_plane is not None: 

293 distance = _np.abs( 

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

295 ) 

296 

297 return distance < tol