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

88 statements  

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

43 other. 

44 

45 Args: 

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

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

48 

49 Returns: 

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

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

52 to each other. 

53 """ 

54 

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

56 for i, node in enumerate(nodes): 

57 coords[i, :] = node.coordinates 

58 partner_indices = _point_partners_to_partner_indices( 

59 *_find_close_points(coords, **kwargs) 

60 ) 

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

62 

63 

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

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

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

67 

68 Args: 

69 nodes: List of nodes whose coordinates need adjustment. 

70 tol: Distance tolerance used to detect partner nodes. 

71 """ 

72 

73 partner_nodes = find_close_nodes(nodes, tol=tol) 

74 for close_nodes in partner_nodes: 

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

76 for node in close_nodes: 

77 node.coordinates = average_coords.copy() 

78 

79 

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

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

82 

83 Args 

84 ---- 

85 node: Node 

86 The node to be checked for its position. 

87 axis: int 

88 Coordinate axis to check. 

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

90 value: float 

91 Value for the coordinate that the node should have. 

92 eps: float 

93 Tolerance to check for equality. 

94 """ 

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

96 

97 

98def get_min_max_coordinates(nodes): 

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

100 nodes. 

101 

102 Return 

103 ---- 

104 min_max_coordinates: 

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

106 """ 

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

108 for i, node in enumerate(nodes): 

109 coordinates[i, :] = node.coordinates 

110 min_max = _np.zeros(6) 

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

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

113 return min_max 

114 

115 

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

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

118 

119 Args: 

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

121 

122 Returns: 

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

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

125 """ 

126 if isinstance(item, _Node): 

127 node = item 

128 elif isinstance(item, _GeometrySetBase): 

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

130 nodes = item.get_points() 

131 if len(nodes) == 1: 

132 node = nodes[0] 

133 else: 

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

135 else: 

136 raise TypeError( 

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

138 ) 

139 

140 if not isinstance(node, _NodeCosserat): 

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

142 

143 return node 

144 

145 

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

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

148 enabled the original list will be returned. 

149 

150 Args 

151 ---- 

152 nodes: list(Nodes) 

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

154 middle_nodes: bool 

155 If middle nodes should be returned or not. 

156 """ 

157 

158 if not middle_nodes: 

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

160 else: 

161 return nodes 

162 

163 

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

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

166 

167 Args: 

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

169 

170 Returns: 

171 Numpy array with all the positions of the nodes. 

172 """ 

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

174 for i, node in enumerate(nodes): 

175 coordinates[i, :] = node.coordinates 

176 return coordinates 

177 

178 

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

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

181 

182 Args: 

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

184 Returns: 

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

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

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

188 """ 

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

190 for i, node in enumerate(nodes): 

191 if isinstance(node, _NodeCosserat): 

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

193 else: 

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

195 # we define the following default value: 

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

197 return quaternions 

198 

199 

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

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

202 

203 Args 

204 ---- 

205 nodes: [Node] 

206 Nodes that should be filtered. 

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

208 Nodes for which this function is true are returned. 

209 middle_nodes: bool 

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

211 """ 

212 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

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

214 

215 

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

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

218 

219 Args 

220 ---- 

221 nodes: list(Nodes) 

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

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

224 middle_nodes: bool 

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

226 """ 

227 

228 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

229 geometry = _GeometryName() 

230 

231 pos = get_nodal_coordinates(node_list) 

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

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

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

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

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

237 # get all nodes with the min / max coordinate 

238 min_max_nodes = [] 

239 for index, value in enumerate( 

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

241 ): 

242 if value: 

243 min_max_nodes.append(node_list[index]) 

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

245 return geometry 

246 

247 

248def is_node_on_plane( 

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

250): 

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

252 origin distance. 

253 

254 Args 

255 ---- 

256 node: 

257 Check if this node coincides with the defined plane. 

258 normal: _np.array, list 

259 Normal vector of defined plane. 

260 origin_distance: float 

261 Distance between origin and defined plane. Mutually exclusive with 

262 point_on_plane. 

263 point_on_plane: _np.array, list 

264 Point on defined plane. Mutually exclusive with origin_distance. 

265 tol: float 

266 Tolerance of evaluation if point coincides with plane 

267 

268 Return 

269 ---- 

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

271 """ 

272 

273 if origin_distance is None and point_on_plane is None: 

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

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

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

277 

278 if origin_distance is not None: 

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

280 distance = _np.abs(projection - origin_distance) 

281 elif point_on_plane is not None: 

282 distance = _np.abs( 

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

284 ) 

285 

286 return distance < tol