Coverage for src/beamme/core/geometry_set.py: 89%
148 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-30 18:48 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-30 18:48 +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 mpy as _mpy
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 # Node set names for the input file file.
43 geometry_set_names = {
44 _mpy.geo.point: "DNODE",
45 _mpy.geo.line: "DLINE",
46 _mpy.geo.surface: "DSURFACE",
47 _mpy.geo.volume: "DVOL",
48 }
50 def __init__(
51 self, geometry_type: _conf.Geometry, name: str | None = None, **kwargs
52 ):
53 """Initialize the geometry set.
55 Args:
56 geometry_type: Type of geometry. Only geometry sets of a single specified geometry type are supported.
57 name: Optional name to identify this geometry set.
58 """
59 super().__init__(**kwargs)
61 self.geometry_type = geometry_type
62 self.name = name
64 def link_to_nodes(
65 self, *, link_to_nodes: str = "explicitly_contained_nodes"
66 ) -> None:
67 """Set a link to this object in the all contained nodes of this
68 geometry set.
70 Args:
71 link_to_nodes:
72 "explicitly_contained_nodes":
73 A link will be set for all nodes that are explicitly part of the geometry set
74 "all_nodes":
75 A link will be set for all nodes that are part of the geometry set, i.e., also
76 nodes connected to elements of an element set. This is mainly used for vtk
77 output so we can color the nodes which are part of element sets.
78 """
79 node_list: list[_Node] | _KeysView[_Node]
80 if link_to_nodes == "explicitly_contained_nodes":
81 node_list = self.get_node_dict().keys()
82 elif link_to_nodes == "all_nodes":
83 node_list = self.get_all_nodes()
84 else:
85 raise ValueError(f'Got unexpected value link nodes="{link_to_nodes}"')
86 for node in node_list:
87 node.node_sets_link.append(self)
89 def check_replaced_nodes(self) -> None:
90 """Check if nodes in this set have to be replaced.
92 We need to do this for explicitly contained nodes in this set.
93 """
94 # Don't iterate directly over the keys as the dict changes during this iteration
95 for node in list(self.get_node_dict().keys()):
96 if node.master_node is not None:
97 self.replace_node(node, node.get_master_node())
99 def replace_node(self, old_node: _Node, new_node: _Node) -> None:
100 """Replace an existing node in this geometry set with a new one.
102 Args:
103 old_node: Node to be replaced.
104 new_node: Node that will be placed instead of old_node.
105 """
107 explicit_nodes_in_this_set = self.get_node_dict()
108 explicit_nodes_in_this_set[new_node] = None
109 del explicit_nodes_in_this_set[old_node]
111 def get_node_dict(self) -> dict[_Node, None]:
112 """Determine the explicitly added nodes for this set, i.e., nodes
113 contained in elements are not returned.
115 Returns:
116 A dictionary containing the explicitly added nodes for this set.
117 """
118 raise NotImplementedError(
119 'The "get_node_dict" method has to be overwritten in the derived class'
120 )
122 def get_points(self) -> list[_Node]:
123 """Determine all points (represented by nodes) for this set.
125 This function only works for point sets.
127 Returns:
128 A list containing the points (represented by nodes) associated with this set.
129 """
130 raise NotImplementedError(
131 'The "get_points" method has to be overwritten in the derived class'
132 )
134 def get_all_nodes(self) -> list[_Node]:
135 """Determine all nodes associated with this set.
137 This includes nodes contained within the geometry added to this
138 set, e.g., nodes connected to elements in element sets.
140 Returns:
141 A list containing all associated nodes.
142 """
143 raise NotImplementedError(
144 'The "get_all_nodes" method has to be overwritten in the derived class'
145 )
147 def dump_to_list(self):
148 """Return a list with the legacy strings of this geometry set."""
150 # Sort nodes based on their global index
151 nodes = sorted(self.get_all_nodes(), key=lambda n: n.i_global)
153 if not nodes:
154 raise ValueError("Writing empty geometry sets is not supported")
156 return [
157 {
158 "type": "NODE",
159 "node_id": node.i_global,
160 "d_type": self.geometry_set_names[self.geometry_type],
161 "d_id": self.i_global,
162 }
163 for node in nodes
164 ]
166 def __add__(self, other):
167 """Create a new geometry set with the combined geometries from this set
168 and the other set.
170 Args:
171 other: Geometry set to be added to this one. This has to be of the same geometry type as this set.
172 Returns:
173 A combined geometry set.
174 """
175 combined_set = self.copy()
176 combined_set.add(other)
177 return combined_set
180class GeometrySet(GeometrySetBase):
181 """Geometry set which is defined by geometric entries."""
183 def __init__(
184 self,
185 geometry: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet",
186 **kwargs,
187 ):
188 """Initialize the geometry set.
190 Args:
191 geometry: Geometry entries to be contained in this set.
192 """
194 # This is ok, we check every single type in the add method
195 if isinstance(geometry, list):
196 geometry_type = self._get_geometry_type(geometry[0])
197 else:
198 geometry_type = self._get_geometry_type(geometry)
200 super().__init__(geometry_type, **kwargs)
202 self.geometry_objects: dict[_conf.Geometry, dict[_Node | _Element, None]] = {}
203 for geo in _mpy.geo:
204 self.geometry_objects[geo] = {}
205 self.add(geometry)
207 @staticmethod
208 def _get_geometry_type(
209 item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet",
210 ) -> _conf.Geometry:
211 """Get the geometry type of a given item.
213 Returns:
214 Geometry type of the geometry set.
215 """
217 if isinstance(item, _Node):
218 return _mpy.geo.point
219 elif isinstance(item, _Beam):
220 return _mpy.geo.line
221 elif isinstance(item, GeometrySet):
222 return item.geometry_type
223 raise TypeError(f"Got unexpected type {type(item)}")
225 def add(
226 self, item: _Node | _Element | _Sequence[_Node | _Element] | "GeometrySet"
227 ) -> None:
228 """Add geometry item(s) to this object."""
230 if isinstance(item, list):
231 for sub_item in item:
232 self.add(sub_item)
233 elif isinstance(item, GeometrySet):
234 if item.geometry_type is self.geometry_type:
235 for geometry in item.geometry_objects[self.geometry_type]:
236 self.add(geometry)
237 else:
238 raise TypeError(
239 "You tried to add a {item.geometry_type} set to a {self.geometry_type} set. "
240 "This is not possible"
241 )
242 elif self._get_geometry_type(item) is self.geometry_type:
243 self.geometry_objects[self.geometry_type][_cast(_Node | _Element, item)] = (
244 None
245 )
246 else:
247 raise TypeError(f"Got unexpected geometry type {type(item)}")
249 def get_node_dict(self) -> dict[_Node, None]:
250 """Determine the explicitly added nodes for this set, i.e., nodes
251 contained in elements for element sets are not returned.
253 Thus, for non-point sets an empty dict is returned.
255 Returns:
256 A dictionary containing the explicitly added nodes for this set.
257 """
258 if self.geometry_type is _mpy.geo.point:
259 return _cast(dict[_Node, None], self.geometry_objects[_mpy.geo.point])
260 else:
261 return {}
263 def get_points(self) -> list[_Node]:
264 """Determine all points (represented by nodes) for this set.
266 This function only works for point sets.
268 Returns:
269 A list containing the points (represented by nodes) associated with this set.
270 """
271 if self.geometry_type is _mpy.geo.point:
272 return list(self.get_node_dict().keys())
273 else:
274 raise TypeError(
275 "The function get_points can only be called for point sets."
276 f" The present type is {self.geometry_type}"
277 )
279 def get_all_nodes(self) -> list[_Node]:
280 """Determine all nodes associated with this set.
282 This includes nodes contained within the geometry added to this
283 set, e.g., nodes connected to elements in element sets.
285 Returns:
286 A list containing all associated nodes.
287 """
289 if self.geometry_type is _mpy.geo.point:
290 return list(
291 _cast(_KeysView[_Node], self.geometry_objects[_mpy.geo.point].keys())
292 )
293 elif self.geometry_type is _mpy.geo.line:
294 nodes = []
295 for element in _cast(
296 _KeysView[_Element], self.geometry_objects[_mpy.geo.line].keys()
297 ):
298 nodes.extend(element.nodes)
299 # Remove duplicates while preserving order
300 return list(dict.fromkeys(nodes))
301 else:
302 raise TypeError(
303 "Currently GeometrySet is only implemented for points and lines"
304 )
306 def get_geometry_objects(self) -> _Sequence[_Node | _Element]:
307 """Get a list of the objects with the specified geometry type.
309 Returns:
310 A list with the contained geometry.
311 """
312 return list(self.geometry_objects[self.geometry_type].keys())
314 def copy(self) -> "GeometrySet":
315 """Create a shallow copy of this object, the reference to the nodes
316 will be the same, but the containers storing them will be copied.
318 Returns:
319 A shallow copy of the geometry set.
320 """
321 return GeometrySet(list(self.geometry_objects[self.geometry_type].keys()))
324class GeometrySetNodes(GeometrySetBase):
325 """Geometry set which is defined by nodes and not explicit geometry."""
327 def __init__(
328 self,
329 geometry_type: _conf.Geometry,
330 nodes: _Union[_Node, list[_Node], "GeometrySetNodes", None] = None,
331 **kwargs,
332 ):
333 """Initialize the geometry set.
335 Args:
336 geometry_type: Type of geometry. This is necessary, as the boundary conditions
337 and input file depend on that type.
338 nodes: Node(s) or list of nodes to be added to this geometry set.
339 """
341 if geometry_type not in _mpy.geo:
342 raise TypeError(f"Expected geometry enum, got {geometry_type}")
344 super().__init__(geometry_type, **kwargs)
345 self.nodes: dict[_Node, None] = {}
346 if nodes is not None:
347 self.add(nodes)
349 def add(self, value: _Union[_Node, list[_Node], "GeometrySetNodes"]) -> None:
350 """Add nodes to this object.
352 Args:
353 nodes: Node(s) or list of nodes to be added to this geometry set.
354 """
356 if isinstance(value, list):
357 # Loop over items and check if they are either Nodes or integers.
358 # This improves the performance considerably when large list of
359 # Nodes are added.
360 for item in value:
361 self.add(item)
362 elif isinstance(value, (int, _Node)):
363 self.nodes[value] = None
364 elif isinstance(value, GeometrySetNodes):
365 # Add all nodes from this geometry set.
366 if self.geometry_type == value.geometry_type:
367 for node in value.nodes:
368 self.add(node)
369 else:
370 raise TypeError(
371 f"You tried to add a {value.geometry_type} set to a {self.geometry_type} set. "
372 "This is not possible"
373 )
374 else:
375 raise TypeError(f"Expected Node or list, but got {type(value)}")
377 def get_node_dict(self) -> dict[_Node, None]:
378 """Determine the explicitly added nodes for this set.
380 Thus, we can simply return all points here.
382 Returns:
383 A dictionary containing the explicitly added nodes for this set.
384 """
385 return self.nodes
387 def get_points(self) -> list[_Node]:
388 """Determine all points (represented by nodes) for this set.
390 This function only works for point sets.
392 Returns:
393 A list containing the points (represented by nodes) associated with this set.
394 """
395 if self.geometry_type is _mpy.geo.point:
396 return list(self.get_node_dict().keys())
397 else:
398 raise TypeError(
399 "The function get_points can only be called for point sets."
400 f" The present type is {self.geometry_type}"
401 )
403 def get_all_nodes(self) -> list[_Node]:
404 """Determine all nodes associated with this set.
406 This includes nodes contained within the geometry added to this
407 set, e.g., nodes connected to elements in element sets.
409 Returns:
410 A list containing all associated nodes.
411 """
412 return list(self.get_node_dict().keys())
414 def copy(self) -> "GeometrySetNodes":
415 """Create a shallow copy of this object, the reference to the nodes
416 will be the same, but the containers storing them will be copied.
418 Returns:
419 A shallow copy of the geometry set.
420 """
421 return GeometrySetNodes(
422 geometry_type=self.geometry_type,
423 nodes=list(self.nodes.keys()),
424 )
427class GeometryName(dict):
428 """Group node geometry sets together.
430 This is mainly used for export from mesh functions. The sets can be
431 accessed by a unique name. There is no distinction between different
432 types of geometry, every name can only be used once -> use
433 meaningful names.
434 """
436 def __setitem__(self, key, value):
437 """Set a geometry set in this container."""
439 if not isinstance(key, str):
440 raise TypeError(f"Expected string, got {type(key)}!")
441 if isinstance(value, GeometrySetBase):
442 super().__setitem__(key, value)
443 else:
444 raise NotImplementedError("GeometryName can only store GeometrySets")
447class GeometrySetContainer(_ContainerBase):
448 """A class to group geometry sets together with the key being the geometry
449 type."""
451 def __init__(self, *args, **kwargs):
452 """Initialize the container and create the default keys in the map."""
453 super().__init__(*args, **kwargs)
455 self.item_types = [GeometrySetBase]
457 for geometry_key in _mpy.geo:
458 self[geometry_key] = []
460 def copy(self):
461 """When creating a copy of this object, all lists in this object will
462 be copied also."""
464 # Create a new geometry set container.
465 copy = GeometrySetContainer()
467 # Add a copy of every list from this container to the new one.
468 for geometry_key in _mpy.geo:
469 copy[geometry_key] = self[geometry_key].copy()
471 return copy