beamme.four_c.model_importer

This module contains functions to load and parse existing 4C input files.

  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."""
 23
 24import tempfile as _tempfile
 25from collections import defaultdict as _defaultdict
 26from pathlib import Path as _Path
 27
 28import numpy as _np
 29
 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.element import Element as _Element
 38from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes
 39from beamme.core.mesh import Mesh as _Mesh
 40from beamme.core.mesh_representation import (
 41    MESH_REPRESENTATION_MAPPINGS as _MESH_REPRESENTATION_MAPPINGS,
 42)
 43from beamme.core.mesh_representation import GeometrySetInfo as _GeometrySetInfo
 44from beamme.core.mesh_representation import MeshRepresentation as _MeshRepresentation
 45from beamme.core.mesh_representation import (
 46    string_to_geometry_set_info as _string_to_geometry_set_info,
 47)
 48from beamme.core.node import Node as _Node
 49from beamme.four_c.element_data import FourCElementData as _FourCElementData
 50from beamme.four_c.element_data import (
 51    four_c_element_data_from_exo_dict as _four_c_element_data_from_exo_dict,
 52)
 53from beamme.four_c.element_data import (
 54    four_c_element_data_from_yaml_dict as _four_c_element_data_from_yaml_dict,
 55)
 56from beamme.four_c.element_solid import get_four_c_solid as _get_four_c_solid
 57from beamme.four_c.input_file import InputFile as _InputFile
 58from beamme.four_c.input_file_mappings import (
 59    INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS,
 60)
 61from beamme.four_c.material import MaterialSolid as _MaterialSolid
 62from beamme.utils.data_structures import (
 63    create_inverse_mapping as _create_inverse_mapping,
 64)
 65from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
 66
 67if _cubitpy_is_available():
 68    import netCDF4 as _netCDF4
 69    from cubitpy.conf import cupy as _cupy
 70    from cubitpy.cubit_utility import (
 71        string_to_node_set_info as _string_to_node_set_info,
 72    )
 73    from cubitpy.exodus_utility import get_exo_info as _get_exo_info
 74
 75
 76class UniqueDataTracker:
 77    """Helper class to track unique data dictionaries and assign IDs to them.
 78
 79    When importing input files, we need to identify elements of the same type. The type
 80    information is given in dictionaries. This class provides a tracker that can be
 81    queried with a given element data and return an already matching element type ID or
 82    create a new one.
 83    """
 84
 85    def __init__(self) -> None:
 86        self.unique_id_to_data: dict[int, _FourCElementData] = {}
 87
 88    def get_unique_id(self, data: _FourCElementData) -> int:
 89        """Get the unique ID for the given data.
 90
 91        If the data has not been seen before, a new ID will be assigned to it.
 92
 93        Args:
 94            data: The data dictionary to get the ID for.
 95
 96        Returns:
 97            The unique ID for the given data.
 98        """
 99        for unique_id, seen_data in self.unique_id_to_data.items():
100            if data == seen_data:
101                return unique_id
102
103        # If we reach this point, the data has not been seen before. We assign a new ID to it.
104        new_unique_id = len(self.unique_id_to_data)
105        self.unique_id_to_data[new_unique_id] = data
106        return new_unique_id
107
108
109def import_cubitpy_model(
110    cubit, convert_input_to_mesh: bool = False
111) -> tuple[_InputFile, _Mesh]:
112    """Convert a CubitPy instance to a BeamMe InputFile.
113
114    Args:
115        cubit (CubitPy): An instance of a cubit model.
116        convert_input_to_mesh: If this is false, the cubit model will be
117            converted to plain FourCIPP input data. If this is true, an input
118            file with all the parameters will be returned and a mesh which
119            contains the mesh information from cubit converted to BeamMe
120            objects.
121
122    Returns:
123        A tuple with the input file and the mesh. If convert_input_to_mesh is
124        False, the mesh will be empty. Note that the input sections which are
125        converted to a BeamMe mesh are removed from the input file object.
126    """
127    temp_dir: str | _Path
128    with _tempfile.TemporaryDirectory() as temp_dir:
129        temp_dir = _Path(temp_dir)
130        input_file_path = temp_dir / "temp_cubit_input_file.4C.yaml"
131        cubit.dump(
132            input_file_path, mesh_in_exo=True, mesh_in_exo_add_node_set_info=True
133        )
134        return import_four_c_model(
135            input_file_path, convert_input_to_mesh=convert_input_to_mesh
136        )
137
138
139def import_four_c_model(
140    input_file_path: _Path, convert_input_to_mesh: bool = False
141) -> tuple[_InputFile, _Mesh]:
142    """Import an existing 4C input file and optionally convert it into a BeamMe mesh.
143
144    Args:
145        input_file_path: A file path to an existing 4C input file that will be
146            imported.
147        convert_input_to_mesh: If True, the input file will be converted to a
148            BeamMe mesh.
149
150    Returns:
151        A tuple with the input file and the mesh. If convert_input_to_mesh is
152        False, the mesh will be empty. Note that the input sections which are
153        converted to a BeamMe mesh are removed from the input file object.
154    """
155    input_file = _InputFile().from_4C_yaml(input_file_path=input_file_path)
156    base_path = input_file_path.parent
157
158    if input_file.contains_mesh_based_geometry_exodus():
159        (
160            mesh_representation,
161            element_type_id_to_data,
162            node_set_id_mesh_representation_to_input_file,
163        ) = _extract_mesh_representation_from_exo(input_file, base_path)
164
165    elif convert_input_to_mesh:
166        (
167            mesh_representation,
168            element_type_id_to_data,
169            node_set_id_mesh_representation_to_input_file,
170        ) = _extract_mesh_representation(input_file)
171
172    else:
173        mesh_representation = None
174        element_type_id_to_data = None
175
176    if convert_input_to_mesh:
177        return _create_mesh_from_mesh_representation(
178            input_file,
179            mesh_representation,
180            element_type_id_to_data,
181            node_set_id_mesh_representation_to_input_file,
182        )
183    else:
184        if mesh_representation is not None:
185            input_file.mesh_representation = mesh_representation
186        if element_type_id_to_data is not None:
187            input_file.element_type_id_to_data = element_type_id_to_data
188        return input_file, _Mesh()
189
190
191def _extract_mesh_representation(
192    input_file: _InputFile,
193) -> tuple[_MeshRepresentation, dict[int, _FourCElementData], dict[int, int]]:
194    """Extract the mesh representation from mesh data directly contained in the input
195    file.
196
197    This will do an inplace removal of the mesh data from the provided input file.
198
199    Args:
200        input_file: The input file containing the mesh data, will be modified in place.
201
202    Returns:
203        A tuple containing:
204        - `mesh_representation`: Contains the mesh data extracted from the input file.
205        - `element_type_id_to_data`: A mapping between the element type ID and the
206            element data.
207        - `node_set_id_mesh_representation_to_input_file`: A mapping that can be used
208            to map the IDs in the mesh representation to the IDs in the input file.
209    """
210    # extract nodes
211    nodes = input_file.pop("NODE COORDS", [])
212    n_nodes = len(nodes)
213    points = _np.zeros((n_nodes, 3))
214    point_types = _np.full(n_nodes, -1)
215    control_point_weights = _np.full(n_nodes, -1.0)
216    for i, node in enumerate(nodes):
217        four_c_node_type = node["data"]["type"]
218        node_id = node["id"]
219        try:
220            node_type = _INPUT_FILE_MAPPINGS["four_c_node_type_to_beamme_node_type"][
221                four_c_node_type
222            ]
223        except KeyError:
224            raise ValueError(
225                f"Unknown node type `{four_c_node_type}` for node {node_id}."
226            )
227        points[i] = node["COORD"]
228        point_types[i] = node_type.value
229        if node_type == _bme.node_type.control_point:
230            control_point_weights[i] = node["data"]["weight"]
231
232    # extract elements
233    element_type_tracker = UniqueDataTracker()
234    elements = input_file.pop("STRUCTURE ELEMENTS", [])
235    n_elements = len(elements)
236    cell_connectivity = []
237    cell_types = _np.full(n_elements, -1)
238    cell_element_type_ids = _np.full(n_elements, -1)
239    cell_material_ids = _np.full(n_elements, -1)
240    for i_element, input_element in enumerate(elements):
241        four_c_element_data, element_id, connectivity, material_id = (
242            _four_c_element_data_from_yaml_dict(input_element)
243        )
244        element_type_id = element_type_tracker.get_unique_id(four_c_element_data)
245
246        # Check if connectivity has to be reordered
247        reorder_indices = _INPUT_FILE_MAPPINGS[
248            "four_c_cell_to_vtk_connectivity_mapping"
249        ].get(four_c_element_data.four_c_cell, None)
250        if reorder_indices is not None:
251            connectivity = connectivity[reorder_indices]
252
253        cell_connectivity.extend([len(connectivity), *connectivity.tolist()])
254
255        try:
256            vtk_cell_type = _INPUT_FILE_MAPPINGS["four_c_cell_to_vtk_cell_type"][
257                four_c_element_data.four_c_cell
258            ]
259        except KeyError:
260            raise ValueError(
261                f"Unknown cell type `{four_c_element_data.four_c_cell}` for element {element_id}."
262            )
263
264        cell_types[i_element] = vtk_cell_type
265        cell_element_type_ids[i_element] = element_type_id
266        cell_material_ids[i_element] = material_id
267
268    # extract geometry sets
269    node_sets: list[_GeometrySetInfo] = []
270    node_set_id_mesh_representation_to_input_file: dict[int, int] = {}
271    for section_name in input_file.sections:
272        if not section_name.endswith("TOPOLOGY"):
273            continue
274
275        items = input_file.pop(section_name, [])
276        if not items:
277            continue
278
279        # Find geometry type for this section
280        try:
281            geometry_type = _INPUT_FILE_MAPPINGS[
282                "geometry_sets_condition_to_geometry_name"
283            ][section_name]
284        except KeyError as e:
285            raise ValueError(f"Unknown geometry section: {section_name}") from e
286
287        # Extract geometry set indices
288        geom_dict: dict[int, set[int]] = _defaultdict(set)
289        for entry in items:
290            geom_dict[entry["d_id"]].add(entry["node_id"] - 1)
291
292        for input_file_node_set_id, node_ids in geom_dict.items():
293            node_set_id = len(node_sets)
294
295            node_set_flag = _np.zeros(n_nodes, dtype=int)
296            node_set_flag[list(node_ids)] = 1
297
298            node_sets.append(
299                _GeometrySetInfo(
300                    geometry_type=geometry_type,
301                    i_global=node_set_id,
302                    point_flag_vector=node_set_flag,
303                )
304            )
305
306            node_set_id_mesh_representation_to_input_file[node_set_id] = (
307                input_file_node_set_id
308            )
309
310    # Create the mesh representation and add the extracted data to it.
311    mesh_representation = _MeshRepresentation(
312        cell_connectivity=cell_connectivity,
313        cell_types=cell_types,
314        points=points,
315        geometry_sets=node_sets,
316        point_data={
317            "point_type": point_types,
318            "control_point_weight": control_point_weights,
319        },
320        cell_data={
321            "element_type_id": cell_element_type_ids,
322            "material_id": cell_material_ids,
323        },
324    )
325
326    return (
327        mesh_representation,
328        element_type_tracker.unique_id_to_data,
329        node_set_id_mesh_representation_to_input_file,
330    )
331
332
333def _get_exodus_path_from_input_file(input_file: _InputFile, base_path: _Path) -> _Path:
334    """Returns the path to the exodus file linked in the input file.
335
336    Args:
337        input_file: The input file to extract the exodus file path from.
338        base_path: The base path for loading the exodus file.
339
340    Returns:
341        The path to the exodus file linked in the input file.
342    """
343    if "STRUCTURE GEOMETRY" in input_file.fourc_input:
344        structure_geometry_section = input_file.fourc_input["STRUCTURE GEOMETRY"]
345        if "FILE" in structure_geometry_section:
346            exodus_file_name = _Path(structure_geometry_section["FILE"])
347            if exodus_file_name.suffix.lower() in [".exo", ".e"]:
348                exodus_file_path = base_path / exodus_file_name
349                if not exodus_file_path.is_file():
350                    raise FileNotFoundError(
351                        "The input file contains a link to an external mesh file "
352                        f"{exodus_file_name}, but this file does not exist at the expected "
353                        f"location {exodus_file_path}."
354                    )
355                return exodus_file_path
356            else:
357                raise ValueError(
358                    "The input file contains a link to an external file "
359                    f"{exodus_file_name}, with the extension {exodus_file_name.suffix}, but only "
360                    ".exo and .e files are supported."
361                )
362        else:
363            raise ValueError(
364                "The input file contains a STRUCTURE GEOMETRY section, but no "
365                "FILE entry."
366            )
367    else:
368        raise ValueError("The input file does not contain a STRUCTURE GEOMETRY section")
369
370
371def _extract_mesh_representation_from_exo(
372    input_file: _InputFile, base_path: _Path
373) -> tuple[_MeshRepresentation, dict[int, _FourCElementData], dict[int, int]]:
374    """Extract the mesh representation from mesh data in exodus format.
375
376    This will do an inplace removal of the extracted sections in the provided input file.
377
378    Args:
379        input_file: The input file containing the mesh data, will be modified in place.
380
381    Returns:
382        A tuple (mesh_representation, element_type_id_to_data, node_set_id_mesh_representation_to_input_file).
383        - `mesh_representation`: Contains the mesh data extracted from the input file.
384        - `element_type_id_to_data`: A mapping between the element type ID and the element data.
385        - `node_set_id_mesh_representation_to_input_file`: A mapping that can be used to map the
386           geometry set IDs in the mesh representation to the ones in the input file.
387    """
388    # Load the exodus file.
389    with _netCDF4.Dataset(
390        _get_exodus_path_from_input_file(input_file, base_path)
391    ) as exo:
392        # Read the coordinate array.
393        if "coordz" not in exo.variables:
394            raise ValueError(
395                "The exodus file provides only 2D coordinates, this is not supported."
396            )
397        coordinates = _np.array(
398            [exo.variables["coord" + dim][:] for dim in ["x", "y", "z"]],
399        ).transpose()
400        n_points = coordinates.shape[0]
401        point_types = _np.full(n_points, _bme.node_type.node.value)
402
403        # Remove the structure geometry section from the input file and extract the
404        # element blocks.
405        element_blocks = input_file.pop("STRUCTURE GEOMETRY")["ELEMENT_BLOCKS"]
406        cubit_id_to_element_blocks = {block["ID"]: block for block in element_blocks}
407
408        # Add the element connectivity
409        element_type_tracker = UniqueDataTracker()
410        block_connectivity_list = []
411        cell_types = []
412        cell_element_type_ids = []
413        cell_material_ids = []
414        _, exo_block_id_to_info = _get_exo_info(exo, "block")
415        for exo_id in sorted(exo_block_id_to_info.keys()):
416            # First, get the element block ID in beamme and the corresponding element data.
417            info = exo_block_id_to_info[exo_id]
418            element_data = cubit_id_to_element_blocks[info["cubit_id"]]
419            four_c_element_data, material_id = _four_c_element_data_from_exo_dict(
420                element_data
421            )
422            element_type_id = element_type_tracker.get_unique_id(four_c_element_data)
423
424            # Get the connectivity information for this block and if necessary
425            # reorder it to match the VTK node ordering.
426            connectivity = exo.variables[f"connect{exo_id + 1}"][:] - 1
427            n_elements_in_block = connectivity.shape[0]
428            n_nodes_per_element = connectivity.shape[1]
429            if (
430                not n_nodes_per_element
431                == _INPUT_FILE_MAPPINGS["four_c_cell_to_element_type_and_n_nodes"][
432                    four_c_element_data.four_c_cell
433                ][1]
434            ):
435                raise ValueError(
436                    f"Number of nodes per element {n_nodes_per_element} in block with "
437                    f"ID {info['cubit_id']} does not match expected number of nodes for "
438                    f"cell type {four_c_element_data.four_c_cell}."
439                )
440            reordering = _MESH_REPRESENTATION_MAPPINGS[
441                "connectivity_mapping_exodus_to_vtk"
442            ].get(n_nodes_per_element, None)
443            if reordering is not None:
444                connectivity = connectivity[:, reordering]
445
446            # Add the data for this element block to the cell data lists.
447            cell_material_ids.extend([material_id] * n_elements_in_block)
448            cell_element_type_ids.extend([element_type_id] * n_elements_in_block)
449            cell_types.extend(
450                [
451                    _INPUT_FILE_MAPPINGS["four_c_cell_to_vtk_cell_type"][
452                        four_c_element_data.four_c_cell
453                    ]
454                ]
455                * n_elements_in_block
456            )
457            block_connectivity = _np.empty(
458                (n_elements_in_block, n_nodes_per_element + 1), dtype=int
459            )
460            block_connectivity[:, 0] = n_nodes_per_element
461            block_connectivity[:, 1:] = connectivity
462            block_connectivity_list.append(block_connectivity.ravel())
463        cell_connectivity = _np.concatenate(block_connectivity_list)
464
465        # Extract the node sets.
466        cubitpy_geometry_type_to_beamme = {
467            _cupy.geometry.vertex: _bme.geo.point,
468            _cupy.geometry.curve: _bme.geo.line,
469            _cupy.geometry.surface: _bme.geo.surface,
470            _cupy.geometry.volume: _bme.geo.volume,
471        }
472        node_set_id_mesh_representation_to_input_file = {}
473        geometry_sets: list[_GeometrySetInfo] = []
474        _, exo_node_set_id_to_info = _get_exo_info(exo, "nodeset")
475        for exo_id in sorted(exo_node_set_id_to_info.keys()):
476            exo_name = exo_node_set_id_to_info[exo_id]["name"]
477            cubit_id, geometry_type_cubitpy, name = _string_to_node_set_info(exo_name)
478
479            node_set_flag = _np.zeros(n_points, dtype=int)
480            node_set_flag[exo.variables[f"node_ns{exo_id + 1}"][:] - 1] = 1
481
482            mesh_representation_id = len(geometry_sets)
483            node_set_id_mesh_representation_to_input_file[mesh_representation_id] = (
484                cubit_id
485            )
486            geometry_sets.append(
487                _GeometrySetInfo(
488                    geometry_type=cubitpy_geometry_type_to_beamme[
489                        geometry_type_cubitpy
490                    ],
491                    i_global=mesh_representation_id,
492                    point_flag_vector=node_set_flag,
493                    name=name,
494                )
495            )
496
497    # Remove the entries in the boundary condition definitions in the input file that
498    # are exodus specific. Also, set the geometry set IDs in the boundary conditions to
499    # the ones in the mesh representation.
500    node_set_id_input_file_to_mesh_representation = _create_inverse_mapping(
501        node_set_id_mesh_representation_to_input_file
502    )
503    for section_name in input_file.sections:
504        if section_name in _INPUT_FILE_MAPPINGS["boundary_conditions"].values():
505            items = input_file.pop(section_name)
506            for bc in items:
507                bc.pop("ENTITY_TYPE")
508                bc["E"] = node_set_id_input_file_to_mesh_representation[bc["E"]] + 1
509            input_file.add({section_name: items})
510
511    # Create the mesh representation
512    mesh_representation = _MeshRepresentation(
513        cell_connectivity=cell_connectivity,
514        cell_types=cell_types,
515        points=coordinates,
516        geometry_sets=geometry_sets,
517        point_data={"point_type": point_types},
518        cell_data={
519            "element_type_id": cell_element_type_ids,
520            "material_id": cell_material_ids,
521        },
522    )
523
524    return (
525        mesh_representation,
526        element_type_tracker.unique_id_to_data,
527        {i: i + 1 for i in range(len(node_set_id_mesh_representation_to_input_file))},
528    )
529
530
531def _create_mesh_from_mesh_representation(
532    input_file,
533    mesh_representation,
534    element_type_id_to_data,
535    node_set_id_mesh_representation_to_input_file,
536) -> tuple[_InputFile, _Mesh]:
537    """Extract a BeamMe mesh from a mesh representation.
538
539    Args:
540        input_file: The input file containing general data.
541        mesh_representation: The mesh representation to convert.
542        node_set_id_mesh_representation_to_input_file: A mapping of the mesh
543            representation node set IDs to input file IDs, which can be used to link
544            the geometry sets in the input file to the node sets in the mesh
545            representation.
546
547    Returns:
548        A tuple (input_file, mesh). The input_file is modified in place to remove
549        sections converted into the BeamMe mesh.
550    """
551    # convert all sections to native objects and add to a new mesh
552    mesh = _Mesh()
553
554    # extract materials
555    material_id_map = _extract_materials_from_input_file(input_file)
556    mesh.materials.extend(material_id_map.values())
557
558    # extract nodes
559    for node_coordinates, node_type in zip(
560        mesh_representation.points, mesh_representation.point_data["point_type"]
561    ):
562        if node_type == _bme.node_type.node.value:
563            mesh.nodes.append(_Node(node_coordinates))
564        else:
565            raise ValueError(
566                f"Mesh conversion for node type {_bme.node_type(node_type).name} is not implemented!"
567            )
568
569    # extract element types
570    element_type_id_to_element_type: dict[int, type[_Element]] = {}
571    for (
572        element_type_id,
573        element_data,
574    ) in element_type_id_to_data.items():
575        element_type, n_nodes = _INPUT_FILE_MAPPINGS[
576            "four_c_cell_to_element_type_and_n_nodes"
577        ][element_data.four_c_cell]
578        if not element_type == _bme.element_type.solid:
579            raise ValueError(
580                f"Mesh conversion for element type {element_type} is not implemented!"
581            )
582        element_type_id_to_element_type[element_type_id] = _get_four_c_solid(
583            element_type,
584            element_data.four_c_type,
585            n_nodes=n_nodes,
586            element_technology=element_data.element_technology,
587        )
588
589    # loop over the elements and create the mesh elements with the correct type, connectivity and material.
590    for connectivity, cell_element_type_id_, material_id in zip(
591        mesh_representation.connectivity_iterator(),
592        mesh_representation.cell_data["element_type_id"],
593        mesh_representation.cell_data["material_id"],
594    ):
595        element_type = element_type_id_to_element_type[cell_element_type_id_]
596
597        reorder_indices = _MESH_REPRESENTATION_MAPPINGS[
598            "element_type_and_n_nodes_to_connectivity_mapping_vtk_to_beamme"
599        ].get((element_type.element_type, len(connectivity)), None)
600        if reorder_indices is not None:
601            nodes = [mesh.nodes[connectivity[i]] for i in reorder_indices]
602        else:
603            nodes = [mesh.nodes[i] for i in connectivity]
604
605        element = element_type(nodes=nodes)
606
607        if not material_id == -1:
608            element.material = material_id_map[material_id]
609        mesh.elements.append(element)
610
611    # extract geometry sets
612    geometry_sets_in_sections: dict[_Geometry, dict[int, _GeometrySetNodes]] = (
613        _defaultdict(dict)
614    )
615    for name in mesh_representation.point_data.keys():
616        info = _string_to_geometry_set_info(name)
617        if info is not None:
618            node_indices = _np.nonzero(mesh_representation.point_data[name])[0]
619            geometry_type = info.geometry_type
620            geometry_set = _GeometrySetNodes(
621                geometry_type,
622                nodes=[mesh.nodes[i] for i in node_indices],
623                name=info.name,
624            )
625            input_file_id = node_set_id_mesh_representation_to_input_file[info.i_global]
626            geometry_sets_in_sections[geometry_type][input_file_id] = geometry_set
627            mesh.add(geometry_set)
628
629    # extract boundary conditions
630    _standard_bc_types = (
631        _bme.bc.dirichlet,
632        _bme.bc.neumann,
633        _bme.bc.locsys,
634        _bme.bc.beam_to_solid_surface_meshtying,
635        _bme.bc.beam_to_solid_surface_contact,
636        _bme.bc.beam_to_solid_volume_meshtying,
637    )
638
639    for (bc_key, geometry_type), section_name in _INPUT_FILE_MAPPINGS[
640        "boundary_conditions"
641    ].items():
642        for bc_data in input_file.pop(section_name, []):
643            geometry_set = geometry_sets_in_sections[geometry_type][bc_data.pop("E")]
644
645            bc_obj: _BoundaryConditionBase
646
647            if bc_key in _standard_bc_types or isinstance(bc_key, str):
648                bc_obj = _BoundaryCondition(geometry_set, bc_data, bc_type=bc_key)
649            elif bc_key is _bme.bc.point_coupling:
650                bc_obj = _Coupling(
651                    geometry_set, bc_key, bc_data, check_overlapping_nodes=False
652                )
653            else:
654                raise ValueError(f"Unexpected boundary condition: {bc_key}")
655
656            mesh.boundary_conditions.append((bc_key, geometry_type), bc_obj)
657
658    return input_file, mesh
659
660
661def _extract_materials_from_input_file(
662    input_file: _InputFile,
663) -> dict[int, _MaterialSolid]:
664    """Extract all materials from the input file and convert them to BeamMe materials.
665
666    Args:
667        input_file: The input file containing the material sections.
668
669    Returns:
670        A mapping of material IDs to BeamMe material objects.
671    """
672    material_id_map_all = {}
673
674    for mat in input_file.pop("MATERIALS", []):
675        mat_id = mat.pop("MAT") - 1
676        if len(mat) != 1:
677            raise ValueError(
678                f"Could not convert the material data `{mat}` to a BeamMe material!"
679            )
680        mat_name, mat_data = list(mat.items())[0]
681        material = _MaterialSolid(material_string=mat_name, data=mat_data)
682        material_id_map_all[mat_id] = material
683
684    nested_materials = set()
685    for material in material_id_map_all.values():
686        # Replace the integer IDs in the "MATIDS" list of the material with the actual
687        # material objects.
688        sub_materials = material.data.get("MATIDS", [])
689        sub_material_ids = _np.array(sub_materials) - 1
690        for i_sub_material, sub_material_id in enumerate(sub_material_ids):
691            try:
692                sub_materials[i_sub_material] = material_id_map_all[sub_material_id]
693            except KeyError as key_exception:
694                raise KeyError(
695                    f"Material ID {sub_material_id} not in material_id_map_all (available "
696                    f"IDs: {list(material_id_map_all.keys())})."
697                ) from key_exception
698            nested_materials.add(sub_material_id)
699
700    # Get a map of all non-nested materials. We assume that only those are used as
701    # materials for elements. Also, add the non-nested materials to the mesh.
702    material_id_map = {
703        key: val
704        for key, val in material_id_map_all.items()
705        if key not in nested_materials
706    }
707
708    return material_id_map
class UniqueDataTracker:
 77class UniqueDataTracker:
 78    """Helper class to track unique data dictionaries and assign IDs to them.
 79
 80    When importing input files, we need to identify elements of the same type. The type
 81    information is given in dictionaries. This class provides a tracker that can be
 82    queried with a given element data and return an already matching element type ID or
 83    create a new one.
 84    """
 85
 86    def __init__(self) -> None:
 87        self.unique_id_to_data: dict[int, _FourCElementData] = {}
 88
 89    def get_unique_id(self, data: _FourCElementData) -> int:
 90        """Get the unique ID for the given data.
 91
 92        If the data has not been seen before, a new ID will be assigned to it.
 93
 94        Args:
 95            data: The data dictionary to get the ID for.
 96
 97        Returns:
 98            The unique ID for the given data.
 99        """
100        for unique_id, seen_data in self.unique_id_to_data.items():
101            if data == seen_data:
102                return unique_id
103
104        # If we reach this point, the data has not been seen before. We assign a new ID to it.
105        new_unique_id = len(self.unique_id_to_data)
106        self.unique_id_to_data[new_unique_id] = data
107        return new_unique_id

Helper class to track unique data dictionaries and assign IDs to them.

When importing input files, we need to identify elements of the same type. The type information is given in dictionaries. This class provides a tracker that can be queried with a given element data and return an already matching element type ID or create a new one.

unique_id_to_data: dict[int, beamme.four_c.element_data.FourCElementData]
def get_unique_id(self, data: beamme.four_c.element_data.FourCElementData) -> int:
 89    def get_unique_id(self, data: _FourCElementData) -> int:
 90        """Get the unique ID for the given data.
 91
 92        If the data has not been seen before, a new ID will be assigned to it.
 93
 94        Args:
 95            data: The data dictionary to get the ID for.
 96
 97        Returns:
 98            The unique ID for the given data.
 99        """
100        for unique_id, seen_data in self.unique_id_to_data.items():
101            if data == seen_data:
102                return unique_id
103
104        # If we reach this point, the data has not been seen before. We assign a new ID to it.
105        new_unique_id = len(self.unique_id_to_data)
106        self.unique_id_to_data[new_unique_id] = data
107        return new_unique_id

Get the unique ID for the given data.

If the data has not been seen before, a new ID will be assigned to it.

Arguments:
  • data: The data dictionary to get the ID for.
Returns:

The unique ID for the given data.

def import_cubitpy_model( cubit, convert_input_to_mesh: bool = False) -> tuple[beamme.four_c.input_file.InputFile, beamme.core.mesh.Mesh]:
110def import_cubitpy_model(
111    cubit, convert_input_to_mesh: bool = False
112) -> tuple[_InputFile, _Mesh]:
113    """Convert a CubitPy instance to a BeamMe InputFile.
114
115    Args:
116        cubit (CubitPy): An instance of a cubit model.
117        convert_input_to_mesh: If this is false, the cubit model will be
118            converted to plain FourCIPP input data. If this is true, an input
119            file with all the parameters will be returned and a mesh which
120            contains the mesh information from cubit converted to BeamMe
121            objects.
122
123    Returns:
124        A tuple with the input file and the mesh. If convert_input_to_mesh is
125        False, the mesh will be empty. Note that the input sections which are
126        converted to a BeamMe mesh are removed from the input file object.
127    """
128    temp_dir: str | _Path
129    with _tempfile.TemporaryDirectory() as temp_dir:
130        temp_dir = _Path(temp_dir)
131        input_file_path = temp_dir / "temp_cubit_input_file.4C.yaml"
132        cubit.dump(
133            input_file_path, mesh_in_exo=True, mesh_in_exo_add_node_set_info=True
134        )
135        return import_four_c_model(
136            input_file_path, convert_input_to_mesh=convert_input_to_mesh
137        )

Convert a CubitPy instance to a BeamMe InputFile.

Arguments:
  • cubit (CubitPy): An instance of a cubit model.
  • convert_input_to_mesh: If this is false, the cubit model will be converted to plain FourCIPP input data. If this is true, an input file with all the parameters will be returned and a mesh which contains the mesh information from cubit converted to BeamMe objects.
Returns:

A tuple with the input file and the mesh. If convert_input_to_mesh is False, the mesh will be empty. Note that the input sections which are converted to a BeamMe mesh are removed from the input file object.

def import_four_c_model( input_file_path: pathlib.Path, convert_input_to_mesh: bool = False) -> tuple[beamme.four_c.input_file.InputFile, beamme.core.mesh.Mesh]:
140def import_four_c_model(
141    input_file_path: _Path, convert_input_to_mesh: bool = False
142) -> tuple[_InputFile, _Mesh]:
143    """Import an existing 4C input file and optionally convert it into a BeamMe mesh.
144
145    Args:
146        input_file_path: A file path to an existing 4C input file that will be
147            imported.
148        convert_input_to_mesh: If True, the input file will be converted to a
149            BeamMe mesh.
150
151    Returns:
152        A tuple with the input file and the mesh. If convert_input_to_mesh is
153        False, the mesh will be empty. Note that the input sections which are
154        converted to a BeamMe mesh are removed from the input file object.
155    """
156    input_file = _InputFile().from_4C_yaml(input_file_path=input_file_path)
157    base_path = input_file_path.parent
158
159    if input_file.contains_mesh_based_geometry_exodus():
160        (
161            mesh_representation,
162            element_type_id_to_data,
163            node_set_id_mesh_representation_to_input_file,
164        ) = _extract_mesh_representation_from_exo(input_file, base_path)
165
166    elif convert_input_to_mesh:
167        (
168            mesh_representation,
169            element_type_id_to_data,
170            node_set_id_mesh_representation_to_input_file,
171        ) = _extract_mesh_representation(input_file)
172
173    else:
174        mesh_representation = None
175        element_type_id_to_data = None
176
177    if convert_input_to_mesh:
178        return _create_mesh_from_mesh_representation(
179            input_file,
180            mesh_representation,
181            element_type_id_to_data,
182            node_set_id_mesh_representation_to_input_file,
183        )
184    else:
185        if mesh_representation is not None:
186            input_file.mesh_representation = mesh_representation
187        if element_type_id_to_data is not None:
188            input_file.element_type_id_to_data = element_type_id_to_data
189        return input_file, _Mesh()

Import an existing 4C input file and optionally convert it into a BeamMe mesh.

Arguments:
  • input_file_path: A file path to an existing 4C input file that will be imported.
  • convert_input_to_mesh: If True, the input file will be converted to a BeamMe mesh.
Returns:

A tuple with the input file and the mesh. If convert_input_to_mesh is False, the mesh will be empty. Note that the input sections which are converted to a BeamMe mesh are removed from the input file object.