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
« 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."""
25from typing import KeysView as _KeysView
26from typing import Sequence as _Sequence
27from typing import cast as _cast
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
38class GeometrySetBase(_BaseMeshItem):
39 """Base class for a geometry set."""
41 def __init__(
42 self, geometry_type: _conf.Geometry, name: str | None = None, **kwargs
43 ):
44 """Initialize the geometry set.
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)
52 self.geometry_type = geometry_type
53 self.name = name
55 def check_replaced_nodes(self) -> None:
56 """Check if nodes in this set have to be replaced.
58 We need to do this for explicitly contained nodes in this set.
59 """
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)
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.
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 )
80 def get_points(self) -> list[_Node]:
81 """Determine all points (represented by nodes) for this set.
83 This function only works for point sets.
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 )
92 def get_all_nodes(self) -> list[_Node]:
93 """Determine all nodes associated with this set.
95 This includes nodes contained within the geometry added to this
96 set, e.g., nodes connected to elements in element sets.
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 )
105 def __add__(self, other):
106 """Create a new geometry set with the combined geometries from this set
107 and the other set.
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
119class GeometrySet(GeometrySetBase):
120 """Geometry set which is defined by geometric entries."""
122 def __init__(
123 self,
124 geometry: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet",
125 **kwargs,
126 ):
127 """Initialize the geometry set.
129 Args:
130 geometry: Geometry entries to be contained in this set.
131 """
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)
139 super().__init__(geometry_type, **kwargs)
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)
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.
152 Returns:
153 Geometry type of the geometry set.
154 """
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)}")
164 def add(
165 self, item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet"
166 ) -> None:
167 """Add geometry item(s) to this object."""
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)}")
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.
192 Thus, for non-point sets an empty dict is returned.
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 {}
202 def get_points(self) -> list[_Node]:
203 """Determine all points (represented by nodes) for this set.
205 This function only works for point sets.
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 )
218 def get_all_nodes(self) -> list[_Node]:
219 """Determine all nodes associated with this set.
221 This includes nodes contained within the geometry added to this
222 set, e.g., nodes connected to elements in element sets.
224 Returns:
225 A list containing all associated nodes.
226 """
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 )
245 def get_geometry_objects(self) -> _Sequence[_Node | _Element]:
246 """Get a list of the objects with the specified geometry type.
248 Returns:
249 A list with the contained geometry.
250 """
251 return list(self.geometry_objects[self.geometry_type].keys())
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.
257 Returns:
258 A shallow copy of the geometry set.
259 """
260 return GeometrySet(list(self.geometry_objects[self.geometry_type].keys()))
263class GeometrySetNodes(GeometrySetBase):
264 """Geometry set which is defined by nodes and not explicit geometry."""
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.
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 """
280 if geometry_type not in _bme.geo:
281 raise TypeError(f"Expected geometry enum, got {geometry_type}")
283 super().__init__(geometry_type, **kwargs)
284 self.nodes: dict[_Node, None] = {}
285 if nodes is not None:
286 self.add(nodes)
288 def add(self, value: "_Node | list[_Node] | GeometrySetNodes") -> None:
289 """Add nodes to this object.
291 Args:
292 nodes: Node(s) or list of nodes to be added to this geometry set.
293 """
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)}")
316 def get_node_dict(self) -> dict[_Node, None]:
317 """Determine the explicitly added nodes for this set.
319 Thus, we can simply return all points here.
321 Returns:
322 A dictionary containing the explicitly added nodes for this set.
323 """
324 return self.nodes
326 def get_points(self) -> list[_Node]:
327 """Determine all points (represented by nodes) for this set.
329 This function only works for point sets.
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 )
342 def get_all_nodes(self) -> list[_Node]:
343 """Determine all nodes associated with this set.
345 This includes nodes contained within the geometry added to this
346 set, e.g., nodes connected to elements in element sets.
348 Returns:
349 A list containing all associated nodes.
350 """
351 return list(self.get_node_dict().keys())
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.
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 )
366class GeometryName(dict):
367 """Group node geometry sets together.
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 """
375 def __setitem__(self, key, value):
376 """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")
386class GeometrySetContainer(_ContainerBase):
387 """A class to group geometry sets together with the key being the geometry
388 type."""
390 def __init__(self, *args, **kwargs):
391 """Initialize the container and create the default keys in the map."""
392 super().__init__(*args, **kwargs)
394 self.item_types = [GeometrySetBase]
396 for geometry_key in _bme.geo:
397 self[geometry_key] = []
399 def copy(self):
400 """When creating a copy of this object, all lists in this object will
401 be copied also."""
403 # Create a new geometry set container.
404 copy = GeometrySetContainer()
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()
410 return copy