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

142 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-29 11:30 +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 bme as _bme 

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 def __init__( 

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

44 ): 

45 """Initialize the geometry set. 

46 

47 Args: 

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

49 name: Optional name to identify this geometry set. 

50 """ 

51 super().__init__(**kwargs) 

52 

53 self.geometry_type = geometry_type 

54 self.name = name 

55 

56 def link_to_nodes( 

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

58 ) -> None: 

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

60 geometry set. 

61 

62 Args: 

63 link_to_nodes: 

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 node_list: list[_Node] | _KeysView[_Node] 

72 if link_to_nodes == "explicitly_contained_nodes": 

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

74 elif link_to_nodes == "all_nodes": 

75 node_list = self.get_all_nodes() 

76 else: 

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

78 for node in node_list: 

79 node.node_sets_link.append(self) 

80 

81 def check_replaced_nodes(self) -> None: 

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

83 

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

85 """ 

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

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

88 if node.master_node is not None: 

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

90 

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

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

93 

94 Args: 

95 old_node: Node to be replaced. 

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

97 """ 

98 

99 explicit_nodes_in_this_set = self.get_node_dict() 

100 explicit_nodes_in_this_set[new_node] = None 

101 del explicit_nodes_in_this_set[old_node] 

102 

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

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

105 contained in elements are not returned. 

106 

107 Returns: 

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

109 """ 

110 raise NotImplementedError( 

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

112 ) 

113 

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

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

116 

117 This function only works for point sets. 

118 

119 Returns: 

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

121 """ 

122 raise NotImplementedError( 

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

124 ) 

125 

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

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

128 

129 This includes nodes contained within the geometry added to this 

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

131 

132 Returns: 

133 A list containing all associated nodes. 

134 """ 

135 raise NotImplementedError( 

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

137 ) 

138 

139 def __add__(self, other): 

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

141 and the other set. 

142 

143 Args: 

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

145 Returns: 

146 A combined geometry set. 

147 """ 

148 combined_set = self.copy() 

149 combined_set.add(other) 

150 return combined_set 

151 

152 

153class GeometrySet(GeometrySetBase): 

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

155 

156 def __init__( 

157 self, 

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

159 **kwargs, 

160 ): 

161 """Initialize the geometry set. 

162 

163 Args: 

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

165 """ 

166 

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

168 if isinstance(geometry, list): 

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

170 else: 

171 geometry_type = self._get_geometry_type(geometry) 

172 

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

174 

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

176 for geo in _bme.geo: 

177 self.geometry_objects[geo] = {} 

178 self.add(geometry) 

179 

180 @staticmethod 

181 def _get_geometry_type( 

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

183 ) -> _conf.Geometry: 

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

185 

186 Returns: 

187 Geometry type of the geometry set. 

188 """ 

189 

190 if isinstance(item, _Node): 

191 return _bme.geo.point 

192 elif isinstance(item, _Beam): 

193 return _bme.geo.line 

194 elif isinstance(item, GeometrySet): 

195 return item.geometry_type 

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

197 

198 def add( 

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

200 ) -> None: 

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

202 

203 if isinstance(item, list): 

204 for sub_item in item: 

205 self.add(sub_item) 

206 elif isinstance(item, GeometrySet): 

207 if item.geometry_type is self.geometry_type: 

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

209 self.add(geometry) 

210 else: 

211 raise TypeError( 

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

213 "This is not possible" 

214 ) 

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

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

217 None 

218 ) 

219 else: 

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

221 

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

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

224 contained in elements for element sets are not returned. 

225 

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

227 

228 Returns: 

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

230 """ 

231 if self.geometry_type is _bme.geo.point: 

232 return _cast(dict[_Node, None], self.geometry_objects[_bme.geo.point]) 

233 else: 

234 return {} 

235 

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

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

238 

239 This function only works for point sets. 

240 

241 Returns: 

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

243 """ 

244 if self.geometry_type is _bme.geo.point: 

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

246 else: 

247 raise TypeError( 

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

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

250 ) 

251 

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

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

254 

255 This includes nodes contained within the geometry added to this 

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

257 

258 Returns: 

259 A list containing all associated nodes. 

260 """ 

261 

262 if self.geometry_type is _bme.geo.point: 

263 return list( 

264 _cast(_KeysView[_Node], self.geometry_objects[_bme.geo.point].keys()) 

265 ) 

266 elif self.geometry_type is _bme.geo.line: 

267 nodes = [] 

268 for element in _cast( 

269 _KeysView[_Element], self.geometry_objects[_bme.geo.line].keys() 

270 ): 

271 nodes.extend(element.nodes) 

272 # Remove duplicates while preserving order 

273 return list(dict.fromkeys(nodes)) 

274 else: 

275 raise TypeError( 

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

277 ) 

278 

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

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

281 

282 Returns: 

283 A list with the contained geometry. 

284 """ 

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

286 

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

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

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

290 

291 Returns: 

292 A shallow copy of the geometry set. 

293 """ 

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

295 

296 

297class GeometrySetNodes(GeometrySetBase): 

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

299 

300 def __init__( 

301 self, 

302 geometry_type: _conf.Geometry, 

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

304 **kwargs, 

305 ): 

306 """Initialize the geometry set. 

307 

308 Args: 

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

310 and input file depend on that type. 

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

312 """ 

313 

314 if geometry_type not in _bme.geo: 

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

316 

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

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

319 if nodes is not None: 

320 self.add(nodes) 

321 

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

323 """Add nodes to this object. 

324 

325 Args: 

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

327 """ 

328 

329 if isinstance(value, list): 

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

331 # This improves the performance considerably when large list of 

332 # Nodes are added. 

333 for item in value: 

334 self.add(item) 

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

336 self.nodes[value] = None 

337 elif isinstance(value, GeometrySetNodes): 

338 # Add all nodes from this geometry set. 

339 if self.geometry_type == value.geometry_type: 

340 for node in value.nodes: 

341 self.add(node) 

342 else: 

343 raise TypeError( 

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

345 "This is not possible" 

346 ) 

347 else: 

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

349 

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

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

352 

353 Thus, we can simply return all points here. 

354 

355 Returns: 

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

357 """ 

358 return self.nodes 

359 

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

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

362 

363 This function only works for point sets. 

364 

365 Returns: 

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

367 """ 

368 if self.geometry_type is _bme.geo.point: 

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

370 else: 

371 raise TypeError( 

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

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

374 ) 

375 

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

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

378 

379 This includes nodes contained within the geometry added to this 

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

381 

382 Returns: 

383 A list containing all associated nodes. 

384 """ 

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

386 

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

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

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

390 

391 Returns: 

392 A shallow copy of the geometry set. 

393 """ 

394 return GeometrySetNodes( 

395 geometry_type=self.geometry_type, 

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

397 ) 

398 

399 

400class GeometryName(dict): 

401 """Group node geometry sets together. 

402 

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

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

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

406 meaningful names. 

407 """ 

408 

409 def __setitem__(self, key, value): 

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

411 

412 if not isinstance(key, str): 

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

414 if isinstance(value, GeometrySetBase): 

415 super().__setitem__(key, value) 

416 else: 

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

418 

419 

420class GeometrySetContainer(_ContainerBase): 

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

422 type.""" 

423 

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

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

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

427 

428 self.item_types = [GeometrySetBase] 

429 

430 for geometry_key in _bme.geo: 

431 self[geometry_key] = [] 

432 

433 def copy(self): 

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

435 be copied also.""" 

436 

437 # Create a new geometry set container. 

438 copy = GeometrySetContainer() 

439 

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

441 for geometry_key in _bme.geo: 

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

443 

444 return copy