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
 26from datetime import datetime as _datetime
 27from pathlib import Path as _Path
 28from typing import Callable as _Callable
 29from typing import Dict as _Dict
 30from typing import List as _List
 31
 32from fourcipp.fourc_input import FourCInput as _FourCInput
 33from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names
 34
 35from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER
 36from beamme.core.conf import bme as _bme
 37from beamme.core.function import Function as _Function
 38from beamme.core.material import Material as _Material
 39from beamme.core.mesh import Mesh as _Mesh
 40from beamme.core.node import Node as _Node
 41from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch
 42from beamme.four_c.input_file_dump_item import dump_item_to_list as _dump_item_to_list
 43from beamme.four_c.input_file_dump_item import (
 44    dump_item_to_section as _dump_item_to_section,
 45)
 46from beamme.four_c.input_file_mappings import (
 47    INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS,
 48)
 49from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
 50from beamme.utils.environment import get_application_path as _get_application_path
 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 = _get_application_path()
190                    if application_path is not None:
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_of_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 = _get_application_path()
408        if application_path is not None:
409            header["BeamMe"]["Application"] = {"path": str(application_path)}
410
411            application_git_sha, application_git_date = _get_git_data(
412                application_path.parent
413            )
414            if application_git_sha is not None and application_git_date is not None:
415                header["BeamMe"]["Application"].update(
416                    {
417                        "git_sha": application_git_sha,
418                        "git_date": application_git_date,
419                    }
420                )
421
422        # BeamMe information
423        beamme_git_sha, beamme_git_date = _get_git_data(
424            _Path(__file__).resolve().parent
425        )
426        if beamme_git_sha is not None and beamme_git_date is not None:
427            header["BeamMe"]["BeamMe"] = {
428                "git_SHA": beamme_git_sha,
429                "git_date": beamme_git_date,
430            }
431
432        # CubitPy information
433        if _cubitpy_is_available():
434            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
435                _os.path.dirname(_cubitpy.__file__)
436            )
437
438            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
439                header["BeamMe"]["CubitPy"] = {
440                    "git_SHA": cubitpy_git_sha,
441                    "git_date": cubitpy_git_date,
442                }
443
444        return header
445
446    def _get_application_script(self, application_path: _Path) -> list[str]:
447        """Get the script that created this input file.
448
449        Args:
450            application_path: Path to the script that created this input file.
451        Returns:
452            A list of strings with the script that created this input file.
453        """
454
455        application_script_lines = [
456            "# Application script which created this input file:\n"
457        ]
458
459        with open(application_path) as script_file:
460            application_script_lines.extend("# " + line for line in script_file)
461
462        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 = _get_application_path()
191                    if application_path is not None:
192                        lines += self._get_application_script(application_path)
193
194                with open(input_file_path, "w") as input_file:
195                    input_file.writelines(lines)
196
197    def add_mesh_to_input_file(self, mesh: _Mesh):
198        """Add a mesh to the input file.
199
200        Args:
201            mesh: The mesh to be added to the input file.
202        """
203
204        # Perform some checks on the mesh.
205        if _bme.check_overlapping_elements:
206            mesh.check_overlapping_elements()
207
208        def _get_global_start_geometry_set(dictionary):
209            """Get the indices for the first "real" BeamMe geometry sets."""
210
211            start_indices_geometry_set = {}
212            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
213                "geometry_sets_geometry_to_condition_name"
214            ].items():
215                max_geometry_set_id = 0
216                if section_name in dictionary:
217                    section_list = dictionary[section_name]
218                    if len(section_list) > 0:
219                        geometry_set_dict = get_geometry_set_indices_from_section(
220                            section_list, append_node_ids=False
221                        )
222                        max_geometry_set_id = max(geometry_set_dict.keys())
223                start_indices_geometry_set[geometry_type] = max_geometry_set_id
224            return start_indices_geometry_set
225
226        def _get_global_start_node():
227            """Get the index for the first "real" BeamMe node."""
228
229            return len(self.sections.get("NODE COORDS", []))
230
231        def _get_global_start_element():
232            """Get the index for the first "real" BeamMe element."""
233
234            return sum(
235                len(self.sections.get(section, []))
236                for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"]
237            )
238
239        def _get_global_start_material():
240            """Get the index for the first "real" BeamMe material.
241
242            We have to account for materials imported from yaml files
243            that have arbitrary numbering.
244            """
245
246            # Get the maximum material index in materials imported from a yaml file
247            max_material_id = 0
248            section_name = "MATERIALS"
249            if section_name in self.sections:
250                for material in self.sections[section_name]:
251                    max_material_id = max(max_material_id, material["MAT"])
252            return max_material_id
253
254        def _get_global_start_function():
255            """Get the index for the first "real" BeamMe function."""
256
257            max_function_id = 0
258            for section_name in self.sections.keys():
259                if section_name.startswith("FUNCT"):
260                    max_function_id = max(
261                        max_function_id, int(section_name.split("FUNCT")[-1])
262                    )
263            return max_function_id
264
265        def _set_i_global(data_list, *, start_index=0):
266            """Set i_global in every item of data_list."""
267
268            # A check is performed that every entry in data_list is unique.
269            if len(data_list) != len(set(data_list)):
270                raise ValueError("Elements in data_list are not unique!")
271
272            # Set the values for i_global.
273            for i, item in enumerate(data_list):
274                item.i_global = i + start_index
275
276        def _set_i_global_elements(element_list, *, start_index=0):
277            """Set i_global in every item of element_list."""
278
279            # A check is performed that every entry in element_list is unique.
280            if len(element_list) != len(set(element_list)):
281                raise ValueError("Elements in element_list are not unique!")
282
283            # Set the values for i_global.
284            i = start_index
285            i_nurbs_patch = 0
286            for item in element_list:
287                # As a NURBS patch can be defined with more elements, an offset is applied to the
288                # rest of the items
289                item.i_global = i
290                if isinstance(item, _NURBSPatch):
291                    item.i_nurbs_patch = i_nurbs_patch
292                    offset = item.get_number_of_elements()
293                    i += offset
294                    i_nurbs_patch += 1
295                else:
296                    i += 1
297
298        def _dump_mesh_items(section_name, data_list):
299            """Output a section name and apply either the default dump or the
300            specialized the dump_to_list for each list item."""
301
302            # Do not write section if no content is available
303            if len(data_list) == 0:
304                return
305
306            list = []
307
308            for item in data_list:
309                _dump_item_to_list(list, item)
310
311            # If section already exists, retrieve from input file and
312            # add newly. We always need to go through fourcipp to convert
313            # the data types correctly.
314            if section_name in self.sections:
315                existing_entries = self.pop(section_name)
316                existing_entries.extend(list)
317                list = existing_entries
318
319            self.add({section_name: list})
320
321        # Add sets from couplings and boundary conditions to a temp container.
322        mesh.unlink_nodes()
323        start_indices_geometry_set = _get_global_start_geometry_set(self.sections)
324        mesh_sets = mesh.get_unique_geometry_sets(
325            geometry_set_start_indices=start_indices_geometry_set
326        )
327
328        # Assign global indices to all entries.
329        start_index_nodes = _get_global_start_node()
330        _set_i_global(mesh.nodes, start_index=start_index_nodes)
331
332        start_index_elements = _get_global_start_element()
333        _set_i_global_elements(mesh.elements, start_index=start_index_elements)
334
335        start_index_materials = _get_global_start_material()
336        _set_i_global(mesh.materials, start_index=start_index_materials)
337
338        start_index_functions = _get_global_start_function()
339        _set_i_global(mesh.functions, start_index=start_index_functions)
340
341        # Add material data to the input file.
342        _dump_mesh_items("MATERIALS", mesh.materials)
343
344        # Add the functions.
345        for function in mesh.functions:
346            self.add({f"FUNCT{function.i_global + 1}": function.data})
347
348        # If there are couplings in the mesh, set the link between the nodes
349        # and elements, so the couplings can decide which DOFs they couple,
350        # depending on the type of the connected beam element.
351        def get_number_of_coupling_conditions(key):
352            """Return the number of coupling conditions in the mesh."""
353            if (key, _bme.geo.point) in mesh.boundary_conditions.keys():
354                return len(mesh.boundary_conditions[key, _bme.geo.point])
355            else:
356                return 0
357
358        if (
359            get_number_of_coupling_conditions(_bme.bc.point_coupling)
360            + get_number_of_coupling_conditions(_bme.bc.point_coupling_penalty)
361            > 0
362        ):
363            mesh.set_node_links()
364
365        # Add the boundary conditions.
366        for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items():
367            if len(bc_list) > 0:
368                section_name = (
369                    bc_key
370                    if isinstance(bc_key, str)
371                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)]
372                )
373                _dump_mesh_items(section_name, bc_list)
374
375        # Add additional element sections, e.g., for NURBS knot vectors.
376        for element in mesh.elements:
377            _dump_item_to_section(self, element)
378
379        # Add the geometry sets.
380        for geom_key, item in mesh_sets.items():
381            if len(item) > 0:
382                _dump_mesh_items(
383                    _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
384                        geom_key
385                    ],
386                    item,
387                )
388
389        # Add the nodes and elements.
390        _dump_mesh_items("NODE COORDS", mesh.nodes)
391        _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements)
392        # TODO: reset all links and counters set in this method.
393
394    def _get_header(self) -> dict:
395        """Return the information header for the current BeamMe run.
396
397        Returns:
398            A dictionary with the header information.
399        """
400
401        header: dict = {"BeamMe": {}}
402
403        header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
404            sep=" ", timespec="seconds"
405        )
406
407        # application which created the input file
408        application_path = _get_application_path()
409        if application_path is not None:
410            header["BeamMe"]["Application"] = {"path": str(application_path)}
411
412            application_git_sha, application_git_date = _get_git_data(
413                application_path.parent
414            )
415            if application_git_sha is not None and application_git_date is not None:
416                header["BeamMe"]["Application"].update(
417                    {
418                        "git_sha": application_git_sha,
419                        "git_date": application_git_date,
420                    }
421                )
422
423        # BeamMe information
424        beamme_git_sha, beamme_git_date = _get_git_data(
425            _Path(__file__).resolve().parent
426        )
427        if beamme_git_sha is not None and beamme_git_date is not None:
428            header["BeamMe"]["BeamMe"] = {
429                "git_SHA": beamme_git_sha,
430                "git_date": beamme_git_date,
431            }
432
433        # CubitPy information
434        if _cubitpy_is_available():
435            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
436                _os.path.dirname(_cubitpy.__file__)
437            )
438
439            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
440                header["BeamMe"]["CubitPy"] = {
441                    "git_SHA": cubitpy_git_sha,
442                    "git_date": cubitpy_git_date,
443                }
444
445        return header
446
447    def _get_application_script(self, application_path: _Path) -> list[str]:
448        """Get the script that created this input file.
449
450        Args:
451            application_path: Path to the script that created this input file.
452        Returns:
453            A list of strings with the script that created this input file.
454        """
455
456        application_script_lines = [
457            "# Application script which created this input file:\n"
458        ]
459
460        with open(application_path) as script_file:
461            application_script_lines.extend("# " + line for line in script_file)
462
463        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 = _get_application_path()
191                    if application_path is not None:
192                        lines += self._get_application_script(application_path)
193
194                with open(input_file_path, "w") as input_file:
195                    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):
197    def add_mesh_to_input_file(self, mesh: _Mesh):
198        """Add a mesh to the input file.
199
200        Args:
201            mesh: The mesh to be added to the input file.
202        """
203
204        # Perform some checks on the mesh.
205        if _bme.check_overlapping_elements:
206            mesh.check_overlapping_elements()
207
208        def _get_global_start_geometry_set(dictionary):
209            """Get the indices for the first "real" BeamMe geometry sets."""
210
211            start_indices_geometry_set = {}
212            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
213                "geometry_sets_geometry_to_condition_name"
214            ].items():
215                max_geometry_set_id = 0
216                if section_name in dictionary:
217                    section_list = dictionary[section_name]
218                    if len(section_list) > 0:
219                        geometry_set_dict = get_geometry_set_indices_from_section(
220                            section_list, append_node_ids=False
221                        )
222                        max_geometry_set_id = max(geometry_set_dict.keys())
223                start_indices_geometry_set[geometry_type] = max_geometry_set_id
224            return start_indices_geometry_set
225
226        def _get_global_start_node():
227            """Get the index for the first "real" BeamMe node."""
228
229            return len(self.sections.get("NODE COORDS", []))
230
231        def _get_global_start_element():
232            """Get the index for the first "real" BeamMe element."""
233
234            return sum(
235                len(self.sections.get(section, []))
236                for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"]
237            )
238
239        def _get_global_start_material():
240            """Get the index for the first "real" BeamMe material.
241
242            We have to account for materials imported from yaml files
243            that have arbitrary numbering.
244            """
245
246            # Get the maximum material index in materials imported from a yaml file
247            max_material_id = 0
248            section_name = "MATERIALS"
249            if section_name in self.sections:
250                for material in self.sections[section_name]:
251                    max_material_id = max(max_material_id, material["MAT"])
252            return max_material_id
253
254        def _get_global_start_function():
255            """Get the index for the first "real" BeamMe function."""
256
257            max_function_id = 0
258            for section_name in self.sections.keys():
259                if section_name.startswith("FUNCT"):
260                    max_function_id = max(
261                        max_function_id, int(section_name.split("FUNCT")[-1])
262                    )
263            return max_function_id
264
265        def _set_i_global(data_list, *, start_index=0):
266            """Set i_global in every item of data_list."""
267
268            # A check is performed that every entry in data_list is unique.
269            if len(data_list) != len(set(data_list)):
270                raise ValueError("Elements in data_list are not unique!")
271
272            # Set the values for i_global.
273            for i, item in enumerate(data_list):
274                item.i_global = i + start_index
275
276        def _set_i_global_elements(element_list, *, start_index=0):
277            """Set i_global in every item of element_list."""
278
279            # A check is performed that every entry in element_list is unique.
280            if len(element_list) != len(set(element_list)):
281                raise ValueError("Elements in element_list are not unique!")
282
283            # Set the values for i_global.
284            i = start_index
285            i_nurbs_patch = 0
286            for item in element_list:
287                # As a NURBS patch can be defined with more elements, an offset is applied to the
288                # rest of the items
289                item.i_global = i
290                if isinstance(item, _NURBSPatch):
291                    item.i_nurbs_patch = i_nurbs_patch
292                    offset = item.get_number_of_elements()
293                    i += offset
294                    i_nurbs_patch += 1
295                else:
296                    i += 1
297
298        def _dump_mesh_items(section_name, data_list):
299            """Output a section name and apply either the default dump or the
300            specialized the dump_to_list for each list item."""
301
302            # Do not write section if no content is available
303            if len(data_list) == 0:
304                return
305
306            list = []
307
308            for item in data_list:
309                _dump_item_to_list(list, item)
310
311            # If section already exists, retrieve from input file and
312            # add newly. We always need to go through fourcipp to convert
313            # the data types correctly.
314            if section_name in self.sections:
315                existing_entries = self.pop(section_name)
316                existing_entries.extend(list)
317                list = existing_entries
318
319            self.add({section_name: list})
320
321        # Add sets from couplings and boundary conditions to a temp container.
322        mesh.unlink_nodes()
323        start_indices_geometry_set = _get_global_start_geometry_set(self.sections)
324        mesh_sets = mesh.get_unique_geometry_sets(
325            geometry_set_start_indices=start_indices_geometry_set
326        )
327
328        # Assign global indices to all entries.
329        start_index_nodes = _get_global_start_node()
330        _set_i_global(mesh.nodes, start_index=start_index_nodes)
331
332        start_index_elements = _get_global_start_element()
333        _set_i_global_elements(mesh.elements, start_index=start_index_elements)
334
335        start_index_materials = _get_global_start_material()
336        _set_i_global(mesh.materials, start_index=start_index_materials)
337
338        start_index_functions = _get_global_start_function()
339        _set_i_global(mesh.functions, start_index=start_index_functions)
340
341        # Add material data to the input file.
342        _dump_mesh_items("MATERIALS", mesh.materials)
343
344        # Add the functions.
345        for function in mesh.functions:
346            self.add({f"FUNCT{function.i_global + 1}": function.data})
347
348        # If there are couplings in the mesh, set the link between the nodes
349        # and elements, so the couplings can decide which DOFs they couple,
350        # depending on the type of the connected beam element.
351        def get_number_of_coupling_conditions(key):
352            """Return the number of coupling conditions in the mesh."""
353            if (key, _bme.geo.point) in mesh.boundary_conditions.keys():
354                return len(mesh.boundary_conditions[key, _bme.geo.point])
355            else:
356                return 0
357
358        if (
359            get_number_of_coupling_conditions(_bme.bc.point_coupling)
360            + get_number_of_coupling_conditions(_bme.bc.point_coupling_penalty)
361            > 0
362        ):
363            mesh.set_node_links()
364
365        # Add the boundary conditions.
366        for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items():
367            if len(bc_list) > 0:
368                section_name = (
369                    bc_key
370                    if isinstance(bc_key, str)
371                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)]
372                )
373                _dump_mesh_items(section_name, bc_list)
374
375        # Add additional element sections, e.g., for NURBS knot vectors.
376        for element in mesh.elements:
377            _dump_item_to_section(self, element)
378
379        # Add the geometry sets.
380        for geom_key, item in mesh_sets.items():
381            if len(item) > 0:
382                _dump_mesh_items(
383                    _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
384                        geom_key
385                    ],
386                    item,
387                )
388
389        # Add the nodes and elements.
390        _dump_mesh_items("NODE COORDS", mesh.nodes)
391        _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements)
392        # 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.