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

82 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 04:21 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2025 MeshPy 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 

27 

28from meshpy.core.conf import mpy as _mpy 

29from meshpy.core.geometry_set import GeometryName as _GeometryName 

30from meshpy.core.geometry_set import GeometrySet as _GeometrySet 

31from meshpy.core.geometry_set import GeometrySetBase as _GeometrySetBase 

32from meshpy.core.node import Node as _Node 

33from meshpy.core.node import NodeCosserat as _NodeCosserat 

34from meshpy.geometric_search.find_close_points import ( 

35 find_close_points as _find_close_points, 

36) 

37from meshpy.geometric_search.find_close_points import ( 

38 point_partners_to_partner_indices as _point_partners_to_partner_indices, 

39) 

40 

41 

42def find_close_nodes(nodes, **kwargs): 

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

44 other. 

45 

46 Args 

47 ---- 

48 nodes: list(Node) 

49 Nodes who are part of the point cloud. 

50 **kwargs: 

51 Arguments passed on to geometric_search.find_close_points 

52 

53 Return 

54 ---- 

55 partner_nodes: list(list(Node)) 

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

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

58 to each other. 

59 """ 

60 

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

62 for i, node in enumerate(nodes): 

63 coords[i, :] = node.coordinates 

64 partner_indices = _point_partners_to_partner_indices( 

65 *_find_close_points(coords, **kwargs) 

66 ) 

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

68 

69 

70def check_node_by_coordinate(node, axis, value, eps=_mpy.eps_pos): 

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

72 

73 Args 

74 ---- 

75 node: Node 

76 The node to be checked for its position. 

77 axis: int 

78 Coordinate axis to check. 

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

80 value: float 

81 Value for the coordinate that the node should have. 

82 eps: float 

83 Tolerance to check for equality. 

84 """ 

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

86 

87 

88def get_min_max_coordinates(nodes): 

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

90 nodes. 

91 

92 Return 

93 ---- 

94 min_max_coordinates: 

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

96 """ 

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

98 for i, node in enumerate(nodes): 

99 coordinates[i, :] = node.coordinates 

100 min_max = _np.zeros(6) 

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

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

103 return min_max 

104 

105 

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

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

108 

109 Args: 

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

111 

112 Returns: 

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

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

115 """ 

116 if isinstance(item, _Node): 

117 node = item 

118 elif isinstance(item, _GeometrySetBase): 

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

120 nodes = item.get_points() 

121 if len(nodes) == 1: 

122 node = nodes[0] 

123 else: 

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

125 else: 

126 raise TypeError( 

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

128 ) 

129 

130 if not isinstance(node, _NodeCosserat): 

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

132 

133 return node 

134 

135 

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

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

138 enabled the original list will be returned. 

139 

140 Args 

141 ---- 

142 nodes: list(Nodes) 

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

144 middle_nodes: bool 

145 If middle nodes should be returned or not. 

146 """ 

147 

148 if not middle_nodes: 

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

150 else: 

151 return nodes 

152 

153 

154def get_nodal_coordinates(nodes): 

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

156 

157 Args 

158 ---- 

159 kwargs: 

160 Will be passed to self.get_global_nodes. 

161 

162 Return 

163 ---- 

164 pos: _np.array 

165 Numpy array with all the positions of the nodes. 

166 """ 

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

168 for i, node in enumerate(nodes): 

169 coordinates[i, :] = node.coordinates 

170 return coordinates 

171 

172 

173def get_nodal_quaternions(nodes): 

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

175 

176 Args 

177 ---- 

178 kwargs: 

179 Will be passed to self.get_global_nodes. 

180 

181 Return 

182 ---- 

183 pos: _np.array 

184 Numpy array with all the positions of the nodes. 

185 """ 

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

187 for i, node in enumerate(nodes): 

188 if isinstance(node, _NodeCosserat): 

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

190 else: 

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

192 # we define the following default value: 

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

194 return quaternions 

195 

196 

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

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

199 

200 Args 

201 ---- 

202 nodes: [Node] 

203 Nodes that should be filtered. 

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

205 Nodes for which this function is true are returned. 

206 middle_nodes: bool 

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

208 """ 

209 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

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

211 

212 

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

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

215 

216 Args 

217 ---- 

218 nodes: list(Nodes) 

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

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

221 middle_nodes: bool 

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

223 """ 

224 

225 node_list = filter_nodes(nodes, middle_nodes=middle_nodes) 

226 geometry = _GeometryName() 

227 

228 pos = get_nodal_coordinates(node_list) 

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

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

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

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

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

234 # get all nodes with the min / max coordinate 

235 min_max_nodes = [] 

236 for index, value in enumerate( 

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

238 ): 

239 if value: 

240 min_max_nodes.append(node_list[index]) 

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

242 return geometry 

243 

244 

245def is_node_on_plane( 

246 node, *, normal=None, origin_distance=None, point_on_plane=None, tol=_mpy.eps_pos 

247): 

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

249 origin distance. 

250 

251 Args 

252 ---- 

253 node: 

254 Check if this node coincides with the defined plane. 

255 normal: _np.array, list 

256 Normal vector of defined plane. 

257 origin_distance: float 

258 Distance between origin and defined plane. Mutually exclusive with 

259 point_on_plane. 

260 point_on_plane: _np.array, list 

261 Point on defined plane. Mutually exclusive with origin_distance. 

262 tol: float 

263 Tolerance of evaluation if point coincides with plane 

264 

265 Return 

266 ---- 

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

268 """ 

269 

270 if origin_distance is None and point_on_plane is None: 

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

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

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

274 

275 if origin_distance is not None: 

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

277 distance = _np.abs(projection - origin_distance) 

278 elif point_on_plane is not None: 

279 distance = _np.abs( 

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

281 ) 

282 

283 return distance < tol