Coverage for src/meshpy/core/geometry_set.py: 89%
132 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 04:26 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 04:26 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2018-2025 MeshPy 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 meshpy.core.base_mesh_item import BaseMeshItem as _BaseMeshItem
26from meshpy.core.conf import mpy as _mpy
27from meshpy.core.container import ContainerBase as _ContainerBase
28from meshpy.core.element_beam import Beam as _Beam
29from meshpy.core.node import Node as _Node
32class GeometrySetBase(_BaseMeshItem):
33 """Base class for a geometry set."""
35 # Node set names for the input file file.
36 geometry_set_names = {
37 _mpy.geo.point: "DNODE",
38 _mpy.geo.line: "DLINE",
39 _mpy.geo.surface: "DSURFACE",
40 _mpy.geo.volume: "DVOL",
41 }
43 def __init__(self, geometry_type, name=None, **kwargs):
44 """Initialize the geometry set.
46 Args
47 ----
48 geometry_type: mpy.geo
49 Type of geometry. MeshPy only supports geometry sets of a single
50 specified geometry type.
51 name: str
52 Optional name to identify this geometry set.
53 """
54 super().__init__(**kwargs)
56 self.geometry_type = geometry_type
57 self.name = name
59 def link_to_nodes(self, *, link_to_nodes="explicitly_contained_nodes"):
60 """Set a link to this object in the all contained nodes of this
61 geometry set.
63 link_to_nodes: str
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 if link_to_nodes == "explicitly_contained_nodes":
72 node_list = self.get_node_dict().keys()
73 elif link_to_nodes == "all_nodes":
74 node_list = self.get_all_nodes()
75 else:
76 raise ValueError(f'Got unexpected value link nodes="{link_to_nodes}"')
77 for node in node_list:
78 node.node_sets_link.append(self)
80 def check_replaced_nodes(self):
81 """Check if nodes in this set have to be replaced.
83 We need to do this for explicitly contained nodes in this set.
84 """
85 # Don't iterate directly over the keys as the dict changes during this iteration
86 for node in list(self.get_node_dict().keys()):
87 if node.master_node is not None:
88 self.replace_node(node, node.get_master_node())
90 def replace_node(self, old_node, new_node):
91 """Replace old_node with new_node."""
93 explicit_nodes_in_this_set = self.get_node_dict()
94 explicit_nodes_in_this_set[new_node] = None
95 del explicit_nodes_in_this_set[old_node]
97 def get_node_dict(self):
98 """Return the dictionary containing the explicitly added nodes for this
99 set."""
100 raise NotImplementedError(
101 'The "get_node_dict" method has to be overwritten in the derived class'
102 )
104 def get_points(self):
105 """Return nodes explicitly associated with this set."""
106 raise NotImplementedError(
107 'The "get_points" method has to be overwritten in the derived class'
108 )
110 def get_all_nodes(self):
111 """Return all nodes associated with this set.
113 This includes nodes contained within the geometry added to this
114 set.
115 """
116 raise NotImplementedError(
117 'The "get_all_nodes" method has to be overwritten in the derived class'
118 )
120 def dump_to_list(self):
121 """Return a list with the legacy strings of this geometry set."""
123 # Sort nodes based on their global index
124 nodes = sorted(self.get_all_nodes(), key=lambda n: n.i_global)
126 if not nodes:
127 raise ValueError("Writing empty geometry sets is not supported")
129 return [
130 {
131 "type": "NODE",
132 "node_id": node.i_global,
133 "d_type": self.geometry_set_names[self.geometry_type],
134 "d_id": self.i_global,
135 }
136 for node in nodes
137 ]
140class GeometrySet(GeometrySetBase):
141 """Geometry set which is defined by geometric entries."""
143 def __init__(self, geometry, **kwargs):
144 """Initialize the geometry set.
146 Args
147 ----
148 geometry: _List or single Geometry/GeometrySet
149 Geometries associated with this set. Empty geometries (i.e., no given)
150 are not supported.
151 """
153 # This is ok, we check every single type in the add method
154 if isinstance(geometry, list):
155 geometry_type = self._get_geometry_type(geometry[0])
156 else:
157 geometry_type = self._get_geometry_type(geometry)
159 super().__init__(geometry_type, **kwargs)
161 self.geometry_objects = {}
162 for geo in _mpy.geo:
163 self.geometry_objects[geo] = {}
164 self.add(geometry)
166 @staticmethod
167 def _get_geometry_type(item):
168 """Return the geometry type of a given item."""
170 if isinstance(item, _Node):
171 return _mpy.geo.point
172 elif isinstance(item, _Beam):
173 return _mpy.geo.line
174 elif isinstance(item, GeometrySet):
175 return item.geometry_type
176 raise TypeError(f"Got unexpected type {type(item)}")
178 def add(self, item):
179 """Add a geometry item to this object."""
181 if isinstance(item, list):
182 for sub_item in item:
183 self.add(sub_item)
184 elif isinstance(item, GeometrySet):
185 if item.geometry_type is self.geometry_type:
186 for geometry in item.geometry_objects[self.geometry_type]:
187 self.add(geometry)
188 else:
189 raise TypeError(
190 "You tried to add a {item.geometry_type} set to a {self.geometry_type} set. "
191 "This is not possible"
192 )
193 elif self._get_geometry_type(item) is self.geometry_type:
194 self.geometry_objects[self.geometry_type][item] = None
195 else:
196 raise TypeError(f"Got unexpected geometry type {type(item)}")
198 def get_node_dict(self):
199 """Return the dictionary containing the explicitly added nodes for this
200 set.
202 For non-point sets an empty dict is returned.
203 """
204 if self.geometry_type is _mpy.geo.point:
205 return self.geometry_objects[_mpy.geo.point]
206 else:
207 return {}
209 def get_points(self):
210 """Return nodes explicitly associated with this set.
212 Only in case this is a point set something is returned here.
213 """
214 if self.geometry_type is _mpy.geo.point:
215 return list(self.geometry_objects[_mpy.geo.point].keys())
216 else:
217 raise TypeError(
218 "The function get_points can only be called for point sets."
219 f" The present type is {self.geometry_type}"
220 )
222 def get_all_nodes(self):
223 """Return all nodes associated with this set.
225 This includes nodes contained within the geometry added to this
226 set.
227 """
229 if self.geometry_type is _mpy.geo.point:
230 return list(self.geometry_objects[_mpy.geo.point].keys())
231 elif self.geometry_type is _mpy.geo.line:
232 nodes = []
233 for element in self.geometry_objects[_mpy.geo.line].keys():
234 nodes.extend(element.nodes)
235 # Remove duplicates while preserving order
236 return list(dict.fromkeys(nodes))
237 else:
238 raise TypeError(
239 "Currently GeometrySet are only implemented for points and lines"
240 )
242 def get_geometry_objects(self):
243 """Return a list of the objects with the specified geometry type."""
244 return list(self.geometry_objects[self.geometry_type].keys())
247class GeometrySetNodes(GeometrySetBase):
248 """Geometry set which is defined by nodes and not explicit geometry."""
250 def __init__(self, geometry_type, nodes=None, **kwargs):
251 """Initialize the geometry set.
253 Args
254 ----
255 geometry_type: mpy.geo
256 Type of geometry. This is necessary, as the boundary conditions
257 and input file depend on that type.
258 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes)
259 Node(s) or list of nodes to be added to this geometry set.
260 """
262 super().__init__(geometry_type, **kwargs)
263 self.nodes = {}
264 if nodes is not None:
265 self.add(nodes)
267 def add(self, value):
268 """Add nodes to this object.
270 Args
271 ----
272 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes)
273 Node(s) or list of nodes to be added to this geometry set.
274 """
276 if isinstance(value, list):
277 # Loop over items and check if they are either Nodes or integers.
278 # This improves the performance considerably when large list of
279 # Nodes are added.
280 for item in value:
281 self.add(item)
282 elif isinstance(value, (int, _Node)):
283 self.nodes[value] = None
284 elif isinstance(value, GeometrySetNodes):
285 # Add all nodes from this geometry set.
286 if self.geometry_type == value.geometry_type:
287 for node in value.nodes:
288 self.add(node)
289 else:
290 raise TypeError(
291 f"You tried to add a {value.geometry_type} set to a {self.geometry_type} set. "
292 "This is not possible"
293 )
294 else:
295 raise TypeError(f"Expected Node or list, but got {type(value)}")
297 def get_node_dict(self):
298 """Return the dictionary containing the explicitly added nodes for this
299 set."""
300 return self.nodes
302 def get_points(self):
303 """Return nodes explicitly associated with this set."""
304 if self.geometry_type is _mpy.geo.point:
305 return self.get_all_nodes()
306 else:
307 raise TypeError(
308 "The function get_points can only be called for point sets."
309 f" The present type is {self.geometry_type}"
310 )
312 def get_all_nodes(self):
313 """Return all nodes associated with this set."""
314 return list(self.nodes.keys())
317class GeometryName(dict):
318 """Group node geometry sets together.
320 This is mainly used for export from mesh functions. The sets can be
321 accessed by a unique name. There is no distinction between different
322 types of geometry, every name can only be used once -> use
323 meaningful names.
324 """
326 def __setitem__(self, key, value):
327 """Set a geometry set in this container."""
329 if not isinstance(key, str):
330 raise TypeError(f"Expected string, got {type(key)}!")
331 if isinstance(value, GeometrySetBase):
332 super().__setitem__(key, value)
333 else:
334 raise NotImplementedError("GeometryName can only store GeometrySets")
337class GeometrySetContainer(_ContainerBase):
338 """A class to group geometry sets together with the key being the geometry
339 type."""
341 def __init__(self, *args, **kwargs):
342 """Initialize the container and create the default keys in the map."""
343 super().__init__(*args, **kwargs)
345 self.item_types = [GeometrySetBase]
347 for geometry_key in _mpy.geo:
348 self[geometry_key] = []
350 def copy(self):
351 """When creating a copy of this object, all lists in this object will
352 be copied also."""
354 # Create a new geometry set container.
355 copy = GeometrySetContainer()
357 # Add a copy of every list from this container to the new one.
358 for geometry_key in _mpy.geo:
359 copy[geometry_key] = self[geometry_key].copy()
361 return copy