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

130 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-08 11:03 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2026 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 cast as _cast 

28 

29import beamme.core.conf as _conf 

30from beamme.core.base_mesh_item import BaseMeshItem as _BaseMeshItem 

31from beamme.core.conf import bme as _bme 

32from beamme.core.container import ContainerBase as _ContainerBase 

33from beamme.core.element import Element as _Element 

34from beamme.core.element_beam import Beam as _Beam 

35from beamme.core.node import Node as _Node 

36 

37 

38class GeometrySetBase(_BaseMeshItem): 

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

40 

41 def __init__( 

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

43 ): 

44 """Initialize the geometry set. 

45 

46 Args: 

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

48 name: Optional name to identify this geometry set. 

49 """ 

50 super().__init__(**kwargs) 

51 

52 self.geometry_type = geometry_type 

53 self.name = name 

54 

55 def check_replaced_nodes(self) -> None: 

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

57 

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

59 """ 

60 

61 explicit_nodes_in_this_set = self.get_node_dict() 

62 nodes_replaced = { 

63 current_node.get_target_node(): None 

64 for current_node in explicit_nodes_in_this_set.keys() 

65 } 

66 explicit_nodes_in_this_set.clear() 

67 explicit_nodes_in_this_set.update(nodes_replaced) 

68 

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

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

71 contained in elements are not returned. 

72 

73 Returns: 

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

75 """ 

76 raise NotImplementedError( 

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

78 ) 

79 

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

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

82 

83 This function only works for point sets. 

84 

85 Returns: 

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

87 """ 

88 raise NotImplementedError( 

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

90 ) 

91 

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

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

94 

95 This includes nodes contained within the geometry added to this 

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

97 

98 Returns: 

99 A list containing all associated nodes. 

100 """ 

101 raise NotImplementedError( 

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

103 ) 

104 

105 def __add__(self, other): 

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

107 and the other set. 

108 

109 Args: 

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

111 Returns: 

112 A combined geometry set. 

113 """ 

114 combined_set = self.copy() 

115 combined_set.add(other) 

116 return combined_set 

117 

118 

119class GeometrySet(GeometrySetBase): 

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

121 

122 def __init__( 

123 self, 

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

125 **kwargs, 

126 ): 

127 """Initialize the geometry set. 

128 

129 Args: 

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

131 """ 

132 

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

134 if isinstance(geometry, list): 

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

136 else: 

137 geometry_type = self._get_geometry_type(geometry) 

138 

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

140 

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

142 for geo in _bme.geo: 

143 self.geometry_objects[geo] = {} 

144 self.add(geometry) 

145 

146 @staticmethod 

147 def _get_geometry_type( 

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

149 ) -> _conf.Geometry: 

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

151 

152 Returns: 

153 Geometry type of the geometry set. 

154 """ 

155 

156 if isinstance(item, _Node): 

157 return _bme.geo.point 

158 elif isinstance(item, _Beam): 

159 return _bme.geo.line 

160 elif isinstance(item, GeometrySet): 

161 return item.geometry_type 

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

163 

164 def add( 

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

166 ) -> None: 

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

168 

169 if isinstance(item, list): 

170 for sub_item in item: 

171 self.add(sub_item) 

172 elif isinstance(item, GeometrySet): 

173 if item.geometry_type is self.geometry_type: 

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

175 self.add(geometry) 

176 else: 

177 raise TypeError( 

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

179 "This is not possible" 

180 ) 

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

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

183 None 

184 ) 

185 else: 

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

187 

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

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

190 contained in elements for element sets are not returned. 

191 

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

193 

194 Returns: 

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

196 """ 

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

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

199 else: 

200 return {} 

201 

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

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

204 

205 This function only works for point sets. 

206 

207 Returns: 

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

209 """ 

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

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

212 else: 

213 raise TypeError( 

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

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

216 ) 

217 

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

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

220 

221 This includes nodes contained within the geometry added to this 

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

223 

224 Returns: 

225 A list containing all associated nodes. 

226 """ 

227 

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

229 return list( 

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

231 ) 

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

233 nodes = [] 

234 for element in _cast( 

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

236 ): 

237 nodes.extend(element.nodes) 

238 # Remove duplicates while preserving order 

239 return list(dict.fromkeys(nodes)) 

240 else: 

241 raise TypeError( 

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

243 ) 

244 

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

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

247 

248 Returns: 

249 A list with the contained geometry. 

250 """ 

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

252 

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

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

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

256 

257 Returns: 

258 A shallow copy of the geometry set. 

259 """ 

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

261 

262 

263class GeometrySetNodes(GeometrySetBase): 

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

265 

266 def __init__( 

267 self, 

268 geometry_type: _conf.Geometry, 

269 nodes: "_Node | list[_Node] | GeometrySetNodes | None" = None, 

270 **kwargs, 

271 ): 

272 """Initialize the geometry set. 

273 

274 Args: 

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

276 and input file depend on that type. 

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

278 """ 

279 

280 if geometry_type not in _bme.geo: 

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

282 

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

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

285 if nodes is not None: 

286 self.add(nodes) 

287 

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

289 """Add nodes to this object. 

290 

291 Args: 

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

293 """ 

294 

295 if isinstance(value, list): 

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

297 # This improves the performance considerably when large list of 

298 # Nodes are added. 

299 for item in value: 

300 self.add(item) 

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

302 self.nodes[value] = None 

303 elif isinstance(value, GeometrySetNodes): 

304 # Add all nodes from this geometry set. 

305 if self.geometry_type == value.geometry_type: 

306 for node in value.nodes: 

307 self.add(node) 

308 else: 

309 raise TypeError( 

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

311 "This is not possible" 

312 ) 

313 else: 

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

315 

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

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

318 

319 Thus, we can simply return all points here. 

320 

321 Returns: 

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

323 """ 

324 return self.nodes 

325 

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

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

328 

329 This function only works for point sets. 

330 

331 Returns: 

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

333 """ 

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

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

336 else: 

337 raise TypeError( 

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

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

340 ) 

341 

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

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

344 

345 This includes nodes contained within the geometry added to this 

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

347 

348 Returns: 

349 A list containing all associated nodes. 

350 """ 

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

352 

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

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

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

356 

357 Returns: 

358 A shallow copy of the geometry set. 

359 """ 

360 return GeometrySetNodes( 

361 geometry_type=self.geometry_type, 

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

363 ) 

364 

365 

366class GeometryName(dict): 

367 """Group node geometry sets together. 

368 

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

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

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

372 meaningful names. 

373 """ 

374 

375 def __setitem__(self, key, value): 

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

377 

378 if not isinstance(key, str): 

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

380 if isinstance(value, GeometrySetBase): 

381 super().__setitem__(key, value) 

382 else: 

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

384 

385 

386class GeometrySetContainer(_ContainerBase): 

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

388 type.""" 

389 

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

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

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

393 

394 self.item_types = [GeometrySetBase] 

395 

396 for geometry_key in _bme.geo: 

397 self[geometry_key] = [] 

398 

399 def copy(self): 

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

401 be copied also.""" 

402 

403 # Create a new geometry set container. 

404 copy = GeometrySetContainer() 

405 

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

407 for geometry_key in _bme.geo: 

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

409 

410 return copy