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
« 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."""
25from typing import KeysView as _KeysView
26from typing import Sequence as _Sequence
27from typing import Union as _Union
28from typing import cast as _cast
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
39class GeometrySetBase(_BaseMeshItem):
40 """Base class for a geometry set."""
42 def __init__(
43 self, geometry_type: _conf.Geometry, name: str | None = None, **kwargs
44 ):
45 """Initialize the geometry set.
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)
53 self.geometry_type = geometry_type
54 self.name = name
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.
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)
81 def check_replaced_nodes(self) -> None:
82 """Check if nodes in this set have to be replaced.
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())
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.
94 Args:
95 old_node: Node to be replaced.
96 new_node: Node that will be placed instead of old_node.
97 """
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]
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.
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 )
114 def get_points(self) -> list[_Node]:
115 """Determine all points (represented by nodes) for this set.
117 This function only works for point sets.
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 )
126 def get_all_nodes(self) -> list[_Node]:
127 """Determine all nodes associated with this set.
129 This includes nodes contained within the geometry added to this
130 set, e.g., nodes connected to elements in element sets.
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 )
139 def __add__(self, other):
140 """Create a new geometry set with the combined geometries from this set
141 and the other set.
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
153class GeometrySet(GeometrySetBase):
154 """Geometry set which is defined by geometric entries."""
156 def __init__(
157 self,
158 geometry: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet",
159 **kwargs,
160 ):
161 """Initialize the geometry set.
163 Args:
164 geometry: Geometry entries to be contained in this set.
165 """
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)
173 super().__init__(geometry_type, **kwargs)
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)
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.
186 Returns:
187 Geometry type of the geometry set.
188 """
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)}")
198 def add(
199 self, item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet"
200 ) -> None:
201 """Add geometry item(s) to this object."""
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)}")
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.
226 Thus, for non-point sets an empty dict is returned.
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 {}
236 def get_points(self) -> list[_Node]:
237 """Determine all points (represented by nodes) for this set.
239 This function only works for point sets.
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 )
252 def get_all_nodes(self) -> list[_Node]:
253 """Determine all nodes associated with this set.
255 This includes nodes contained within the geometry added to this
256 set, e.g., nodes connected to elements in element sets.
258 Returns:
259 A list containing all associated nodes.
260 """
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 )
279 def get_geometry_objects(self) -> _Sequence[_Node | _Element]:
280 """Get a list of the objects with the specified geometry type.
282 Returns:
283 A list with the contained geometry.
284 """
285 return list(self.geometry_objects[self.geometry_type].keys())
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.
291 Returns:
292 A shallow copy of the geometry set.
293 """
294 return GeometrySet(list(self.geometry_objects[self.geometry_type].keys()))
297class GeometrySetNodes(GeometrySetBase):
298 """Geometry set which is defined by nodes and not explicit geometry."""
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.
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 """
314 if geometry_type not in _bme.geo:
315 raise TypeError(f"Expected geometry enum, got {geometry_type}")
317 super().__init__(geometry_type, **kwargs)
318 self.nodes: dict[_Node, None] = {}
319 if nodes is not None:
320 self.add(nodes)
322 def add(self, value: _Union[_Node, list[_Node], "GeometrySetNodes"]) -> None:
323 """Add nodes to this object.
325 Args:
326 nodes: Node(s) or list of nodes to be added to this geometry set.
327 """
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)}")
350 def get_node_dict(self) -> dict[_Node, None]:
351 """Determine the explicitly added nodes for this set.
353 Thus, we can simply return all points here.
355 Returns:
356 A dictionary containing the explicitly added nodes for this set.
357 """
358 return self.nodes
360 def get_points(self) -> list[_Node]:
361 """Determine all points (represented by nodes) for this set.
363 This function only works for point sets.
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 )
376 def get_all_nodes(self) -> list[_Node]:
377 """Determine all nodes associated with this set.
379 This includes nodes contained within the geometry added to this
380 set, e.g., nodes connected to elements in element sets.
382 Returns:
383 A list containing all associated nodes.
384 """
385 return list(self.get_node_dict().keys())
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.
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 )
400class GeometryName(dict):
401 """Group node geometry sets together.
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 """
409 def __setitem__(self, key, value):
410 """Set a geometry set in this container."""
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")
420class GeometrySetContainer(_ContainerBase):
421 """A class to group geometry sets together with the key being the geometry
422 type."""
424 def __init__(self, *args, **kwargs):
425 """Initialize the container and create the default keys in the map."""
426 super().__init__(*args, **kwargs)
428 self.item_types = [GeometrySetBase]
430 for geometry_key in _bme.geo:
431 self[geometry_key] = []
433 def copy(self):
434 """When creating a copy of this object, all lists in this object will
435 be copied also."""
437 # Create a new geometry set container.
438 copy = GeometrySetContainer()
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()
444 return copy