beamme.four_c.input_file

This module defines the classes that are used to create an input file for 4C.

  1# The MIT License (MIT)
  2#
  3# Copyright (c) 2018-2025 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 defines the classes that are used to create an input file for
 234C."""
 24
 25import os as _os
 26import sys as _sys
 27from datetime import datetime as _datetime
 28from pathlib import Path as _Path
 29from typing import Callable as _Callable
 30from typing import Dict as _Dict
 31from typing import List as _List
 32
 33from fourcipp.fourc_input import FourCInput as _FourCInput
 34from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names
 35
 36from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER
 37from beamme.core.conf import bme as _bme
 38from beamme.core.function import Function as _Function
 39from beamme.core.material import Material as _Material
 40from beamme.core.mesh import Mesh as _Mesh
 41from beamme.core.node import Node as _Node
 42from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch
 43from beamme.four_c.input_file_dump_item import dump_item_to_list as _dump_item_to_list
 44from beamme.four_c.input_file_dump_item import (
 45    dump_item_to_section as _dump_item_to_section,
 46)
 47from beamme.four_c.input_file_mappings import (
 48    INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS,
 49)
 50from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
 51from beamme.utils.environment import get_git_data as _get_git_data
 52
 53if _cubitpy_is_available():
 54    import cubitpy as _cubitpy
 55
 56
 57def get_geometry_set_indices_from_section(
 58    section_list: _List, *, append_node_ids: bool = True
 59) -> _Dict:
 60    """Return a dictionary with the geometry set ID as keys and the node IDs as
 61    values.
 62
 63    Args:
 64        section_list: A list with the legacy strings for the geometry pair
 65        append_node_ids: If the node IDs shall be appended, or only the
 66            dict with the keys should be returned.
 67    """
 68
 69    geometry_set_dict: _Dict[int, _List[int]] = {}
 70    for entry in section_list:
 71        id_geometry_set = entry["d_id"]
 72        index_node = entry["node_id"] - 1
 73        if id_geometry_set not in geometry_set_dict:
 74            geometry_set_dict[id_geometry_set] = []
 75        if append_node_ids:
 76            geometry_set_dict[id_geometry_set].append(index_node)
 77
 78    return geometry_set_dict
 79
 80
 81class InputFile(_FourCInput):
 82    """An item that represents a complete 4C input file."""
 83
 84    def __init__(self, sections=None):
 85        """Initialize the input file."""
 86
 87        super().__init__(sections=sections)
 88
 89        # Contents of NOX xml file.
 90        self.nox_xml_contents = ""
 91
 92        # Register converters to directly convert non-primitive types
 93        # to native Python types via the FourCIPP type converter.
 94        self.type_converter.register_numpy_types()
 95        self.type_converter.register_type(
 96            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
 97        )
 98
 99    def add(self, object_to_add, **kwargs):
100        """Add a mesh or a dictionary to the input file.
101
102        Args:
103            object: The object to be added. This can be a mesh or a dictionary.
104            **kwargs: Additional arguments to be passed to the add method.
105        """
106
107        if isinstance(object_to_add, _Mesh):
108            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
109
110        else:
111            super().combine_sections(object_to_add)
112
113    def dump(
114        self,
115        input_file_path: str | _Path,
116        *,
117        nox_xml_file: str | None = None,
118        add_header_default: bool = True,
119        add_header_information: bool = True,
120        add_footer_application_script: bool = True,
121        validate=True,
122        validate_sections_only: bool = False,
123        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
124        fourcipp_yaml_style: bool = True,
125    ):
126        """Write the input file to disk.
127
128        Args:
129            input_file_path:
130                Path to the input file that should be created.
131            nox_xml_file:
132                If this is a string, the NOX xml file will be created with this
133                name. If this is None, the NOX xml file will be created with the
134                name of the input file with the extension ".nox.xml".
135            add_header_default:
136                Prepend the default header comment to the input file.
137            add_header_information:
138                If the information header should be exported to the input file
139                Contains creation date, git details of BeamMe, CubitPy and
140                original application which created the input file if available.
141            add_footer_application_script:
142                Append the application script which creates the input files as a
143                comment at the end of the input file.
144            validate:
145                Validate if the created input file is compatible with 4C with FourCIPP.
146            validate_sections_only:
147                Validate each section independently. Required sections are no longer
148                required, but the sections must be valid.
149            sort_function:
150                A function which sorts the sections of the input file.
151            fourcipp_yaml_style:
152                If True, the input file is written in the fourcipp yaml style.
153        """
154
155        # Make sure the given input file is a Path instance.
156        input_file_path = _Path(input_file_path)
157
158        if self.nox_xml_contents:
159            if nox_xml_file is None:
160                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
161
162            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
163
164            # Write the xml file to the disc.
165            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
166                xml_file.write(self.nox_xml_contents)
167
168        # Add information header to the input file
169        if add_header_information:
170            self.add({"TITLE": self._get_header()})
171
172        super().dump(
173            input_file_path=input_file_path,
174            validate=validate,
175            validate_sections_only=validate_sections_only,
176            convert_to_native_types=False,  # conversion already happens during add()
177            sort_function=sort_function,
178            use_fourcipp_yaml_style=fourcipp_yaml_style,
179        )
180
181        if add_header_default or add_footer_application_script:
182            with open(input_file_path, "r") as input_file:
183                lines = input_file.readlines()
184
185                if add_header_default:
186                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
187
188                if add_footer_application_script:
189                    application_path = _Path(_sys.argv[0]).resolve()
190                    lines += self._get_application_script(application_path)
191
192                with open(input_file_path, "w") as input_file:
193                    input_file.writelines(lines)
194
195    def add_mesh_to_input_file(self, mesh: _Mesh):
196        """Add a mesh to the input file.
197
198        Args:
199            mesh: The mesh to be added to the input file.
200        """
201
202        # Perform some checks on the mesh.
203        if _bme.check_overlapping_elements:
204            mesh.check_overlapping_elements()
205
206        def _get_global_start_geometry_set(dictionary):
207            """Get the indices for the first "real" BeamMe geometry sets."""
208
209            start_indices_geometry_set = {}
210            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
211                "geometry_sets_geometry_to_condition_name"
212            ].items():
213                max_geometry_set_id = 0
214                if section_name in dictionary:
215                    section_list = dictionary[section_name]
216                    if len(section_list) > 0:
217                        geometry_set_dict = get_geometry_set_indices_from_section(
218                            section_list, append_node_ids=False
219                        )
220                        max_geometry_set_id = max(geometry_set_dict.keys())
221                start_indices_geometry_set[geometry_type] = max_geometry_set_id
222            return start_indices_geometry_set
223
224        def _get_global_start_node():
225            """Get the index for the first "real" BeamMe node."""
226
227            return len(self.sections.get("NODE COORDS", []))
228
229        def _get_global_start_element():
230            """Get the index for the first "real" BeamMe element."""
231
232            return sum(
233                len(self.sections.get(section, []))
234                for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"]
235            )
236
237        def _get_global_start_material():
238            """Get the index for the first "real" BeamMe material.
239
240            We have to account for materials imported from yaml files
241            that have arbitrary numbering.
242            """
243
244            # Get the maximum material index in materials imported from a yaml file
245            max_material_id = 0
246            section_name = "MATERIALS"
247            if section_name in self.sections:
248                for material in self.sections[section_name]:
249                    max_material_id = max(max_material_id, material["MAT"])
250            return max_material_id
251
252        def _get_global_start_function():
253            """Get the index for the first "real" BeamMe function."""
254
255            max_function_id = 0
256            for section_name in self.sections.keys():
257                if section_name.startswith("FUNCT"):
258                    max_function_id = max(
259                        max_function_id, int(section_name.split("FUNCT")[-1])
260                    )
261            return max_function_id
262
263        def _set_i_global(data_list, *, start_index=0):
264            """Set i_global in every item of data_list."""
265
266            # A check is performed that every entry in data_list is unique.
267            if len(data_list) != len(set(data_list)):
268                raise ValueError("Elements in data_list are not unique!")
269
270            # Set the values for i_global.
271            for i, item in enumerate(data_list):
272                item.i_global = i + start_index
273
274        def _set_i_global_elements(element_list, *, start_index=0):
275            """Set i_global in every item of element_list."""
276
277            # A check is performed that every entry in element_list is unique.
278            if len(element_list) != len(set(element_list)):
279                raise ValueError("Elements in element_list are not unique!")
280
281            # Set the values for i_global.
282            i = start_index
283            i_nurbs_patch = 0
284            for item in element_list:
285                # As a NURBS patch can be defined with more elements, an offset is applied to the
286                # rest of the items
287                item.i_global = i
288                if isinstance(item, _NURBSPatch):
289                    item.i_nurbs_patch = i_nurbs_patch
290                    offset = item.get_number_elements()
291                    i += offset
292                    i_nurbs_patch += 1
293                else:
294                    i += 1
295
296        def _dump_mesh_items(section_name, data_list):
297            """Output a section name and apply either the default dump or the
298            specialized the dump_to_list for each list item."""
299
300            # Do not write section if no content is available
301            if len(data_list) == 0:
302                return
303
304            list = []
305
306            for item in data_list:
307                _dump_item_to_list(list, item)
308
309            # If section already exists, retrieve from input file and
310            # add newly. We always need to go through fourcipp to convert
311            # the data types correctly.
312            if section_name in self.sections:
313                existing_entries = self.pop(section_name)
314                existing_entries.extend(list)
315                list = existing_entries
316
317            self.add({section_name: list})
318
319        # Add sets from couplings and boundary conditions to a temp container.
320        mesh.unlink_nodes()
321        start_indices_geometry_set = _get_global_start_geometry_set(self.sections)
322        mesh_sets = mesh.get_unique_geometry_sets(
323            geometry_set_start_indices=start_indices_geometry_set
324        )
325
326        # Assign global indices to all entries.
327        start_index_nodes = _get_global_start_node()
328        _set_i_global(mesh.nodes, start_index=start_index_nodes)
329
330        start_index_elements = _get_global_start_element()
331        _set_i_global_elements(mesh.elements, start_index=start_index_elements)
332
333        start_index_materials = _get_global_start_material()
334        _set_i_global(mesh.materials, start_index=start_index_materials)
335
336        start_index_functions = _get_global_start_function()
337        _set_i_global(mesh.functions, start_index=start_index_functions)
338
339        # Add material data to the input file.
340        _dump_mesh_items("MATERIALS", mesh.materials)
341
342        # Add the functions.
343        for function in mesh.functions:
344            self.add({f"FUNCT{function.i_global + 1}": function.data})
345
346        # If there are couplings in the mesh, set the link between the nodes
347        # and elements, so the couplings can decide which DOFs they couple,
348        # depending on the type of the connected beam element.
349        def get_number_of_coupling_conditions(key):
350            """Return the number of coupling conditions in the mesh."""
351            if (key, _bme.geo.point) in mesh.boundary_conditions.keys():
352                return len(mesh.boundary_conditions[key, _bme.geo.point])
353            else:
354                return 0
355
356        if (
357            get_number_of_coupling_conditions(_bme.bc.point_coupling)
358            + get_number_of_coupling_conditions(_bme.bc.point_coupling_penalty)
359            > 0
360        ):
361            mesh.set_node_links()
362
363        # Add the boundary conditions.
364        for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items():
365            if len(bc_list) > 0:
366                section_name = (
367                    bc_key
368                    if isinstance(bc_key, str)
369                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)]
370                )
371                _dump_mesh_items(section_name, bc_list)
372
373        # Add additional element sections, e.g., for NURBS knot vectors.
374        for element in mesh.elements:
375            _dump_item_to_section(self, element)
376
377        # Add the geometry sets.
378        for geom_key, item in mesh_sets.items():
379            if len(item) > 0:
380                _dump_mesh_items(
381                    _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
382                        geom_key
383                    ],
384                    item,
385                )
386
387        # Add the nodes and elements.
388        _dump_mesh_items("NODE COORDS", mesh.nodes)
389        _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements)
390        # TODO: reset all links and counters set in this method.
391
392    def _get_header(self) -> dict:
393        """Return the information header for the current BeamMe run.
394
395        Returns:
396            A dictionary with the header information.
397        """
398
399        header: dict = {"BeamMe": {}}
400
401        header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
402            sep=" ", timespec="seconds"
403        )
404
405        # application which created the input file
406        application_path = _Path(_sys.argv[0]).resolve()
407        header["BeamMe"]["Application"] = {"path": str(application_path)}
408
409        application_git_sha, application_git_date = _get_git_data(
410            application_path.parent
411        )
412        if application_git_sha is not None and application_git_date is not None:
413            header["BeamMe"]["Application"].update(
414                {
415                    "git_sha": application_git_sha,
416                    "git_date": application_git_date,
417                }
418            )
419
420        # BeamMe information
421        beamme_git_sha, beamme_git_date = _get_git_data(
422            _Path(__file__).resolve().parent
423        )
424        if beamme_git_sha is not None and beamme_git_date is not None:
425            header["BeamMe"]["BeamMe"] = {
426                "git_SHA": beamme_git_sha,
427                "git_date": beamme_git_date,
428            }
429
430        # CubitPy information
431        if _cubitpy_is_available():
432            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
433                _os.path.dirname(_cubitpy.__file__)
434            )
435
436            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
437                header["BeamMe"]["CubitPy"] = {
438                    "git_SHA": cubitpy_git_sha,
439                    "git_date": cubitpy_git_date,
440                }
441
442        return header
443
444    def _get_application_script(self, application_path: _Path) -> list[str]:
445        """Get the script that created this input file.
446
447        Args:
448            application_path: Path to the script that created this input file.
449        Returns:
450            A list of strings with the script that created this input file.
451        """
452
453        application_script_lines = [
454            "# Application script which created this input file:\n"
455        ]
456
457        with open(application_path) as script_file:
458            application_script_lines.extend("# " + line for line in script_file)
459
460        return application_script_lines
def get_geometry_set_indices_from_section(section_list: List, *, append_node_ids: bool = True) -> Dict:
58def get_geometry_set_indices_from_section(
59    section_list: _List, *, append_node_ids: bool = True
60) -> _Dict:
61    """Return a dictionary with the geometry set ID as keys and the node IDs as
62    values.
63
64    Args:
65        section_list: A list with the legacy strings for the geometry pair
66        append_node_ids: If the node IDs shall be appended, or only the
67            dict with the keys should be returned.
68    """
69
70    geometry_set_dict: _Dict[int, _List[int]] = {}
71    for entry in section_list:
72        id_geometry_set = entry["d_id"]
73        index_node = entry["node_id"] - 1
74        if id_geometry_set not in geometry_set_dict:
75            geometry_set_dict[id_geometry_set] = []
76        if append_node_ids:
77            geometry_set_dict[id_geometry_set].append(index_node)
78
79    return geometry_set_dict

Return a dictionary with the geometry set ID as keys and the node IDs as values.

Arguments:
  • section_list: A list with the legacy strings for the geometry pair
  • append_node_ids: If the node IDs shall be appended, or only the dict with the keys should be returned.
class InputFile(fourcipp.fourc_input.FourCInput):
 82class InputFile(_FourCInput):
 83    """An item that represents a complete 4C input file."""
 84
 85    def __init__(self, sections=None):
 86        """Initialize the input file."""
 87
 88        super().__init__(sections=sections)
 89
 90        # Contents of NOX xml file.
 91        self.nox_xml_contents = ""
 92
 93        # Register converters to directly convert non-primitive types
 94        # to native Python types via the FourCIPP type converter.
 95        self.type_converter.register_numpy_types()
 96        self.type_converter.register_type(
 97            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
 98        )
 99
100    def add(self, object_to_add, **kwargs):
101        """Add a mesh or a dictionary to the input file.
102
103        Args:
104            object: The object to be added. This can be a mesh or a dictionary.
105            **kwargs: Additional arguments to be passed to the add method.
106        """
107
108        if isinstance(object_to_add, _Mesh):
109            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
110
111        else:
112            super().combine_sections(object_to_add)
113
114    def dump(
115        self,
116        input_file_path: str | _Path,
117        *,
118        nox_xml_file: str | None = None,
119        add_header_default: bool = True,
120        add_header_information: bool = True,
121        add_footer_application_script: bool = True,
122        validate=True,
123        validate_sections_only: bool = False,
124        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
125        fourcipp_yaml_style: bool = True,
126    ):
127        """Write the input file to disk.
128
129        Args:
130            input_file_path:
131                Path to the input file that should be created.
132            nox_xml_file:
133                If this is a string, the NOX xml file will be created with this
134                name. If this is None, the NOX xml file will be created with the
135                name of the input file with the extension ".nox.xml".
136            add_header_default:
137                Prepend the default header comment to the input file.
138            add_header_information:
139                If the information header should be exported to the input file
140                Contains creation date, git details of BeamMe, CubitPy and
141                original application which created the input file if available.
142            add_footer_application_script:
143                Append the application script which creates the input files as a
144                comment at the end of the input file.
145            validate:
146                Validate if the created input file is compatible with 4C with FourCIPP.
147            validate_sections_only:
148                Validate each section independently. Required sections are no longer
149                required, but the sections must be valid.
150            sort_function:
151                A function which sorts the sections of the input file.
152            fourcipp_yaml_style:
153                If True, the input file is written in the fourcipp yaml style.
154        """
155
156        # Make sure the given input file is a Path instance.
157        input_file_path = _Path(input_file_path)
158
159        if self.nox_xml_contents:
160            if nox_xml_file is None:
161                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
162
163            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
164
165            # Write the xml file to the disc.
166            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
167                xml_file.write(self.nox_xml_contents)
168
169        # Add information header to the input file
170        if add_header_information:
171            self.add({"TITLE": self._get_header()})
172
173        super().dump(
174            input_file_path=input_file_path,
175            validate=validate,
176            validate_sections_only=validate_sections_only,
177            convert_to_native_types=False,  # conversion already happens during add()
178            sort_function=sort_function,
179            use_fourcipp_yaml_style=fourcipp_yaml_style,
180        )
181
182        if add_header_default or add_footer_application_script:
183            with open(input_file_path, "r") as input_file:
184                lines = input_file.readlines()
185
186                if add_header_default:
187                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
188
189                if add_footer_application_script:
190                    application_path = _Path(_sys.argv[0]).resolve()
191                    lines += self._get_application_script(application_path)
192
193                with open(input_file_path, "w") as input_file:
194                    input_file.writelines(lines)
195
196    def add_mesh_to_input_file(self, mesh: _Mesh):
197        """Add a mesh to the input file.
198
199        Args:
200            mesh: The mesh to be added to the input file.
201        """
202
203        # Perform some checks on the mesh.
204        if _bme.check_overlapping_elements:
205            mesh.check_overlapping_elements()
206
207        def _get_global_start_geometry_set(dictionary):
208            """Get the indices for the first "real" BeamMe geometry sets."""
209
210            start_indices_geometry_set = {}
211            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
212                "geometry_sets_geometry_to_condition_name"
213            ].items():
214                max_geometry_set_id = 0
215                if section_name in dictionary:
216                    section_list = dictionary[section_name]
217                    if len(section_list) > 0:
218                        geometry_set_dict = get_geometry_set_indices_from_section(
219                            section_list, append_node_ids=False
220                        )
221                        max_geometry_set_id = max(geometry_set_dict.keys())
222                start_indices_geometry_set[geometry_type] = max_geometry_set_id
223            return start_indices_geometry_set
224
225        def _get_global_start_node():
226            """Get the index for the first "real" BeamMe node."""
227
228            return len(self.sections.get("NODE COORDS", []))
229
230        def _get_global_start_element():
231            """Get the index for the first "real" BeamMe element."""
232
233            return sum(
234                len(self.sections.get(section, []))
235                for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"]
236            )
237
238        def _get_global_start_material():
239            """Get the index for the first "real" BeamMe material.
240
241            We have to account for materials imported from yaml files
242            that have arbitrary numbering.
243            """
244
245            # Get the maximum material index in materials imported from a yaml file
246            max_material_id = 0
247            section_name = "MATERIALS"
248            if section_name in self.sections:
249                for material in self.sections[section_name]:
250                    max_material_id = max(max_material_id, material["MAT"])
251            return max_material_id
252
253        def _get_global_start_function():
254            """Get the index for the first "real" BeamMe function."""
255
256            max_function_id = 0
257            for section_name in self.sections.keys():
258                if section_name.startswith("FUNCT"):
259                    max_function_id = max(
260                        max_function_id, int(section_name.split("FUNCT")[-1])
261                    )
262            return max_function_id
263
264        def _set_i_global(data_list, *, start_index=0):
265            """Set i_global in every item of data_list."""
266
267            # A check is performed that every entry in data_list is unique.
268            if len(data_list) != len(set(data_list)):
269                raise ValueError("Elements in data_list are not unique!")
270
271            # Set the values for i_global.
272            for i, item in enumerate(data_list):
273                item.i_global = i + start_index
274
275        def _set_i_global_elements(element_list, *, start_index=0):
276            """Set i_global in every item of element_list."""
277
278            # A check is performed that every entry in element_list is unique.
279            if len(element_list) != len(set(element_list)):
280                raise ValueError("Elements in element_list are not unique!")
281
282            # Set the values for i_global.
283            i = start_index
284            i_nurbs_patch = 0
285            for item in element_list:
286                # As a NURBS patch can be defined with more elements, an offset is applied to the
287                # rest of the items
288                item.i_global = i
289                if isinstance(item, _NURBSPatch):
290                    item.i_nurbs_patch = i_nurbs_patch
291                    offset = item.get_number_elements()
292                    i += offset
293                    i_nurbs_patch += 1
294                else:
295                    i += 1
296
297        def _dump_mesh_items(section_name, data_list):
298            """Output a section name and apply either the default dump or the
299            specialized the dump_to_list for each list item."""
300
301            # Do not write section if no content is available
302            if len(data_list) == 0:
303                return
304
305            list = []
306
307            for item in data_list:
308                _dump_item_to_list(list, item)
309
310            # If section already exists, retrieve from input file and
311            # add newly. We always need to go through fourcipp to convert
312            # the data types correctly.
313            if section_name in self.sections:
314                existing_entries = self.pop(section_name)
315                existing_entries.extend(list)
316                list = existing_entries
317
318            self.add({section_name: list})
319
320        # Add sets from couplings and boundary conditions to a temp container.
321        mesh.unlink_nodes()
322        start_indices_geometry_set = _get_global_start_geometry_set(self.sections)
323        mesh_sets = mesh.get_unique_geometry_sets(
324            geometry_set_start_indices=start_indices_geometry_set
325        )
326
327        # Assign global indices to all entries.
328        start_index_nodes = _get_global_start_node()
329        _set_i_global(mesh.nodes, start_index=start_index_nodes)
330
331        start_index_elements = _get_global_start_element()
332        _set_i_global_elements(mesh.elements, start_index=start_index_elements)
333
334        start_index_materials = _get_global_start_material()
335        _set_i_global(mesh.materials, start_index=start_index_materials)
336
337        start_index_functions = _get_global_start_function()
338        _set_i_global(mesh.functions, start_index=start_index_functions)
339
340        # Add material data to the input file.
341        _dump_mesh_items("MATERIALS", mesh.materials)
342
343        # Add the functions.
344        for function in mesh.functions:
345            self.add({f"FUNCT{function.i_global + 1}": function.data})
346
347        # If there are couplings in the mesh, set the link between the nodes
348        # and elements, so the couplings can decide which DOFs they couple,
349        # depending on the type of the connected beam element.
350        def get_number_of_coupling_conditions(key):
351            """Return the number of coupling conditions in the mesh."""
352            if (key, _bme.geo.point) in mesh.boundary_conditions.keys():
353                return len(mesh.boundary_conditions[key, _bme.geo.point])
354            else:
355                return 0
356
357        if (
358            get_number_of_coupling_conditions(_bme.bc.point_coupling)
359            + get_number_of_coupling_conditions(_bme.bc.point_coupling_penalty)
360            > 0
361        ):
362            mesh.set_node_links()
363
364        # Add the boundary conditions.
365        for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items():
366            if len(bc_list) > 0:
367                section_name = (
368                    bc_key
369                    if isinstance(bc_key, str)
370                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)]
371                )
372                _dump_mesh_items(section_name, bc_list)
373
374        # Add additional element sections, e.g., for NURBS knot vectors.
375        for element in mesh.elements:
376            _dump_item_to_section(self, element)
377
378        # Add the geometry sets.
379        for geom_key, item in mesh_sets.items():
380            if len(item) > 0:
381                _dump_mesh_items(
382                    _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
383                        geom_key
384                    ],
385                    item,
386                )
387
388        # Add the nodes and elements.
389        _dump_mesh_items("NODE COORDS", mesh.nodes)
390        _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements)
391        # TODO: reset all links and counters set in this method.
392
393    def _get_header(self) -> dict:
394        """Return the information header for the current BeamMe run.
395
396        Returns:
397            A dictionary with the header information.
398        """
399
400        header: dict = {"BeamMe": {}}
401
402        header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
403            sep=" ", timespec="seconds"
404        )
405
406        # application which created the input file
407        application_path = _Path(_sys.argv[0]).resolve()
408        header["BeamMe"]["Application"] = {"path": str(application_path)}
409
410        application_git_sha, application_git_date = _get_git_data(
411            application_path.parent
412        )
413        if application_git_sha is not None and application_git_date is not None:
414            header["BeamMe"]["Application"].update(
415                {
416                    "git_sha": application_git_sha,
417                    "git_date": application_git_date,
418                }
419            )
420
421        # BeamMe information
422        beamme_git_sha, beamme_git_date = _get_git_data(
423            _Path(__file__).resolve().parent
424        )
425        if beamme_git_sha is not None and beamme_git_date is not None:
426            header["BeamMe"]["BeamMe"] = {
427                "git_SHA": beamme_git_sha,
428                "git_date": beamme_git_date,
429            }
430
431        # CubitPy information
432        if _cubitpy_is_available():
433            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
434                _os.path.dirname(_cubitpy.__file__)
435            )
436
437            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
438                header["BeamMe"]["CubitPy"] = {
439                    "git_SHA": cubitpy_git_sha,
440                    "git_date": cubitpy_git_date,
441                }
442
443        return header
444
445    def _get_application_script(self, application_path: _Path) -> list[str]:
446        """Get the script that created this input file.
447
448        Args:
449            application_path: Path to the script that created this input file.
450        Returns:
451            A list of strings with the script that created this input file.
452        """
453
454        application_script_lines = [
455            "# Application script which created this input file:\n"
456        ]
457
458        with open(application_path) as script_file:
459            application_script_lines.extend("# " + line for line in script_file)
460
461        return application_script_lines

An item that represents a complete 4C input file.

InputFile(sections=None)
85    def __init__(self, sections=None):
86        """Initialize the input file."""
87
88        super().__init__(sections=sections)
89
90        # Contents of NOX xml file.
91        self.nox_xml_contents = ""
92
93        # Register converters to directly convert non-primitive types
94        # to native Python types via the FourCIPP type converter.
95        self.type_converter.register_numpy_types()
96        self.type_converter.register_type(
97            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
98        )

Initialize the input file.

nox_xml_contents
def add(self, object_to_add, **kwargs):
100    def add(self, object_to_add, **kwargs):
101        """Add a mesh or a dictionary to the input file.
102
103        Args:
104            object: The object to be added. This can be a mesh or a dictionary.
105            **kwargs: Additional arguments to be passed to the add method.
106        """
107
108        if isinstance(object_to_add, _Mesh):
109            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
110
111        else:
112            super().combine_sections(object_to_add)

Add a mesh or a dictionary to the input file.

Arguments:
  • object: The object to be added. This can be a mesh or a dictionary.
  • **kwargs: Additional arguments to be passed to the add method.
def dump( self, input_file_path: str | pathlib.Path, *, nox_xml_file: str | None = None, add_header_default: bool = True, add_header_information: bool = True, add_footer_application_script: bool = True, validate=True, validate_sections_only: bool = False, sort_function: Optional[Callable[[dict], dict]] = <function sort_by_section_names>, fourcipp_yaml_style: bool = True):
114    def dump(
115        self,
116        input_file_path: str | _Path,
117        *,
118        nox_xml_file: str | None = None,
119        add_header_default: bool = True,
120        add_header_information: bool = True,
121        add_footer_application_script: bool = True,
122        validate=True,
123        validate_sections_only: bool = False,
124        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
125        fourcipp_yaml_style: bool = True,
126    ):
127        """Write the input file to disk.
128
129        Args:
130            input_file_path:
131                Path to the input file that should be created.
132            nox_xml_file:
133                If this is a string, the NOX xml file will be created with this
134                name. If this is None, the NOX xml file will be created with the
135                name of the input file with the extension ".nox.xml".
136            add_header_default:
137                Prepend the default header comment to the input file.
138            add_header_information:
139                If the information header should be exported to the input file
140                Contains creation date, git details of BeamMe, CubitPy and
141                original application which created the input file if available.
142            add_footer_application_script:
143                Append the application script which creates the input files as a
144                comment at the end of the input file.
145            validate:
146                Validate if the created input file is compatible with 4C with FourCIPP.
147            validate_sections_only:
148                Validate each section independently. Required sections are no longer
149                required, but the sections must be valid.
150            sort_function:
151                A function which sorts the sections of the input file.
152            fourcipp_yaml_style:
153                If True, the input file is written in the fourcipp yaml style.
154        """
155
156        # Make sure the given input file is a Path instance.
157        input_file_path = _Path(input_file_path)
158
159        if self.nox_xml_contents:
160            if nox_xml_file is None:
161                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
162
163            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
164
165            # Write the xml file to the disc.
166            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
167                xml_file.write(self.nox_xml_contents)
168
169        # Add information header to the input file
170        if add_header_information:
171            self.add({"TITLE": self._get_header()})
172
173        super().dump(
174            input_file_path=input_file_path,
175            validate=validate,
176            validate_sections_only=validate_sections_only,
177            convert_to_native_types=False,  # conversion already happens during add()
178            sort_function=sort_function,
179            use_fourcipp_yaml_style=fourcipp_yaml_style,
180        )
181
182        if add_header_default or add_footer_application_script:
183            with open(input_file_path, "r") as input_file:
184                lines = input_file.readlines()
185
186                if add_header_default:
187                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
188
189                if add_footer_application_script:
190                    application_path = _Path(_sys.argv[0]).resolve()
191                    lines += self._get_application_script(application_path)
192
193                with open(input_file_path, "w") as input_file:
194                    input_file.writelines(lines)

Write the input file to disk.

Arguments:
  • input_file_path: Path to the input file that should be created.
  • nox_xml_file: If this is a string, the NOX xml file will be created with this name. If this is None, the NOX xml file will be created with the name of the input file with the extension ".nox.xml".
  • add_header_default: Prepend the default header comment to the input file.
  • add_header_information: If the information header should be exported to the input file Contains creation date, git details of BeamMe, CubitPy and original application which created the input file if available.
  • add_footer_application_script: Append the application script which creates the input files as a comment at the end of the input file.
  • validate: Validate if the created input file is compatible with 4C with FourCIPP.
  • validate_sections_only: Validate each section independently. Required sections are no longer required, but the sections must be valid.
  • sort_function: A function which sorts the sections of the input file.
  • fourcipp_yaml_style: If True, the input file is written in the fourcipp yaml style.
def add_mesh_to_input_file(self, mesh: beamme.core.mesh.Mesh):
196    def add_mesh_to_input_file(self, mesh: _Mesh):
197        """Add a mesh to the input file.
198
199        Args:
200            mesh: The mesh to be added to the input file.
201        """
202
203        # Perform some checks on the mesh.
204        if _bme.check_overlapping_elements:
205            mesh.check_overlapping_elements()
206
207        def _get_global_start_geometry_set(dictionary):
208            """Get the indices for the first "real" BeamMe geometry sets."""
209
210            start_indices_geometry_set = {}
211            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
212                "geometry_sets_geometry_to_condition_name"
213            ].items():
214                max_geometry_set_id = 0
215                if section_name in dictionary:
216                    section_list = dictionary[section_name]
217                    if len(section_list) > 0:
218                        geometry_set_dict = get_geometry_set_indices_from_section(
219                            section_list, append_node_ids=False
220                        )
221                        max_geometry_set_id = max(geometry_set_dict.keys())
222                start_indices_geometry_set[geometry_type] = max_geometry_set_id
223            return start_indices_geometry_set
224
225        def _get_global_start_node():
226            """Get the index for the first "real" BeamMe node."""
227
228            return len(self.sections.get("NODE COORDS", []))
229
230        def _get_global_start_element():
231            """Get the index for the first "real" BeamMe element."""
232
233            return sum(
234                len(self.sections.get(section, []))
235                for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"]
236            )
237
238        def _get_global_start_material():
239            """Get the index for the first "real" BeamMe material.
240
241            We have to account for materials imported from yaml files
242            that have arbitrary numbering.
243            """
244
245            # Get the maximum material index in materials imported from a yaml file
246            max_material_id = 0
247            section_name = "MATERIALS"
248            if section_name in self.sections:
249                for material in self.sections[section_name]:
250                    max_material_id = max(max_material_id, material["MAT"])
251            return max_material_id
252
253        def _get_global_start_function():
254            """Get the index for the first "real" BeamMe function."""
255
256            max_function_id = 0
257            for section_name in self.sections.keys():
258                if section_name.startswith("FUNCT"):
259                    max_function_id = max(
260                        max_function_id, int(section_name.split("FUNCT")[-1])
261                    )
262            return max_function_id
263
264        def _set_i_global(data_list, *, start_index=0):
265            """Set i_global in every item of data_list."""
266
267            # A check is performed that every entry in data_list is unique.
268            if len(data_list) != len(set(data_list)):
269                raise ValueError("Elements in data_list are not unique!")
270
271            # Set the values for i_global.
272            for i, item in enumerate(data_list):
273                item.i_global = i + start_index
274
275        def _set_i_global_elements(element_list, *, start_index=0):
276            """Set i_global in every item of element_list."""
277
278            # A check is performed that every entry in element_list is unique.
279            if len(element_list) != len(set(element_list)):
280                raise ValueError("Elements in element_list are not unique!")
281
282            # Set the values for i_global.
283            i = start_index
284            i_nurbs_patch = 0
285            for item in element_list:
286                # As a NURBS patch can be defined with more elements, an offset is applied to the
287                # rest of the items
288                item.i_global = i
289                if isinstance(item, _NURBSPatch):
290                    item.i_nurbs_patch = i_nurbs_patch
291                    offset = item.get_number_elements()
292                    i += offset
293                    i_nurbs_patch += 1
294                else:
295                    i += 1
296
297        def _dump_mesh_items(section_name, data_list):
298            """Output a section name and apply either the default dump or the
299            specialized the dump_to_list for each list item."""
300
301            # Do not write section if no content is available
302            if len(data_list) == 0:
303                return
304
305            list = []
306
307            for item in data_list:
308                _dump_item_to_list(list, item)
309
310            # If section already exists, retrieve from input file and
311            # add newly. We always need to go through fourcipp to convert
312            # the data types correctly.
313            if section_name in self.sections:
314                existing_entries = self.pop(section_name)
315                existing_entries.extend(list)
316                list = existing_entries
317
318            self.add({section_name: list})
319
320        # Add sets from couplings and boundary conditions to a temp container.
321        mesh.unlink_nodes()
322        start_indices_geometry_set = _get_global_start_geometry_set(self.sections)
323        mesh_sets = mesh.get_unique_geometry_sets(
324            geometry_set_start_indices=start_indices_geometry_set
325        )
326
327        # Assign global indices to all entries.
328        start_index_nodes = _get_global_start_node()
329        _set_i_global(mesh.nodes, start_index=start_index_nodes)
330
331        start_index_elements = _get_global_start_element()
332        _set_i_global_elements(mesh.elements, start_index=start_index_elements)
333
334        start_index_materials = _get_global_start_material()
335        _set_i_global(mesh.materials, start_index=start_index_materials)
336
337        start_index_functions = _get_global_start_function()
338        _set_i_global(mesh.functions, start_index=start_index_functions)
339
340        # Add material data to the input file.
341        _dump_mesh_items("MATERIALS", mesh.materials)
342
343        # Add the functions.
344        for function in mesh.functions:
345            self.add({f"FUNCT{function.i_global + 1}": function.data})
346
347        # If there are couplings in the mesh, set the link between the nodes
348        # and elements, so the couplings can decide which DOFs they couple,
349        # depending on the type of the connected beam element.
350        def get_number_of_coupling_conditions(key):
351            """Return the number of coupling conditions in the mesh."""
352            if (key, _bme.geo.point) in mesh.boundary_conditions.keys():
353                return len(mesh.boundary_conditions[key, _bme.geo.point])
354            else:
355                return 0
356
357        if (
358            get_number_of_coupling_conditions(_bme.bc.point_coupling)
359            + get_number_of_coupling_conditions(_bme.bc.point_coupling_penalty)
360            > 0
361        ):
362            mesh.set_node_links()
363
364        # Add the boundary conditions.
365        for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items():
366            if len(bc_list) > 0:
367                section_name = (
368                    bc_key
369                    if isinstance(bc_key, str)
370                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)]
371                )
372                _dump_mesh_items(section_name, bc_list)
373
374        # Add additional element sections, e.g., for NURBS knot vectors.
375        for element in mesh.elements:
376            _dump_item_to_section(self, element)
377
378        # Add the geometry sets.
379        for geom_key, item in mesh_sets.items():
380            if len(item) > 0:
381                _dump_mesh_items(
382                    _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
383                        geom_key
384                    ],
385                    item,
386                )
387
388        # Add the nodes and elements.
389        _dump_mesh_items("NODE COORDS", mesh.nodes)
390        _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements)
391        # TODO: reset all links and counters set in this method.

Add a mesh to the input file.

Arguments:
  • mesh: The mesh to be added to the input file.