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

132 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-06-13 04:26 +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 

25from meshpy.core.base_mesh_item import BaseMeshItem as _BaseMeshItem 

26from meshpy.core.conf import mpy as _mpy 

27from meshpy.core.container import ContainerBase as _ContainerBase 

28from meshpy.core.element_beam import Beam as _Beam 

29from meshpy.core.node import Node as _Node 

30 

31 

32class GeometrySetBase(_BaseMeshItem): 

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

34 

35 # Node set names for the input file file. 

36 geometry_set_names = { 

37 _mpy.geo.point: "DNODE", 

38 _mpy.geo.line: "DLINE", 

39 _mpy.geo.surface: "DSURFACE", 

40 _mpy.geo.volume: "DVOL", 

41 } 

42 

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

44 """Initialize the geometry set. 

45 

46 Args 

47 ---- 

48 geometry_type: mpy.geo 

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

50 specified geometry type. 

51 name: str 

52 Optional name to identify this geometry set. 

53 """ 

54 super().__init__(**kwargs) 

55 

56 self.geometry_type = geometry_type 

57 self.name = name 

58 

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

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

61 geometry set. 

62 

63 link_to_nodes: str 

64 "explicitly_contained_nodes": 

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

66 "all_nodes": 

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

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

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

70 """ 

71 if link_to_nodes == "explicitly_contained_nodes": 

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

73 elif link_to_nodes == "all_nodes": 

74 node_list = self.get_all_nodes() 

75 else: 

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

77 for node in node_list: 

78 node.node_sets_link.append(self) 

79 

80 def check_replaced_nodes(self): 

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

82 

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

84 """ 

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

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

87 if node.master_node is not None: 

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

89 

90 def replace_node(self, old_node, new_node): 

91 """Replace old_node with new_node.""" 

92 

93 explicit_nodes_in_this_set = self.get_node_dict() 

94 explicit_nodes_in_this_set[new_node] = None 

95 del explicit_nodes_in_this_set[old_node] 

96 

97 def get_node_dict(self): 

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

99 set.""" 

100 raise NotImplementedError( 

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

102 ) 

103 

104 def get_points(self): 

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

106 raise NotImplementedError( 

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

108 ) 

109 

110 def get_all_nodes(self): 

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

112 

113 This includes nodes contained within the geometry added to this 

114 set. 

115 """ 

116 raise NotImplementedError( 

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

118 ) 

119 

120 def dump_to_list(self): 

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

122 

123 # Sort nodes based on their global index 

124 nodes = sorted(self.get_all_nodes(), key=lambda n: n.i_global) 

125 

126 if not nodes: 

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

128 

129 return [ 

130 { 

131 "type": "NODE", 

132 "node_id": node.i_global, 

133 "d_type": self.geometry_set_names[self.geometry_type], 

134 "d_id": self.i_global, 

135 } 

136 for node in nodes 

137 ] 

138 

139 

140class GeometrySet(GeometrySetBase): 

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

142 

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

144 """Initialize the geometry set. 

145 

146 Args 

147 ---- 

148 geometry: _List or single Geometry/GeometrySet 

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

150 are not supported. 

151 """ 

152 

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

154 if isinstance(geometry, list): 

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

156 else: 

157 geometry_type = self._get_geometry_type(geometry) 

158 

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

160 

161 self.geometry_objects = {} 

162 for geo in _mpy.geo: 

163 self.geometry_objects[geo] = {} 

164 self.add(geometry) 

165 

166 @staticmethod 

167 def _get_geometry_type(item): 

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

169 

170 if isinstance(item, _Node): 

171 return _mpy.geo.point 

172 elif isinstance(item, _Beam): 

173 return _mpy.geo.line 

174 elif isinstance(item, GeometrySet): 

175 return item.geometry_type 

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

177 

178 def add(self, item): 

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

180 

181 if isinstance(item, list): 

182 for sub_item in item: 

183 self.add(sub_item) 

184 elif isinstance(item, GeometrySet): 

185 if item.geometry_type is self.geometry_type: 

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

187 self.add(geometry) 

188 else: 

189 raise TypeError( 

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

191 "This is not possible" 

192 ) 

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

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

195 else: 

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

197 

198 def get_node_dict(self): 

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

200 set. 

201 

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

203 """ 

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

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

206 else: 

207 return {} 

208 

209 def get_points(self): 

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

211 

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

213 """ 

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

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

216 else: 

217 raise TypeError( 

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

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

220 ) 

221 

222 def get_all_nodes(self): 

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

224 

225 This includes nodes contained within the geometry added to this 

226 set. 

227 """ 

228 

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

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

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

232 nodes = [] 

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

234 nodes.extend(element.nodes) 

235 # Remove duplicates while preserving order 

236 return list(dict.fromkeys(nodes)) 

237 else: 

238 raise TypeError( 

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

240 ) 

241 

242 def get_geometry_objects(self): 

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

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

245 

246 

247class GeometrySetNodes(GeometrySetBase): 

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

249 

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

251 """Initialize the geometry set. 

252 

253 Args 

254 ---- 

255 geometry_type: mpy.geo 

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

257 and input file depend on that type. 

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

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

260 """ 

261 

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

263 self.nodes = {} 

264 if nodes is not None: 

265 self.add(nodes) 

266 

267 def add(self, value): 

268 """Add nodes to this object. 

269 

270 Args 

271 ---- 

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

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

274 """ 

275 

276 if isinstance(value, list): 

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

278 # This improves the performance considerably when large list of 

279 # Nodes are added. 

280 for item in value: 

281 self.add(item) 

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

283 self.nodes[value] = None 

284 elif isinstance(value, GeometrySetNodes): 

285 # Add all nodes from this geometry set. 

286 if self.geometry_type == value.geometry_type: 

287 for node in value.nodes: 

288 self.add(node) 

289 else: 

290 raise TypeError( 

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

292 "This is not possible" 

293 ) 

294 else: 

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

296 

297 def get_node_dict(self): 

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

299 set.""" 

300 return self.nodes 

301 

302 def get_points(self): 

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

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

305 return self.get_all_nodes() 

306 else: 

307 raise TypeError( 

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

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

310 ) 

311 

312 def get_all_nodes(self): 

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

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

315 

316 

317class GeometryName(dict): 

318 """Group node geometry sets together. 

319 

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

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

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

323 meaningful names. 

324 """ 

325 

326 def __setitem__(self, key, value): 

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

328 

329 if not isinstance(key, str): 

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

331 if isinstance(value, GeometrySetBase): 

332 super().__setitem__(key, value) 

333 else: 

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

335 

336 

337class GeometrySetContainer(_ContainerBase): 

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

339 type.""" 

340 

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

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

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

344 

345 self.item_types = [GeometrySetBase] 

346 

347 for geometry_key in _mpy.geo: 

348 self[geometry_key] = [] 

349 

350 def copy(self): 

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

352 be copied also.""" 

353 

354 # Create a new geometry set container. 

355 copy = GeometrySetContainer() 

356 

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

358 for geometry_key in _mpy.geo: 

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

360 

361 return copy