Coverage for src/beamme/four_c/model_importer.py: 93%
178 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +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 contains functions to load and parse existing 4C input files."""
24from collections import defaultdict as _defaultdict
25from pathlib import Path as _Path
26from typing import Tuple as _Tuple
28import numpy as _np
30from beamme.core.boundary_condition import BoundaryCondition as _BoundaryCondition
31from beamme.core.boundary_condition import (
32 BoundaryConditionBase as _BoundaryConditionBase,
33)
34from beamme.core.conf import Geometry as _Geometry
35from beamme.core.conf import bme as _bme
36from beamme.core.coupling import Coupling as _Coupling
37from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes
38from beamme.core.mesh import Mesh as _Mesh
39from beamme.core.mesh_representation import (
40 MESH_REPRESENTATION_MAPPINGS as _MESH_REPRESENTATION_MAPPINGS,
41)
42from beamme.core.mesh_representation import GeometrySetInfo as _GeometrySetInfo
43from beamme.core.mesh_representation import MeshRepresentation as _MeshRepresentation
44from beamme.core.mesh_representation import (
45 string_to_geometry_set_info as _string_to_geometry_set_info,
46)
47from beamme.core.node import Node as _Node
48from beamme.four_c.element_data import FourCElementData as _FourCElementData
49from beamme.four_c.element_data import (
50 four_c_element_data_from_legacy_dict as _four_c_element_data_from_legacy_dict,
51)
52from beamme.four_c.element_solid import get_four_c_solid as _get_four_c_solid
53from beamme.four_c.input_file import InputFile as _InputFile
54from beamme.four_c.input_file_mappings import (
55 INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS,
56)
57from beamme.four_c.material import MaterialSolid as _MaterialSolid
58from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
60if _cubitpy_is_available():
61 from cubitpy.cubit_to_fourc_input import (
62 get_input_file_with_mesh as _get_input_file_with_mesh,
63 )
66class UniqueDataTracker:
67 """Helper class to track unique data dictionaries and assign IDs to them.
69 When importing input files, we need to identify elements of the same
70 type. The type information is given in dictionaries. This class
71 provides a tracker that can be queried with a given element data and
72 return an already matching element type ID or create a new one.
73 """
75 def __init__(self) -> None:
76 self.unique_id_to_data: dict[int, _FourCElementData] = {}
78 def get_unique_id(self, data: _FourCElementData) -> int:
79 """Get the unique ID for the given data. If the data has not been seen
80 before, a new ID will be assigned to it.
82 Args:
83 data: The data dictionary to get the ID for.
85 Returns:
86 The unique ID for the given data.
87 """
88 for unique_id, seen_data in self.unique_id_to_data.items():
89 if data == seen_data:
90 return unique_id
92 # If we reach this point, the data has not been seen before. We assign a new ID to it.
93 new_unique_id = len(self.unique_id_to_data)
94 self.unique_id_to_data[new_unique_id] = data
95 return new_unique_id
98def import_cubitpy_model(
99 cubit, convert_input_to_mesh: bool = False
100) -> _Tuple[_InputFile, _Mesh]:
101 """Convert a CubitPy instance to a BeamMe InputFile.
103 Args:
104 cubit (CubitPy): An instance of a cubit model.
105 convert_input_to_mesh: If this is false, the cubit model will be
106 converted to plain FourCIPP input data. If this is true, an input
107 file with all the parameters will be returned and a mesh which
108 contains the mesh information from cubit converted to BeamMe
109 objects.
111 Returns:
112 A tuple with the input file and the mesh. If convert_input_to_mesh is
113 False, the mesh will be empty. Note that the input sections which are
114 converted to a BeamMe mesh are removed from the input file object.
115 """
117 input_file = _InputFile()
118 input_file.add(_get_input_file_with_mesh(cubit).sections)
120 if convert_input_to_mesh:
121 return _extract_mesh_from_input_file(input_file)
122 else:
123 return input_file, _Mesh()
126def import_four_c_model(
127 input_file_path: _Path, convert_input_to_mesh: bool = False
128) -> _Tuple[_InputFile, _Mesh]:
129 """Import an existing 4C input file and optionally convert it into a BeamMe
130 mesh.
132 Args:
133 input_file_path: A file path to an existing 4C input file that will be
134 imported.
135 convert_input_to_mesh: If True, the input file will be converted to a
136 BeamMe mesh.
138 Returns:
139 A tuple with the input file and the mesh. If convert_input_to_mesh is
140 False, the mesh will be empty. Note that the input sections which are
141 converted to a BeamMe mesh are removed from the input file object.
142 """
144 input_file = _InputFile().from_4C_yaml(input_file_path=input_file_path)
146 if convert_input_to_mesh:
147 return _extract_mesh_from_input_file(input_file)
148 else:
149 return input_file, _Mesh()
152def _extract_mesh_from_input_file(input_file: _InputFile) -> tuple[_InputFile, _Mesh]:
153 """Convert an InputFile into a native mesh by translating sections like
154 materials, nodes, elements, geometry sets, and boundary conditions.
156 Args:
157 input_file: The input file containing 4C sections.
158 Returns:
159 A tuple (input_file, mesh). The input_file is modified in place to remove
160 sections converted into BeamMe objects.
161 """
162 if input_file.contains_external_mesh_based_geometry():
163 raise NotImplementedError(
164 "Importing external mesh-based geometry from 4C input files "
165 "is not yet implemented."
166 )
167 else:
168 (
169 mesh_representation,
170 element_type_id_to_data,
171 node_set_id_mesh_representation_to_input_file,
172 ) = _extract_mesh_representation(input_file)
173 return _create_mesh_from_mesh_representation(
174 input_file,
175 mesh_representation,
176 element_type_id_to_data,
177 node_set_id_mesh_representation_to_input_file,
178 )
181def _extract_mesh_representation(
182 input_file: _InputFile,
183) -> tuple[_MeshRepresentation, dict[int, _FourCElementData], dict[int, int]]:
184 """Extract the mesh representation from mesh data directly contained in the
185 input file.
187 This will do an inplace removal of the mesh data from the provided input file.
189 Args:
190 input_file: The input file containing the mesh data, will be modified in place.
192 Returns:
193 A tuple containing:
194 - `mesh_representation`: Contains the mesh data extracted from the input file.
195 - `element_type_id_to_data`: A mapping between the element type ID and the
196 element data.
197 - `node_set_id_mesh_representation_to_input_file`: A mapping that can be used
198 to map the IDs in the mesh representation to the IDs in the input file.
199 """
201 # extract nodes
202 nodes = input_file.pop("NODE COORDS", [])
203 n_nodes = len(nodes)
204 points = _np.zeros((n_nodes, 3))
205 point_types = _np.full(n_nodes, -1)
206 control_point_weights = _np.full(n_nodes, -1.0)
207 for i, node in enumerate(nodes):
208 four_c_node_type = node["data"]["type"]
209 node_id = node["id"]
210 try:
211 node_type = _INPUT_FILE_MAPPINGS["four_c_node_type_to_beamme_node_type"][
212 four_c_node_type
213 ]
214 except KeyError:
215 raise ValueError(
216 f"Unknown node type `{four_c_node_type}` for node {node_id}."
217 )
218 points[i] = node["COORD"]
219 point_types[i] = node_type.value
220 if node_type == _bme.node_type.control_point:
221 control_point_weights[i] = node["data"]["weight"]
223 # extract elements
224 element_type_tracker = UniqueDataTracker()
225 elements = input_file.pop("STRUCTURE ELEMENTS", [])
226 n_elements = len(elements)
227 cell_connectivity = []
228 cell_types = _np.full(n_elements, -1)
229 cell_element_type_ids = _np.full(n_elements, -1)
230 cell_material_ids = _np.full(n_elements, -1)
231 for i_element, input_element in enumerate(elements):
232 four_c_element_data, element_id, connectivity, material_id = (
233 _four_c_element_data_from_legacy_dict(input_element)
234 )
235 element_type_id = element_type_tracker.get_unique_id(four_c_element_data)
237 # Check if connectivity has to be reordered
238 reorder_indices = _INPUT_FILE_MAPPINGS[
239 "four_c_cell_to_vtk_connectivity_mapping"
240 ].get(four_c_element_data.four_c_cell, None)
241 if reorder_indices is not None:
242 connectivity = connectivity[reorder_indices]
244 cell_connectivity.extend([len(connectivity), *connectivity.tolist()])
246 try:
247 vtk_cell_type = _INPUT_FILE_MAPPINGS["four_c_cell_to_vtk_cell_type"][
248 four_c_element_data.four_c_cell
249 ]
250 except KeyError:
251 raise ValueError(
252 f"Unknown cell type `{four_c_element_data.four_c_cell}` for element {element_id}."
253 )
255 cell_types[i_element] = vtk_cell_type
256 cell_element_type_ids[i_element] = element_type_id
257 cell_material_ids[i_element] = material_id
259 # extract geometry sets
260 node_sets: list[_GeometrySetInfo] = []
261 node_set_id_mesh_representation_to_input_file: dict[int, int] = {}
262 for section_name in input_file.sections:
263 if not section_name.endswith("TOPOLOGY"):
264 continue
266 items = input_file.pop(section_name, [])
267 if not items:
268 continue
270 # Find geometry type for this section
271 try:
272 geometry_type = _INPUT_FILE_MAPPINGS[
273 "geometry_sets_condition_to_geometry_name"
274 ][section_name]
275 except KeyError as e:
276 raise ValueError(f"Unknown geometry section: {section_name}") from e
278 # Extract geometry set indices
279 geom_dict: dict[int, set[int]] = _defaultdict(set)
280 for entry in items:
281 geom_dict[entry["d_id"]].add(entry["node_id"] - 1)
283 for input_file_node_set_id, node_ids in geom_dict.items():
284 node_set_id = len(node_sets)
286 node_set_flag = _np.zeros(n_nodes, dtype=int)
287 node_set_flag[list(node_ids)] = 1
289 node_sets.append(
290 _GeometrySetInfo(
291 geometry_type=geometry_type,
292 i_global=node_set_id,
293 point_flag_vector=node_set_flag,
294 )
295 )
297 node_set_id_mesh_representation_to_input_file[node_set_id] = (
298 input_file_node_set_id
299 )
301 # Create the mesh representation and add the extracted data to it.
302 mesh_representation = _MeshRepresentation(
303 cell_connectivity=cell_connectivity,
304 cell_types=cell_types,
305 points=points,
306 geometry_sets=node_sets,
307 point_data={
308 "point_type": point_types,
309 "control_point_weight": control_point_weights,
310 },
311 cell_data={
312 "element_type_id": cell_element_type_ids,
313 "material_id": cell_material_ids,
314 },
315 )
317 return (
318 mesh_representation,
319 element_type_tracker.unique_id_to_data,
320 node_set_id_mesh_representation_to_input_file,
321 )
324def _create_mesh_from_mesh_representation(
325 input_file,
326 mesh_representation,
327 element_type_id_to_data,
328 node_set_id_mesh_representation_to_input_file,
329) -> tuple[_InputFile, _Mesh]:
330 """Extract a BeamMe mesh from a mesh representation.
332 Args:
333 input_file: The input file containing general data.
334 mesh_representation: The mesh representation to convert.
335 node_set_id_mesh_representation_to_input_file: A mapping of the mesh
336 representation node set IDs to input file IDs, which can be used to link
337 the geometry sets in the input file to the node sets in the mesh
338 representation.
340 Returns:
341 A tuple (input_file, mesh). The input_file is modified in place to remove
342 sections converted into the BeamMe mesh.
343 """
345 # convert all sections to native objects and add to a new mesh
346 mesh = _Mesh()
348 # extract materials
349 material_id_map = _extract_materials_from_input_file(input_file)
350 mesh.materials.extend(material_id_map.values())
352 # extract nodes
353 for node_coordinates, node_type in zip(
354 mesh_representation.points, mesh_representation.point_data["point_type"]
355 ):
356 if node_type == _bme.node_type.node.value:
357 mesh.nodes.append(_Node(node_coordinates))
358 else:
359 raise ValueError(
360 f"Mesh conversion for node type {_bme.node_type(node_type).name} is not implemented!"
361 )
363 # extract element types
364 element_type_id_to_element_type: dict[int, type] = {}
365 for (
366 element_type_id,
367 element_data,
368 ) in element_type_id_to_data.items():
369 element_type, n_nodes = _INPUT_FILE_MAPPINGS[
370 "four_c_cell_to_element_type_and_n_nodes"
371 ][element_data.four_c_cell]
372 if not element_type == _bme.element_type.solid:
373 raise ValueError(
374 f"Mesh conversion for element type {element_type} is not implemented!"
375 )
376 element_type_id_to_element_type[element_type_id] = _get_four_c_solid(
377 element_type,
378 element_data.four_c_type,
379 n_nodes=n_nodes,
380 element_technology=element_data.element_technology,
381 )
383 # loop over the elements and create the mesh elements with the correct type, connectivity and material.
384 for connectivity, cell_element_type_id_, material_id in zip(
385 mesh_representation.connectivity_iterator(),
386 mesh_representation.cell_data["element_type_id"],
387 mesh_representation.cell_data["material_id"],
388 ):
389 element_type = element_type_id_to_element_type[cell_element_type_id_]
391 reorder_indices = _MESH_REPRESENTATION_MAPPINGS[
392 "element_type_and_n_nodes_to_connectivity_mapping_vtk_to_beamme"
393 ].get((element_type.element_type, len(connectivity)), None)
394 if reorder_indices is not None:
395 nodes = [mesh.nodes[connectivity[i]] for i in reorder_indices]
396 else:
397 nodes = [mesh.nodes[i] for i in connectivity]
399 element = element_type(nodes=nodes)
401 if not material_id == -1:
402 element.material = material_id_map[material_id]
403 mesh.elements.append(element)
405 # extract geometry sets
406 geometry_sets_in_sections: dict[_Geometry, dict[int, _GeometrySetNodes]] = (
407 _defaultdict(dict)
408 )
409 for name in mesh_representation.point_data.keys():
410 info = _string_to_geometry_set_info(name)
411 if info is not None:
412 node_indices = _np.nonzero(mesh_representation.point_data[name])[0]
413 geometry_type = info.geometry_type
414 geometry_set = _GeometrySetNodes(
415 geometry_type, nodes=[mesh.nodes[i] for i in node_indices]
416 )
417 input_file_id = node_set_id_mesh_representation_to_input_file[info.i_global]
418 geometry_sets_in_sections[geometry_type][input_file_id] = geometry_set
419 mesh.add(geometry_set)
421 # extract boundary conditions
422 _standard_bc_types = (
423 _bme.bc.dirichlet,
424 _bme.bc.neumann,
425 _bme.bc.locsys,
426 _bme.bc.beam_to_solid_surface_meshtying,
427 _bme.bc.beam_to_solid_surface_contact,
428 _bme.bc.beam_to_solid_volume_meshtying,
429 )
431 for (bc_key, geometry_type), section_name in _INPUT_FILE_MAPPINGS[
432 "boundary_conditions"
433 ].items():
434 for bc_data in input_file.pop(section_name, []):
435 geometry_set = geometry_sets_in_sections[geometry_type][bc_data.pop("E")]
437 bc_obj: _BoundaryConditionBase
439 if bc_key in _standard_bc_types or isinstance(bc_key, str):
440 bc_obj = _BoundaryCondition(geometry_set, bc_data, bc_type=bc_key)
441 elif bc_key is _bme.bc.point_coupling:
442 bc_obj = _Coupling(
443 geometry_set, bc_key, bc_data, check_overlapping_nodes=False
444 )
445 else:
446 raise ValueError(f"Unexpected boundary condition: {bc_key}")
448 mesh.boundary_conditions.append((bc_key, geometry_type), bc_obj)
450 return input_file, mesh
453def _extract_materials_from_input_file(
454 input_file: _InputFile,
455) -> dict[int, _MaterialSolid]:
456 """Extract all materials from the input file and convert them to BeamMe
457 materials.
459 Args:
460 input_file: The input file containing the material sections.
462 Returns:
463 A mapping of material IDs to BeamMe material objects.
464 """
466 material_id_map_all = {}
468 for mat in input_file.pop("MATERIALS", []):
469 mat_id = mat.pop("MAT") - 1
470 if len(mat) != 1:
471 raise ValueError(
472 f"Could not convert the material data `{mat}` to a BeamMe material!"
473 )
474 mat_name, mat_data = list(mat.items())[0]
475 material = _MaterialSolid(material_string=mat_name, data=mat_data)
476 material_id_map_all[mat_id] = material
478 nested_materials = set()
479 for material in material_id_map_all.values():
480 # Replace the integer IDs in the "MATIDS" list of the material with the actual
481 # material objects.
482 sub_materials = material.data.get("MATIDS", [])
483 sub_material_ids = _np.array(sub_materials) - 1
484 for i_sub_material, sub_material_id in enumerate(sub_material_ids):
485 try:
486 sub_materials[i_sub_material] = material_id_map_all[sub_material_id]
487 except KeyError as key_exception:
488 raise KeyError(
489 f"Material ID {sub_material_id} not in material_id_map_all (available "
490 f"IDs: {list(material_id_map_all.keys())})."
491 ) from key_exception
492 nested_materials.add(sub_material_id)
494 # Get a map of all non-nested materials. We assume that only those are used as
495 # materials for elements. Also, add the non-nested materials to the mesh.
496 material_id_map = {
497 key: val
498 for key, val in material_id_map_all.items()
499 if key not in nested_materials
500 }
502 return material_id_map