Coverage for src/meshpy/core/geometry_set.py: 88%
139 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-28 04:21 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-28 04:21 +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."""
25import numpy as _np
27from meshpy.core.base_mesh_item import BaseMeshItem as _BaseMeshItem
28from meshpy.core.conf import mpy as _mpy
29from meshpy.core.container import ContainerBase as _ContainerBase
30from meshpy.core.element_beam import Beam as _Beam
31from meshpy.core.node import Node as _Node
32from meshpy.utils.environment import fourcipp_is_available as _fourcipp_is_available
35class GeometrySetBase(_BaseMeshItem):
36 """Base class for a geometry set."""
38 # Node set names for the input file file.
39 geometry_set_names = {
40 _mpy.geo.point: "DNODE",
41 _mpy.geo.line: "DLINE",
42 _mpy.geo.surface: "DSURFACE",
43 _mpy.geo.volume: "DVOL",
44 }
46 def __init__(self, geometry_type, name=None, **kwargs):
47 """Initialize the geometry set.
49 Args
50 ----
51 geometry_type: mpy.geo
52 Type of geometry. MeshPy only supports geometry sets of a single
53 specified geometry type.
54 name: str
55 Optional name to identify this geometry set.
56 """
57 super().__init__(**kwargs)
59 self.geometry_type = geometry_type
60 self.name = name
62 def link_to_nodes(self, *, link_to_nodes="explicitly_contained_nodes"):
63 """Set a link to this object in the all contained nodes of this
64 geometry set.
66 link_to_nodes: str
67 "explicitly_contained_nodes":
68 A link will be set for all nodes that are explicitly part of the geometry set
69 "all_nodes":
70 A link will be set for all nodes that are part of the geometry set, i.e., also
71 nodes connected to elements of an element set. This is mainly used for vtk
72 output so we can color the nodes which are part of element sets.
73 """
74 if link_to_nodes == "explicitly_contained_nodes":
75 node_list = self.get_node_dict().keys()
76 elif link_to_nodes == "all_nodes":
77 node_list = self.get_all_nodes()
78 else:
79 raise ValueError(f'Got unexpected value link nodes="{link_to_nodes}"')
80 for node in node_list:
81 node.node_sets_link.append(self)
83 def check_replaced_nodes(self):
84 """Check if nodes in this set have to be replaced.
86 We need to do this for explicitly contained nodes in this set.
87 """
88 # Don't iterate directly over the keys as the dict changes during this iteration
89 for node in list(self.get_node_dict().keys()):
90 if node.master_node is not None:
91 self.replace_node(node, node.get_master_node())
93 def replace_node(self, old_node, new_node):
94 """Replace old_node with new_node."""
96 explicit_nodes_in_this_set = self.get_node_dict()
97 explicit_nodes_in_this_set[new_node] = None
98 del explicit_nodes_in_this_set[old_node]
100 def get_node_dict(self):
101 """Return the dictionary containing the explicitly added nodes for this
102 set."""
103 raise NotImplementedError(
104 'The "get_node_dict" method has to be overwritten in the derived class'
105 )
107 def get_points(self):
108 """Return nodes explicitly associated with this set."""
109 raise NotImplementedError(
110 'The "get_points" method has to be overwritten in the derived class'
111 )
113 def get_all_nodes(self):
114 """Return all nodes associated with this set.
116 This includes nodes contained within the geometry added to this
117 set.
118 """
119 raise NotImplementedError(
120 'The "get_all_nodes" method has to be overwritten in the derived class'
121 )
123 def dump_to_list(self):
124 """Return a list with the legacy strings of this geometry set."""
126 if _fourcipp_is_available():
127 raise ValueError(
128 "Port this functionality to dump the geometry set to a suitable data format"
129 )
131 # Sort the nodes based on the node GID.
132 nodes = self.get_all_nodes()
133 if len(nodes) == 0:
134 raise ValueError("Writing empty geometry sets is not supported")
135 nodes_id = [node.i_global for node in nodes]
136 sort_indices = _np.argsort(nodes_id)
137 nodes = [nodes[i] for i in sort_indices]
139 return [
140 f"NODE {node.i_global} {self.geometry_set_names[self.geometry_type]} {self.i_global}"
141 for node in nodes
142 ]
145class GeometrySet(GeometrySetBase):
146 """Geometry set which is defined by geometric entries."""
148 def __init__(self, geometry, **kwargs):
149 """Initialize the geometry set.
151 Args
152 ----
153 geometry: _List or single Geometry/GeometrySet
154 Geometries associated with this set. Empty geometries (i.e., no given)
155 are not supported.
156 """
158 # This is ok, we check every single type in the add method
159 if isinstance(geometry, list):
160 geometry_type = self._get_geometry_type(geometry[0])
161 else:
162 geometry_type = self._get_geometry_type(geometry)
164 super().__init__(geometry_type, **kwargs)
166 self.geometry_objects = {}
167 for geo in _mpy.geo:
168 self.geometry_objects[geo] = {}
169 self.add(geometry)
171 @staticmethod
172 def _get_geometry_type(item):
173 """Return the geometry type of a given item."""
175 if isinstance(item, _Node):
176 return _mpy.geo.point
177 elif isinstance(item, _Beam):
178 return _mpy.geo.line
179 elif isinstance(item, GeometrySet):
180 return item.geometry_type
181 raise TypeError(f"Got unexpected type {type(item)}")
183 def add(self, item):
184 """Add a geometry item to this object."""
186 if isinstance(item, list):
187 for sub_item in item:
188 self.add(sub_item)
189 elif isinstance(item, GeometrySet):
190 if item.geometry_type is self.geometry_type:
191 for geometry in item.geometry_objects[self.geometry_type]:
192 self.add(geometry)
193 else:
194 raise TypeError(
195 "You tried to add a {item.geometry_type} set to a {self.geometry_type} set. "
196 "This is not possible"
197 )
198 elif self._get_geometry_type(item) is self.geometry_type:
199 self.geometry_objects[self.geometry_type][item] = None
200 else:
201 raise TypeError(f"Got unexpected geometry type {type(item)}")
203 def get_node_dict(self):
204 """Return the dictionary containing the explicitly added nodes for this
205 set.
207 For non-point sets an empty dict is returned.
208 """
209 if self.geometry_type is _mpy.geo.point:
210 return self.geometry_objects[_mpy.geo.point]
211 else:
212 return {}
214 def get_points(self):
215 """Return nodes explicitly associated with this set.
217 Only in case this is a point set something is returned here.
218 """
219 if self.geometry_type is _mpy.geo.point:
220 return list(self.geometry_objects[_mpy.geo.point].keys())
221 else:
222 raise TypeError(
223 "The function get_points can only be called for point sets."
224 f" The present type is {self.geometry_type}"
225 )
227 def get_all_nodes(self):
228 """Return all nodes associated with this set.
230 This includes nodes contained within the geometry added to this
231 set.
232 """
234 if self.geometry_type is _mpy.geo.point:
235 return list(self.geometry_objects[_mpy.geo.point].keys())
236 elif self.geometry_type is _mpy.geo.line:
237 nodes = []
238 for element in self.geometry_objects[_mpy.geo.line].keys():
239 nodes.extend(element.nodes)
240 # Remove duplicates while preserving order
241 return list(dict.fromkeys(nodes))
242 else:
243 raise TypeError(
244 "Currently GeometrySet are only implemented for points and lines"
245 )
247 def get_geometry_objects(self):
248 """Return a list of the objects with the specified geometry type."""
249 return list(self.geometry_objects[self.geometry_type].keys())
252class GeometrySetNodes(GeometrySetBase):
253 """Geometry set which is defined by nodes and not explicit geometry."""
255 def __init__(self, geometry_type, nodes=None, **kwargs):
256 """Initialize the geometry set.
258 Args
259 ----
260 geometry_type: mpy.geo
261 Type of geometry. This is necessary, as the boundary conditions
262 and input file depend on that type.
263 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes)
264 Node(s) or list of nodes to be added to this geometry set.
265 """
267 super().__init__(geometry_type, **kwargs)
268 self.nodes = {}
269 if nodes is not None:
270 self.add(nodes)
272 def add(self, value):
273 """Add nodes to this object.
275 Args
276 ----
277 nodes: Node, GeometrySetNodes, list(Nodes), list(GeometrySetNodes)
278 Node(s) or list of nodes to be added to this geometry set.
279 """
281 if isinstance(value, list):
282 # Loop over items and check if they are either Nodes or integers.
283 # This improves the performance considerably when large list of
284 # Nodes are added.
285 for item in value:
286 self.add(item)
287 elif isinstance(value, (int, _Node)):
288 self.nodes[value] = None
289 elif isinstance(value, GeometrySetNodes):
290 # Add all nodes from this geometry set.
291 if self.geometry_type == value.geometry_type:
292 for node in value.nodes:
293 self.add(node)
294 else:
295 raise TypeError(
296 f"You tried to add a {value.geometry_type} set to a {self.geometry_type} set. "
297 "This is not possible"
298 )
299 else:
300 raise TypeError(f"Expected Node or list, but got {type(value)}")
302 def get_node_dict(self):
303 """Return the dictionary containing the explicitly added nodes for this
304 set."""
305 return self.nodes
307 def get_points(self):
308 """Return nodes explicitly associated with this set."""
309 if self.geometry_type is _mpy.geo.point:
310 return self.get_all_nodes()
311 else:
312 raise TypeError(
313 "The function get_points can only be called for point sets."
314 f" The present type is {self.geometry_type}"
315 )
317 def get_all_nodes(self):
318 """Return all nodes associated with this set."""
319 return list(self.nodes.keys())
322class GeometryName(dict):
323 """Group node geometry sets together.
325 This is mainly used for export from mesh functions. The sets can be
326 accessed by a unique name. There is no distinction between different
327 types of geometry, every name can only be used once -> use
328 meaningful names.
329 """
331 def __setitem__(self, key, value):
332 """Set a geometry set in this container."""
334 if not isinstance(key, str):
335 raise TypeError(f"Expected string, got {type(key)}!")
336 if isinstance(value, GeometrySetBase):
337 super().__setitem__(key, value)
338 else:
339 raise NotImplementedError("GeometryName can only store GeometrySets")
342class GeometrySetContainer(_ContainerBase):
343 """A class to group geometry sets together with the key being the geometry
344 type."""
346 def __init__(self, *args, **kwargs):
347 """Initialize the container and create the default keys in the map."""
348 super().__init__(*args, **kwargs)
350 self.item_types = [GeometrySetBase]
352 for geometry_key in _mpy.geo:
353 self[geometry_key] = []
355 def copy(self):
356 """When creating a copy of this object, all lists in this object will
357 be copied also."""
359 # Create a new geometry set container.
360 copy = GeometrySetContainer()
362 # Add a copy of every list from this container to the new one.
363 for geometry_key in _mpy.geo:
364 copy[geometry_key] = self[geometry_key].copy()
366 return copy