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

133 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 15:20 +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 file.""" 

23 

24from __future__ import annotations as _annotations 

25 

26from collections.abc import KeysView as _KeysView 

27from collections.abc import Sequence as _Sequence 

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 check_replaced_nodes(self) -> None: 

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

58 

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

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 contained in 

71 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 and the 

107 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 # This is ok, we check every single type in the add method 

133 if isinstance(geometry, list): 

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

135 else: 

136 geometry_type = self._get_geometry_type(geometry) 

137 

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

139 

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

141 for geo in _bme.geo: 

142 self.geometry_objects[geo] = {} 

143 self.add(geometry) 

144 

145 @staticmethod 

146 def _get_geometry_type( 

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

148 ) -> _conf.Geometry: 

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

150 

151 Returns: 

152 Geometry type of the geometry set. 

153 """ 

154 if isinstance(item, _Node): 

155 return _bme.geo.point 

156 elif isinstance(item, _Beam): 

157 return _bme.geo.line 

158 elif isinstance(item, GeometrySet): 

159 return item.geometry_type 

160 elif ( 

161 isinstance(item, _Element) 

162 and type(item).element_type == _bme.element_type.space_time_beam 

163 ): 

164 return _bme.geo.surface 

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

166 

167 def add( 

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

169 ) -> None: 

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

171 if isinstance(item, list): 

172 for sub_item in item: 

173 self.add(sub_item) 

174 elif isinstance(item, GeometrySet): 

175 if item.geometry_type is self.geometry_type: 

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

177 self.add(geometry) 

178 else: 

179 raise TypeError( 

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

181 "This is not possible" 

182 ) 

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

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

185 None 

186 ) 

187 else: 

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

189 

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

191 """Determine the explicitly added nodes for this set, i.e., nodes contained in 

192 elements for element sets are not returned. 

193 

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

195 

196 Returns: 

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

198 """ 

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

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

201 else: 

202 return {} 

203 

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

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

206 

207 This function only works for point sets. 

208 

209 Returns: 

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

211 """ 

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

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

214 else: 

215 raise TypeError( 

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

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

218 ) 

219 

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

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

222 

223 This includes nodes contained within the geometry added to this 

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

225 

226 Returns: 

227 A list containing all associated nodes. 

228 """ 

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

230 return list( 

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

232 ) 

233 elif ( 

234 self.geometry_type is _bme.geo.line 

235 or self.geometry_type is _bme.geo.surface 

236 ): 

237 nodes = [] 

238 for element in _cast( 

239 _KeysView[_Element], self.geometry_objects[self.geometry_type].keys() 

240 ): 

241 nodes.extend(element.nodes) 

242 # Remove duplicates while preserving order 

243 return list(dict.fromkeys(nodes)) 

244 else: 

245 raise TypeError( 

246 "Currently GeometrySet is only implemented for points, lines and surfaces" 

247 ) 

248 

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

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

251 

252 Returns: 

253 A list with the contained geometry. 

254 """ 

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

256 

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

258 """Create a shallow copy of this object, the reference to the nodes will be the 

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

260 

261 Returns: 

262 A shallow copy of the geometry set. 

263 """ 

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

265 

266 

267class GeometrySetNodes(GeometrySetBase): 

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

269 

270 def __init__( 

271 self, 

272 geometry_type: _conf.Geometry, 

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

274 **kwargs, 

275 ): 

276 """Initialize the geometry set. 

277 

278 Args: 

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

280 and input file depend on that type. 

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

282 """ 

283 if geometry_type not in _bme.geo: 

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

285 

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

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

288 if nodes is not None: 

289 self.add(nodes) 

290 

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

292 """Add nodes to this object. 

293 

294 Args: 

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

296 """ 

297 if isinstance(value, list): 

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

299 # This improves the performance considerably when large list of 

300 # Nodes are added. 

301 for item in value: 

302 self.add(item) 

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

304 self.nodes[value] = None 

305 elif isinstance(value, GeometrySetNodes): 

306 # Add all nodes from this geometry set. 

307 if self.geometry_type == value.geometry_type: 

308 for node in value.nodes: 

309 self.add(node) 

310 else: 

311 raise TypeError( 

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

313 "This is not possible" 

314 ) 

315 else: 

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

317 

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

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

320 

321 Thus, we can simply return all points here. 

322 

323 Returns: 

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

325 """ 

326 return self.nodes 

327 

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

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

330 

331 This function only works for point sets. 

332 

333 Returns: 

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

335 """ 

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

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

338 else: 

339 raise TypeError( 

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

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

342 ) 

343 

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

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

346 

347 This includes nodes contained within the geometry added to this 

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

349 

350 Returns: 

351 A list containing all associated nodes. 

352 """ 

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

354 

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

356 """Create a shallow copy of this object, the reference to the nodes will be the 

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

358 

359 Returns: 

360 A shallow copy of the geometry set. 

361 """ 

362 return GeometrySetNodes( 

363 geometry_type=self.geometry_type, 

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

365 ) 

366 

367 

368class GeometryName(dict): 

369 """Group node geometry sets together. 

370 

371 This is mainly used for export from mesh functions. The sets can be accessed by a 

372 unique name. There is no distinction between different types of geometry, every name 

373 can only be used once -> use meaningful names. 

374 """ 

375 

376 def __setitem__(self, key, value): 

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

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 type.""" 

388 

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

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

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

392 

393 self.item_types = [GeometrySetBase] 

394 

395 for geometry_key in _bme.geo: 

396 self[geometry_key] = [] 

397 

398 def copy(self): 

399 """When creating a copy of this object, all lists in this object will be copied 

400 also.""" 

401 # Create a new geometry set container. 

402 copy = GeometrySetContainer() 

403 

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

405 for geometry_key in _bme.geo: 

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

407 

408 return copy