Coverage for src/meshpy/core/geometry_set.py: 88%

139 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"""This module implements a basic class to manage geometry in the input 

23file.""" 

24 

25import numpy as _np 

26 

27from meshpy.core.base_mesh_item import BaseMeshItem as _BaseMeshItem 

28from meshpy.core.conf import mpy as _mpy 

29from meshpy.core.container import ContainerBase as _ContainerBase 

30from meshpy.core.element_beam import Beam as _Beam 

31from meshpy.core.node import Node as _Node 

32from meshpy.utils.environment import fourcipp_is_available as _fourcipp_is_available 

33 

34 

35class GeometrySetBase(_BaseMeshItem): 

36 """Base class for a geometry set.""" 

37 

38 # Node set names for the input file file. 

39 geometry_set_names = { 

40 _mpy.geo.point: "DNODE", 

41 _mpy.geo.line: "DLINE", 

42 _mpy.geo.surface: "DSURFACE", 

43 _mpy.geo.volume: "DVOL", 

44 } 

45 

46 def __init__(self, geometry_type, name=None, **kwargs): 

47 """Initialize the geometry set. 

48 

49 Args 

50 ---- 

51 geometry_type: mpy.geo 

52 Type of geometry. MeshPy only supports geometry sets of a single 

53 specified geometry type. 

54 name: str 

55 Optional name to identify this geometry set. 

56 """ 

57 super().__init__(**kwargs) 

58 

59 self.geometry_type = geometry_type 

60 self.name = name 

61 

62 def link_to_nodes(self, *, link_to_nodes="explicitly_contained_nodes"): 

63 """Set a link to this object in the all contained nodes of this 

64 geometry set. 

65 

66 link_to_nodes: str 

67 "explicitly_contained_nodes": 

68 A link will be set for all nodes that are explicitly part of the geometry set 

69 "all_nodes": 

70 A link will be set for all nodes that are part of the geometry set, i.e., also 

71 nodes connected to elements of an element set. This is mainly used for vtk 

72 output so we can color the nodes which are part of element sets. 

73 """ 

74 if link_to_nodes == "explicitly_contained_nodes": 

75 node_list = self.get_node_dict().keys() 

76 elif link_to_nodes == "all_nodes": 

77 node_list = self.get_all_nodes() 

78 else: 

79 raise ValueError(f'Got unexpected value link nodes="{link_to_nodes}"') 

80 for node in node_list: 

81 node.node_sets_link.append(self) 

82 

83 def check_replaced_nodes(self): 

84 """Check if nodes in this set have to be replaced. 

85 

86 We need to do this for explicitly contained nodes in this set. 

87 """ 

88 # Don't iterate directly over the keys as the dict changes during this iteration 

89 for node in list(self.get_node_dict().keys()): 

90 if node.master_node is not None: 

91 self.replace_node(node, node.get_master_node()) 

92 

93 def replace_node(self, old_node, new_node): 

94 """Replace old_node with new_node.""" 

95 

96 explicit_nodes_in_this_set = self.get_node_dict() 

97 explicit_nodes_in_this_set[new_node] = None 

98 del explicit_nodes_in_this_set[old_node] 

99 

100 def get_node_dict(self): 

101 """Return the dictionary containing the explicitly added nodes for this 

102 set.""" 

103 raise NotImplementedError( 

104 'The "get_node_dict" method has to be overwritten in the derived class' 

105 ) 

106 

107 def get_points(self): 

108 """Return nodes explicitly associated with this set.""" 

109 raise NotImplementedError( 

110 'The "get_points" method has to be overwritten in the derived class' 

111 ) 

112 

113 def get_all_nodes(self): 

114 """Return all nodes associated with this set. 

115 

116 This includes nodes contained within the geometry added to this 

117 set. 

118 """ 

119 raise NotImplementedError( 

120 'The "get_all_nodes" method has to be overwritten in the derived class' 

121 ) 

122 

123 def dump_to_list(self): 

124 """Return a list with the legacy strings of this geometry set.""" 

125 

126 if _fourcipp_is_available(): 

127 raise ValueError( 

128 "Port this functionality to dump the geometry set to a suitable data format" 

129 ) 

130 

131 # Sort the nodes based on the node GID. 

132 nodes = self.get_all_nodes() 

133 if len(nodes) == 0: 

134 raise ValueError("Writing empty geometry sets is not supported") 

135 nodes_id = [node.i_global for node in nodes] 

136 sort_indices = _np.argsort(nodes_id) 

137 nodes = [nodes[i] for i in sort_indices] 

138 

139 return [ 

140 f"NODE {node.i_global} {self.geometry_set_names[self.geometry_type]} {self.i_global}" 

141 for node in nodes 

142 ] 

143 

144 

145class GeometrySet(GeometrySetBase): 

146 """Geometry set which is defined by geometric entries.""" 

147 

148 def __init__(self, geometry, **kwargs): 

149 """Initialize the geometry set. 

150 

151 Args 

152 ---- 

153 geometry: _List or single Geometry/GeometrySet 

154 Geometries associated with this set. Empty geometries (i.e., no given) 

155 are not supported. 

156 """ 

157 

158 # This is ok, we check every single type in the add method 

159 if isinstance(geometry, list): 

160 geometry_type = self._get_geometry_type(geometry[0]) 

161 else: 

162 geometry_type = self._get_geometry_type(geometry) 

163 

164 super().__init__(geometry_type, **kwargs) 

165 

166 self.geometry_objects = {} 

167 for geo in _mpy.geo: 

168 self.geometry_objects[geo] = {} 

169 self.add(geometry) 

170 

171 @staticmethod 

172 def _get_geometry_type(item): 

173 """Return the geometry type of a given item.""" 

174 

175 if isinstance(item, _Node): 

176 return _mpy.geo.point 

177 elif isinstance(item, _Beam): 

178 return _mpy.geo.line 

179 elif isinstance(item, GeometrySet): 

180 return item.geometry_type 

181 raise TypeError(f"Got unexpected type {type(item)}") 

182 

183 def add(self, item): 

184 """Add a geometry item to this object.""" 

185 

186 if isinstance(item, list): 

187 for sub_item in item: 

188 self.add(sub_item) 

189 elif isinstance(item, GeometrySet): 

190 if item.geometry_type is self.geometry_type: 

191 for geometry in item.geometry_objects[self.geometry_type]: 

192 self.add(geometry) 

193 else: 

194 raise TypeError( 

195 "You tried to add a {item.geometry_type} set to a {self.geometry_type} set. " 

196 "This is not possible" 

197 ) 

198 elif self._get_geometry_type(item) is self.geometry_type: 

199 self.geometry_objects[self.geometry_type][item] = None 

200 else: 

201 raise TypeError(f"Got unexpected geometry type {type(item)}") 

202 

203 def get_node_dict(self): 

204 """Return the dictionary containing the explicitly added nodes for this 

205 set. 

206 

207 For non-point sets an empty dict is returned. 

208 """ 

209 if self.geometry_type is _mpy.geo.point: 

210 return self.geometry_objects[_mpy.geo.point] 

211 else: 

212 return {} 

213 

214 def get_points(self): 

215 """Return nodes explicitly associated with this set. 

216 

217 Only in case this is a point set something is returned here. 

218 """ 

219 if self.geometry_type is _mpy.geo.point: 

220 return list(self.geometry_objects[_mpy.geo.point].keys()) 

221 else: 

222 raise TypeError( 

223 "The function get_points can only be called for point sets." 

224 f" The present type is {self.geometry_type}" 

225 ) 

226 

227 def get_all_nodes(self): 

228 """Return all nodes associated with this set. 

229 

230 This includes nodes contained within the geometry added to this 

231 set. 

232 """ 

233 

234 if self.geometry_type is _mpy.geo.point: 

235 return list(self.geometry_objects[_mpy.geo.point].keys()) 

236 elif self.geometry_type is _mpy.geo.line: 

237 nodes = [] 

238 for element in self.geometry_objects[_mpy.geo.line].keys(): 

239 nodes.extend(element.nodes) 

240 # Remove duplicates while preserving order 

241 return list(dict.fromkeys(nodes)) 

242 else: 

243 raise TypeError( 

244 "Currently GeometrySet are only implemented for points and lines" 

245 ) 

246 

247 def get_geometry_objects(self): 

248 """Return a list of the objects with the specified geometry type.""" 

249 return list(self.geometry_objects[self.geometry_type].keys()) 

250 

251 

252class GeometrySetNodes(GeometrySetBase): 

253 """Geometry set which is defined by nodes and not explicit geometry.""" 

254 

255 def __init__(self, geometry_type, nodes=None, **kwargs): 

256 """Initialize the geometry set. 

257 

258 Args 

259 ---- 

260 geometry_type: mpy.geo 

261 Type of geometry. This is necessary, as the boundary conditions 

262 and input file depend on that type. 

263 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes) 

264 Node(s) or list of nodes to be added to this geometry set. 

265 """ 

266 

267 super().__init__(geometry_type, **kwargs) 

268 self.nodes = {} 

269 if nodes is not None: 

270 self.add(nodes) 

271 

272 def add(self, value): 

273 """Add nodes to this object. 

274 

275 Args 

276 ---- 

277 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes) 

278 Node(s) or list of nodes to be added to this geometry set. 

279 """ 

280 

281 if isinstance(value, list): 

282 # Loop over items and check if they are either Nodes or integers. 

283 # This improves the performance considerably when large list of 

284 # Nodes are added. 

285 for item in value: 

286 self.add(item) 

287 elif isinstance(value, (int, _Node)): 

288 self.nodes[value] = None 

289 elif isinstance(value, GeometrySetNodes): 

290 # Add all nodes from this geometry set. 

291 if self.geometry_type == value.geometry_type: 

292 for node in value.nodes: 

293 self.add(node) 

294 else: 

295 raise TypeError( 

296 f"You tried to add a {value.geometry_type} set to a {self.geometry_type} set. " 

297 "This is not possible" 

298 ) 

299 else: 

300 raise TypeError(f"Expected Node or list, but got {type(value)}") 

301 

302 def get_node_dict(self): 

303 """Return the dictionary containing the explicitly added nodes for this 

304 set.""" 

305 return self.nodes 

306 

307 def get_points(self): 

308 """Return nodes explicitly associated with this set.""" 

309 if self.geometry_type is _mpy.geo.point: 

310 return self.get_all_nodes() 

311 else: 

312 raise TypeError( 

313 "The function get_points can only be called for point sets." 

314 f" The present type is {self.geometry_type}" 

315 ) 

316 

317 def get_all_nodes(self): 

318 """Return all nodes associated with this set.""" 

319 return list(self.nodes.keys()) 

320 

321 

322class GeometryName(dict): 

323 """Group node geometry sets together. 

324 

325 This is mainly used for export from mesh functions. The sets can be 

326 accessed by a unique name. There is no distinction between different 

327 types of geometry, every name can only be used once -> use 

328 meaningful names. 

329 """ 

330 

331 def __setitem__(self, key, value): 

332 """Set a geometry set in this container.""" 

333 

334 if not isinstance(key, str): 

335 raise TypeError(f"Expected string, got {type(key)}!") 

336 if isinstance(value, GeometrySetBase): 

337 super().__setitem__(key, value) 

338 else: 

339 raise NotImplementedError("GeometryName can only store GeometrySets") 

340 

341 

342class GeometrySetContainer(_ContainerBase): 

343 """A class to group geometry sets together with the key being the geometry 

344 type.""" 

345 

346 def __init__(self, *args, **kwargs): 

347 """Initialize the container and create the default keys in the map.""" 

348 super().__init__(*args, **kwargs) 

349 

350 self.item_types = [GeometrySetBase] 

351 

352 for geometry_key in _mpy.geo: 

353 self[geometry_key] = [] 

354 

355 def copy(self): 

356 """When creating a copy of this object, all lists in this object will 

357 be copied also.""" 

358 

359 # Create a new geometry set container. 

360 copy = GeometrySetContainer() 

361 

362 # Add a copy of every list from this container to the new one. 

363 for geometry_key in _mpy.geo: 

364 copy[geometry_key] = self[geometry_key].copy() 

365 

366 return copy