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
« 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."""
24from __future__ import annotations as _annotations
26from collections.abc import KeysView as _KeysView
27from collections.abc import Sequence as _Sequence
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 check_replaced_nodes(self) -> None:
57 """Check if nodes in this set have to be replaced.
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)
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.
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 and the
107 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 """
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)
138 super().__init__(geometry_type, **kwargs)
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)
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.
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)}")
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)}")
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.
194 Thus, for non-point sets an empty dict is returned.
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 {}
204 def get_points(self) -> list[_Node]:
205 """Determine all points (represented by nodes) for this set.
207 This function only works for point sets.
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 )
220 def get_all_nodes(self) -> list[_Node]:
221 """Determine all nodes associated with this set.
223 This includes nodes contained within the geometry added to this
224 set, e.g., nodes connected to elements in element sets.
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 )
249 def get_geometry_objects(self) -> _Sequence[_Node | _Element]:
250 """Get a list of the objects with the specified geometry type.
252 Returns:
253 A list with the contained geometry.
254 """
255 return list(self.geometry_objects[self.geometry_type].keys())
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.
261 Returns:
262 A shallow copy of the geometry set.
263 """
264 return GeometrySet(list(self.geometry_objects[self.geometry_type].keys()))
267class GeometrySetNodes(GeometrySetBase):
268 """Geometry set which is defined by nodes and not explicit geometry."""
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.
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}")
286 super().__init__(geometry_type, **kwargs)
287 self.nodes: dict[_Node, None] = {}
288 if nodes is not None:
289 self.add(nodes)
291 def add(self, value: "_Node | list[_Node] | GeometrySetNodes") -> None:
292 """Add nodes to this object.
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)}")
318 def get_node_dict(self) -> dict[_Node, None]:
319 """Determine the explicitly added nodes for this set.
321 Thus, we can simply return all points here.
323 Returns:
324 A dictionary containing the explicitly added nodes for this set.
325 """
326 return self.nodes
328 def get_points(self) -> list[_Node]:
329 """Determine all points (represented by nodes) for this set.
331 This function only works for point sets.
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 )
344 def get_all_nodes(self) -> list[_Node]:
345 """Determine all nodes associated with this set.
347 This includes nodes contained within the geometry added to this
348 set, e.g., nodes connected to elements in element sets.
350 Returns:
351 A list containing all associated nodes.
352 """
353 return list(self.get_node_dict().keys())
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.
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 )
368class GeometryName(dict):
369 """Group node geometry sets together.
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 """
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")
386class GeometrySetContainer(_ContainerBase):
387 """A class to group geometry sets together with the key being the geometry type."""
389 def __init__(self, *args, **kwargs):
390 """Initialize the container and create the default keys in the map."""
391 super().__init__(*args, **kwargs)
393 self.item_types = [GeometrySetBase]
395 for geometry_key in _bme.geo:
396 self[geometry_key] = []
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()
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()
408 return copy