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

148 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-30 18:48 +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"""This module implements a basic class to manage geometry in the input 

23file.""" 

24 

25from typing import KeysView as _KeysView 

26from typing import Sequence as _Sequence 

27from typing import Union as _Union 

28from typing import cast as _cast 

29 

30import beamme.core.conf as _conf 

31from beamme.core.base_mesh_item import BaseMeshItem as _BaseMeshItem 

32from beamme.core.conf import mpy as _mpy 

33from beamme.core.container import ContainerBase as _ContainerBase 

34from beamme.core.element import Element as _Element 

35from beamme.core.element_beam import Beam as _Beam 

36from beamme.core.node import Node as _Node 

37 

38 

39class GeometrySetBase(_BaseMeshItem): 

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

41 

42 # Node set names for the input file file. 

43 geometry_set_names = { 

44 _mpy.geo.point: "DNODE", 

45 _mpy.geo.line: "DLINE", 

46 _mpy.geo.surface: "DSURFACE", 

47 _mpy.geo.volume: "DVOL", 

48 } 

49 

50 def __init__( 

51 self, geometry_type: _conf.Geometry, name: str | None = None, **kwargs 

52 ): 

53 """Initialize the geometry set. 

54 

55 Args: 

56 geometry_type: Type of geometry. Only geometry sets of a single specified geometry type are supported. 

57 name: Optional name to identify this geometry set. 

58 """ 

59 super().__init__(**kwargs) 

60 

61 self.geometry_type = geometry_type 

62 self.name = name 

63 

64 def link_to_nodes( 

65 self, *, link_to_nodes: str = "explicitly_contained_nodes" 

66 ) -> None: 

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

68 geometry set. 

69 

70 Args: 

71 link_to_nodes: 

72 "explicitly_contained_nodes": 

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

74 "all_nodes": 

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

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

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

78 """ 

79 node_list: list[_Node] | _KeysView[_Node] 

80 if link_to_nodes == "explicitly_contained_nodes": 

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

82 elif link_to_nodes == "all_nodes": 

83 node_list = self.get_all_nodes() 

84 else: 

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

86 for node in node_list: 

87 node.node_sets_link.append(self) 

88 

89 def check_replaced_nodes(self) -> None: 

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

91 

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

93 """ 

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

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

96 if node.master_node is not None: 

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

98 

99 def replace_node(self, old_node: _Node, new_node: _Node) -> None: 

100 """Replace an existing node in this geometry set with a new one. 

101 

102 Args: 

103 old_node: Node to be replaced. 

104 new_node: Node that will be placed instead of old_node. 

105 """ 

106 

107 explicit_nodes_in_this_set = self.get_node_dict() 

108 explicit_nodes_in_this_set[new_node] = None 

109 del explicit_nodes_in_this_set[old_node] 

110 

111 def get_node_dict(self) -> dict[_Node, None]: 

112 """Determine the explicitly added nodes for this set, i.e., nodes 

113 contained in elements are not returned. 

114 

115 Returns: 

116 A dictionary containing the explicitly added nodes for this set. 

117 """ 

118 raise NotImplementedError( 

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

120 ) 

121 

122 def get_points(self) -> list[_Node]: 

123 """Determine all points (represented by nodes) for this set. 

124 

125 This function only works for point sets. 

126 

127 Returns: 

128 A list containing the points (represented by nodes) associated with this set. 

129 """ 

130 raise NotImplementedError( 

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

132 ) 

133 

134 def get_all_nodes(self) -> list[_Node]: 

135 """Determine all nodes associated with this set. 

136 

137 This includes nodes contained within the geometry added to this 

138 set, e.g., nodes connected to elements in element sets. 

139 

140 Returns: 

141 A list containing all associated nodes. 

142 """ 

143 raise NotImplementedError( 

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

145 ) 

146 

147 def dump_to_list(self): 

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

149 

150 # Sort nodes based on their global index 

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

152 

153 if not nodes: 

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

155 

156 return [ 

157 { 

158 "type": "NODE", 

159 "node_id": node.i_global, 

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

161 "d_id": self.i_global, 

162 } 

163 for node in nodes 

164 ] 

165 

166 def __add__(self, other): 

167 """Create a new geometry set with the combined geometries from this set 

168 and the other set. 

169 

170 Args: 

171 other: Geometry set to be added to this one. This has to be of the same geometry type as this set. 

172 Returns: 

173 A combined geometry set. 

174 """ 

175 combined_set = self.copy() 

176 combined_set.add(other) 

177 return combined_set 

178 

179 

180class GeometrySet(GeometrySetBase): 

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

182 

183 def __init__( 

184 self, 

185 geometry: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet", 

186 **kwargs, 

187 ): 

188 """Initialize the geometry set. 

189 

190 Args: 

191 geometry: Geometry entries to be contained in this set. 

192 """ 

193 

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

195 if isinstance(geometry, list): 

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

197 else: 

198 geometry_type = self._get_geometry_type(geometry) 

199 

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

201 

202 self.geometry_objects: dict[_conf.Geometry, dict[_Node | _Element, None]] = {} 

203 for geo in _mpy.geo: 

204 self.geometry_objects[geo] = {} 

205 self.add(geometry) 

206 

207 @staticmethod 

208 def _get_geometry_type( 

209 item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet", 

210 ) -> _conf.Geometry: 

211 """Get the geometry type of a given item. 

212 

213 Returns: 

214 Geometry type of the geometry set. 

215 """ 

216 

217 if isinstance(item, _Node): 

218 return _mpy.geo.point 

219 elif isinstance(item, _Beam): 

220 return _mpy.geo.line 

221 elif isinstance(item, GeometrySet): 

222 return item.geometry_type 

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

224 

225 def add( 

226 self, item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet" 

227 ) -> None: 

228 """Add geometry item(s) to this object.""" 

229 

230 if isinstance(item, list): 

231 for sub_item in item: 

232 self.add(sub_item) 

233 elif isinstance(item, GeometrySet): 

234 if item.geometry_type is self.geometry_type: 

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

236 self.add(geometry) 

237 else: 

238 raise TypeError( 

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

240 "This is not possible" 

241 ) 

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

243 self.geometry_objects[self.geometry_type][_cast(_Node | _Element, item)] = ( 

244 None 

245 ) 

246 else: 

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

248 

249 def get_node_dict(self) -> dict[_Node, None]: 

250 """Determine the explicitly added nodes for this set, i.e., nodes 

251 contained in elements for element sets are not returned. 

252 

253 Thus, for non-point sets an empty dict is returned. 

254 

255 Returns: 

256 A dictionary containing the explicitly added nodes for this set. 

257 """ 

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

259 return _cast(dict[_Node, None], self.geometry_objects[_mpy.geo.point]) 

260 else: 

261 return {} 

262 

263 def get_points(self) -> list[_Node]: 

264 """Determine all points (represented by nodes) for this set. 

265 

266 This function only works for point sets. 

267 

268 Returns: 

269 A list containing the points (represented by nodes) associated with this set. 

270 """ 

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

272 return list(self.get_node_dict().keys()) 

273 else: 

274 raise TypeError( 

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

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

277 ) 

278 

279 def get_all_nodes(self) -> list[_Node]: 

280 """Determine all nodes associated with this set. 

281 

282 This includes nodes contained within the geometry added to this 

283 set, e.g., nodes connected to elements in element sets. 

284 

285 Returns: 

286 A list containing all associated nodes. 

287 """ 

288 

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

290 return list( 

291 _cast(_KeysView[_Node], self.geometry_objects[_mpy.geo.point].keys()) 

292 ) 

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

294 nodes = [] 

295 for element in _cast( 

296 _KeysView[_Element], self.geometry_objects[_mpy.geo.line].keys() 

297 ): 

298 nodes.extend(element.nodes) 

299 # Remove duplicates while preserving order 

300 return list(dict.fromkeys(nodes)) 

301 else: 

302 raise TypeError( 

303 "Currently GeometrySet is only implemented for points and lines" 

304 ) 

305 

306 def get_geometry_objects(self) -> _Sequence[_Node | _Element]: 

307 """Get a list of the objects with the specified geometry type. 

308 

309 Returns: 

310 A list with the contained geometry. 

311 """ 

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

313 

314 def copy(self) -> "GeometrySet": 

315 """Create a shallow copy of this object, the reference to the nodes 

316 will be the same, but the containers storing them will be copied. 

317 

318 Returns: 

319 A shallow copy of the geometry set. 

320 """ 

321 return GeometrySet(list(self.geometry_objects[self.geometry_type].keys())) 

322 

323 

324class GeometrySetNodes(GeometrySetBase): 

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

326 

327 def __init__( 

328 self, 

329 geometry_type: _conf.Geometry, 

330 nodes: _Union[_Node, list[_Node], "GeometrySetNodes", None] = None, 

331 **kwargs, 

332 ): 

333 """Initialize the geometry set. 

334 

335 Args: 

336 geometry_type: Type of geometry. This is necessary, as the boundary conditions 

337 and input file depend on that type. 

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

339 """ 

340 

341 if geometry_type not in _mpy.geo: 

342 raise TypeError(f"Expected geometry enum, got {geometry_type}") 

343 

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

345 self.nodes: dict[_Node, None] = {} 

346 if nodes is not None: 

347 self.add(nodes) 

348 

349 def add(self, value: _Union[_Node, list[_Node], "GeometrySetNodes"]) -> None: 

350 """Add nodes to this object. 

351 

352 Args: 

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

354 """ 

355 

356 if isinstance(value, list): 

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

358 # This improves the performance considerably when large list of 

359 # Nodes are added. 

360 for item in value: 

361 self.add(item) 

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

363 self.nodes[value] = None 

364 elif isinstance(value, GeometrySetNodes): 

365 # Add all nodes from this geometry set. 

366 if self.geometry_type == value.geometry_type: 

367 for node in value.nodes: 

368 self.add(node) 

369 else: 

370 raise TypeError( 

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

372 "This is not possible" 

373 ) 

374 else: 

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

376 

377 def get_node_dict(self) -> dict[_Node, None]: 

378 """Determine the explicitly added nodes for this set. 

379 

380 Thus, we can simply return all points here. 

381 

382 Returns: 

383 A dictionary containing the explicitly added nodes for this set. 

384 """ 

385 return self.nodes 

386 

387 def get_points(self) -> list[_Node]: 

388 """Determine all points (represented by nodes) for this set. 

389 

390 This function only works for point sets. 

391 

392 Returns: 

393 A list containing the points (represented by nodes) associated with this set. 

394 """ 

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

396 return list(self.get_node_dict().keys()) 

397 else: 

398 raise TypeError( 

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

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

401 ) 

402 

403 def get_all_nodes(self) -> list[_Node]: 

404 """Determine all nodes associated with this set. 

405 

406 This includes nodes contained within the geometry added to this 

407 set, e.g., nodes connected to elements in element sets. 

408 

409 Returns: 

410 A list containing all associated nodes. 

411 """ 

412 return list(self.get_node_dict().keys()) 

413 

414 def copy(self) -> "GeometrySetNodes": 

415 """Create a shallow copy of this object, the reference to the nodes 

416 will be the same, but the containers storing them will be copied. 

417 

418 Returns: 

419 A shallow copy of the geometry set. 

420 """ 

421 return GeometrySetNodes( 

422 geometry_type=self.geometry_type, 

423 nodes=list(self.nodes.keys()), 

424 ) 

425 

426 

427class GeometryName(dict): 

428 """Group node geometry sets together. 

429 

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

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

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

433 meaningful names. 

434 """ 

435 

436 def __setitem__(self, key, value): 

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

438 

439 if not isinstance(key, str): 

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

441 if isinstance(value, GeometrySetBase): 

442 super().__setitem__(key, value) 

443 else: 

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

445 

446 

447class GeometrySetContainer(_ContainerBase): 

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

449 type.""" 

450 

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

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

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

454 

455 self.item_types = [GeometrySetBase] 

456 

457 for geometry_key in _mpy.geo: 

458 self[geometry_key] = [] 

459 

460 def copy(self): 

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

462 be copied also.""" 

463 

464 # Create a new geometry set container. 

465 copy = GeometrySetContainer() 

466 

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

468 for geometry_key in _mpy.geo: 

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

470 

471 return copy