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-2026 BeamMe Authors
  4#
  5# Permission is hereby granted, free of charge, to any person obtaining a copy
  6# of this software and associated documentation files (the "Software"), to deal
  7# in the Software without restriction, including without limitation the rights
  8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9# copies of the Software, and to permit persons to whom the Software is
 10# furnished to do so, subject to the following conditions:
 11#
 12# The above copyright notice and this permission notice shall be included in
 13# all copies or substantial portions of the Software.
 14#
 15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21# THE SOFTWARE.
 22"""This module defines the classes that are used to create an input file for
 234C."""
 24
 25from __future__ import annotations as _annotations
 26
 27import os as _os
 28from datetime import datetime as _datetime
 29from pathlib import Path as _Path
 30from typing import Any as _Any
 31from typing import Callable as _Callable
 32from typing import List as _List
 33
 34from fourcipp.fourc_input import FourCInput as _FourCInput
 35from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names
 36from fourcipp.utils.not_set import NOT_SET as _NOT_SET
 37
 38from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER
 39from beamme.core.conf import bme as _bme
 40from beamme.core.function import Function as _Function
 41from beamme.core.material import Material as _Material
 42from beamme.core.mesh import Mesh as _Mesh
 43from beamme.core.node import Node as _Node
 44from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch
 45from beamme.four_c.input_file_dump_item import dump_item_to_list as _dump_item_to_list
 46from beamme.four_c.input_file_dump_item import (
 47    dump_item_to_section as _dump_item_to_section,
 48)
 49from beamme.four_c.input_file_mappings import (
 50    INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS,
 51)
 52from beamme.four_c.material import (
 53    get_all_contained_materials as _get_all_contained_materials,
 54)
 55from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
 56from beamme.utils.environment import get_application_path as _get_application_path
 57from beamme.utils.environment import get_git_data as _get_git_data
 58
 59if _cubitpy_is_available():
 60    import cubitpy as _cubitpy
 61
 62
 63class InputFile:
 64    """An item that represents a complete 4C input file."""
 65
 66    def __init__(self):
 67        """Initialize the input file."""
 68
 69        self.fourc_input = _FourCInput()
 70
 71        # Contents of NOX xml file.
 72        self.nox_xml_contents = ""
 73
 74        # Register converters to directly convert non-primitive types
 75        # to native Python types via the FourCIPP type converter.
 76        self.fourc_input.type_converter.register_numpy_types()
 77        self.fourc_input.type_converter.register_type(
 78            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
 79        )
 80
 81    def __contains__(self, key: str) -> bool:
 82        """Contains function.
 83
 84        Allows to use the `in` operator.
 85
 86        Args:
 87            key: Section name to check if it is set
 88
 89        Returns:
 90            True if section is set
 91        """
 92
 93        return key in self.fourc_input
 94
 95    def __setitem__(self, key: str, value: _Any) -> None:
 96        """Set section.
 97
 98        Args:
 99            key: Section name
100            value: Section entry
101        """
102
103        self.fourc_input[key] = value
104
105    def __getitem__(self, key: str) -> _Any:
106        """Get section of input file.
107
108        Allows to use the indexing operator.
109
110        Args:
111            key: Section name to get
112
113        Returns:
114            The section content
115        """
116
117        return self.fourc_input[key]
118
119    @classmethod
120    def from_4C_yaml(
121        cls, input_file_path: str | _Path, header_only: bool = False
122    ) -> InputFile:
123        """Load 4C yaml file.
124
125        Args:
126            input_file_path: Path to yaml file
127            header_only: Only extract header, i.e., all sections except the legacy ones
128
129        Returns:
130            Initialised object
131        """
132
133        obj = cls()
134        obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only)
135        return obj
136
137    @property
138    def sections(self) -> dict:
139        """All the set sections.
140
141        Returns:
142            dict: Set sections
143        """
144
145        return self.fourc_input.sections
146
147    def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any:
148        """Pop section of input file.
149
150        Args:
151            key: Section name to pop
152
153        Returns:
154            The section content
155        """
156
157        return self.fourc_input.pop(key, default_value)
158
159    def add(self, object_to_add, **kwargs):
160        """Add a mesh or a dictionary to the input file.
161
162        Args:
163            object: The object to be added. This can be a mesh or a dictionary.
164            **kwargs: Additional arguments to be passed to the add method.
165        """
166
167        if isinstance(object_to_add, _Mesh):
168            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
169
170        else:
171            self.fourc_input.combine_sections(object_to_add)
172
173    def dump(
174        self,
175        input_file_path: str | _Path,
176        *,
177        nox_xml_file: str | None = None,
178        add_header_default: bool = True,
179        add_header_information: bool = True,
180        add_footer_application_script: bool = True,
181        validate=True,
182        validate_sections_only: bool = False,
183        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
184        fourcipp_yaml_style: bool = True,
185    ):
186        """Write the input file to disk.
187
188        Args:
189            input_file_path:
190                Path to the input file that should be created.
191            nox_xml_file:
192                If this is a string, the NOX xml file will be created with this
193                name. If this is None, the NOX xml file will be created with the
194                name of the input file with the extension ".nox.xml".
195            add_header_default:
196                Prepend the default header comment to the input file.
197            add_header_information:
198                If the information header should be exported to the input file
199                Contains creation date, git details of BeamMe, CubitPy and
200                original application which created the input file if available.
201            add_footer_application_script:
202                Append the application script which creates the input files as a
203                comment at the end of the input file.
204            validate:
205                Validate if the created input file is compatible with 4C with FourCIPP.
206            validate_sections_only:
207                Validate each section independently. Required sections are no longer
208                required, but the sections must be valid.
209            sort_function:
210                A function which sorts the sections of the input file.
211            fourcipp_yaml_style:
212                If True, the input file is written in the fourcipp yaml style.
213        """
214
215        # Make sure the given input file is a Path instance.
216        input_file_path = _Path(input_file_path)
217
218        if self.nox_xml_contents:
219            if nox_xml_file is None:
220                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
221
222            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
223
224            # Write the xml file to the disc.
225            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
226                xml_file.write(self.nox_xml_contents)
227
228        # Add information header to the input file
229        if add_header_information:
230            self.add({"TITLE": self._get_header()})
231
232        self.fourc_input.dump(
233            input_file_path=input_file_path,
234            validate=validate,
235            validate_sections_only=validate_sections_only,
236            convert_to_native_types=False,  # conversion already happens during add()
237            sort_function=sort_function,
238            use_fourcipp_yaml_style=fourcipp_yaml_style,
239        )
240
241        if add_header_default or add_footer_application_script:
242            with open(input_file_path, "r") as input_file:
243                lines = input_file.readlines()
244
245                if add_header_default:
246                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
247
248                if add_footer_application_script:
249                    application_path = _get_application_path()
250                    if application_path is not None:
251                        lines += self._get_application_script(application_path)
252
253                with open(input_file_path, "w") as input_file:
254                    input_file.writelines(lines)
255
256    def add_mesh_to_input_file(self, mesh: _Mesh) -> None:
257        """Add a mesh to the input file.
258
259        Args:
260            mesh: The mesh to be added to the input file.
261        """
262
263        if _bme.check_overlapping_elements:
264            mesh.check_overlapping_elements()
265
266        # Compute geometry-set starting indices
267        start_indices_geometry_set = {
268            geometry_type: max(
269                (entry["d_id"] for entry in self.sections.get(section_name, [])),
270                default=0,
271            )
272            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
273                "geometry_sets_geometry_to_condition_name"
274            ].items()
275        }
276
277        # Determine global start indices
278        start_index_nodes = len(self.sections.get("NODE COORDS", []))
279
280        start_index_elements = sum(
281            len(self.sections.get(section, []))
282            for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS")
283        )
284
285        start_index_functions = max(
286            (
287                int(section.split("FUNCT")[-1])
288                for section in self.sections
289                if section.startswith("FUNCT")
290            ),
291            default=0,
292        )
293
294        start_index_materials = max(
295            (material["MAT"] for material in self.sections.get("MATERIALS", [])),
296            default=0,
297        )  # materials imported from YAML may have arbitrary numbering
298
299        # Add sets from couplings and boundary conditions to a temp container
300        mesh.unlink_nodes()
301        mesh_sets = mesh.get_unique_geometry_sets(
302            geometry_set_start_indices=start_indices_geometry_set
303        )
304
305        # Assign global indices
306        #   Nodes
307        if len(mesh.nodes) != len(set(mesh.nodes)):
308            raise ValueError("Nodes are not unique!")
309        for i, node in enumerate(mesh.nodes, start=start_index_nodes):
310            node.i_global = i
311
312        #   Elements
313        if len(mesh.elements) != len(set(mesh.elements)):
314            raise ValueError("Elements are not unique!")
315        i = start_index_elements
316        nurbs_count = 0
317
318        for element in mesh.elements:
319            element.i_global = i
320            if isinstance(element, _NURBSPatch):
321                element.i_nurbs_patch = nurbs_count
322                i += element.get_number_of_elements()
323                nurbs_count += 1
324                continue
325            i += 1
326
327        #   Materials: Get a list of all materials in the mesh,
328        #   including nested sub-materials.
329        all_materials = [
330            material
331            for mesh_material in mesh.materials
332            for material in _get_all_contained_materials(mesh_material)
333        ]
334        if len(all_materials) != len(set(all_materials)):
335            raise ValueError("Materials are not unique!")
336        for i, material in enumerate(all_materials, start=start_index_materials):
337            material.i_global = i
338
339        #   Functions
340        if len(mesh.functions) != len(set(mesh.functions)):
341            raise ValueError("Functions are not unique!")
342        for i, function in enumerate(mesh.functions, start=start_index_functions):
343            function.i_global = i
344
345        # Dump mesh to input file
346        def _dump(section_name: str, items: _List) -> None:
347            """Dump list of items to a section in the input file.
348
349            Args:
350                section_name: Name of the section
351                items: List of items to be dumped
352            """
353            if not items:  # do not write empty sections
354                return
355            dumped: list[_Any] = []
356            for item in items:
357                _dump_item_to_list(dumped, item)
358
359            # Go through FourCIPP to convert to native types
360            # TODO this can be simplified/removed by using an internal type converter
361            if section_name in self.sections:
362                existing = self.pop(section_name)
363                existing.extend(dumped)
364                dumped = existing
365
366            self.add({section_name: dumped})
367
368        #   Materials
369        _dump("MATERIALS", all_materials)
370
371        #   Functions
372        for function in mesh.functions:
373            self.add({f"FUNCT{function.i_global + 1}": function.data})
374
375        #   Couplings
376        #     If there are couplings in the mesh, set the link between the nodes
377        #     and elements, so the couplings can decide which DOFs they couple,
378        #     depending on the type of the connected beam element.
379        if any(
380            mesh.boundary_conditions.get((key, _bme.geo.point), [])
381            for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty)
382        ):
383            mesh.set_node_links()
384
385        #   Boundary conditions
386        for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items():
387            if bc_list:
388                section = (
389                    bc_key
390                    if isinstance(bc_key, str)
391                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][
392                        (bc_key, geometry_key)
393                    ]
394                )
395                _dump(section, bc_list)
396
397        #   Additional element sections (NURBS etc.)
398        for element in mesh.elements:
399            _dump_item_to_section(self, element)
400
401        #   Geometry sets
402        for geometry_key, items in mesh_sets.items():
403            _dump(
404                _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
405                    geometry_key
406                ],
407                items,
408            )
409
410        #   Nodes
411        _dump("NODE COORDS", mesh.nodes)
412        #   Elements
413        _dump("STRUCTURE ELEMENTS", mesh.elements)
414
415        # TODO: reset all links and counters set in this method.
416
417    def _get_header(self) -> dict:
418        """Return the information header for the current BeamMe run.
419
420        Returns:
421            A dictionary with the header information.
422        """
423
424        header: dict = {"BeamMe": {}}
425
426        header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
427            sep=" ", timespec="seconds"
428        )
429
430        # application which created the input file
431        application_path = _get_application_path()
432        if application_path is not None:
433            header["BeamMe"]["Application"] = {"path": str(application_path)}
434
435            application_git_sha, application_git_date = _get_git_data(
436                application_path.parent
437            )
438            if application_git_sha is not None and application_git_date is not None:
439                header["BeamMe"]["Application"].update(
440                    {
441                        "git_sha": application_git_sha,
442                        "git_date": application_git_date,
443                    }
444                )
445
446        # BeamMe information
447        beamme_git_sha, beamme_git_date = _get_git_data(
448            _Path(__file__).resolve().parent
449        )
450        if beamme_git_sha is not None and beamme_git_date is not None:
451            header["BeamMe"]["BeamMe"] = {
452                "git_SHA": beamme_git_sha,
453                "git_date": beamme_git_date,
454            }
455
456        # CubitPy information
457        if _cubitpy_is_available():
458            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
459                _os.path.dirname(_cubitpy.__file__)
460            )
461
462            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
463                header["BeamMe"]["CubitPy"] = {
464                    "git_SHA": cubitpy_git_sha,
465                    "git_date": cubitpy_git_date,
466                }
467
468        return header
469
470    def _get_application_script(self, application_path: _Path) -> list[str]:
471        """Get the script that created this input file.
472
473        Args:
474            application_path: Path to the script that created this input file.
475        Returns:
476            A list of strings with the script that created this input file.
477        """
478
479        application_script_lines = [
480            "# Application script which created this input file:\n"
481        ]
482
483        with open(application_path) as script_file:
484            application_script_lines.extend("# " + line for line in script_file)
485
486        return application_script_lines
class InputFile:
 64class InputFile:
 65    """An item that represents a complete 4C input file."""
 66
 67    def __init__(self):
 68        """Initialize the input file."""
 69
 70        self.fourc_input = _FourCInput()
 71
 72        # Contents of NOX xml file.
 73        self.nox_xml_contents = ""
 74
 75        # Register converters to directly convert non-primitive types
 76        # to native Python types via the FourCIPP type converter.
 77        self.fourc_input.type_converter.register_numpy_types()
 78        self.fourc_input.type_converter.register_type(
 79            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
 80        )
 81
 82    def __contains__(self, key: str) -> bool:
 83        """Contains function.
 84
 85        Allows to use the `in` operator.
 86
 87        Args:
 88            key: Section name to check if it is set
 89
 90        Returns:
 91            True if section is set
 92        """
 93
 94        return key in self.fourc_input
 95
 96    def __setitem__(self, key: str, value: _Any) -> None:
 97        """Set section.
 98
 99        Args:
100            key: Section name
101            value: Section entry
102        """
103
104        self.fourc_input[key] = value
105
106    def __getitem__(self, key: str) -> _Any:
107        """Get section of input file.
108
109        Allows to use the indexing operator.
110
111        Args:
112            key: Section name to get
113
114        Returns:
115            The section content
116        """
117
118        return self.fourc_input[key]
119
120    @classmethod
121    def from_4C_yaml(
122        cls, input_file_path: str | _Path, header_only: bool = False
123    ) -> InputFile:
124        """Load 4C yaml file.
125
126        Args:
127            input_file_path: Path to yaml file
128            header_only: Only extract header, i.e., all sections except the legacy ones
129
130        Returns:
131            Initialised object
132        """
133
134        obj = cls()
135        obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only)
136        return obj
137
138    @property
139    def sections(self) -> dict:
140        """All the set sections.
141
142        Returns:
143            dict: Set sections
144        """
145
146        return self.fourc_input.sections
147
148    def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any:
149        """Pop section of input file.
150
151        Args:
152            key: Section name to pop
153
154        Returns:
155            The section content
156        """
157
158        return self.fourc_input.pop(key, default_value)
159
160    def add(self, object_to_add, **kwargs):
161        """Add a mesh or a dictionary to the input file.
162
163        Args:
164            object: The object to be added. This can be a mesh or a dictionary.
165            **kwargs: Additional arguments to be passed to the add method.
166        """
167
168        if isinstance(object_to_add, _Mesh):
169            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
170
171        else:
172            self.fourc_input.combine_sections(object_to_add)
173
174    def dump(
175        self,
176        input_file_path: str | _Path,
177        *,
178        nox_xml_file: str | None = None,
179        add_header_default: bool = True,
180        add_header_information: bool = True,
181        add_footer_application_script: bool = True,
182        validate=True,
183        validate_sections_only: bool = False,
184        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
185        fourcipp_yaml_style: bool = True,
186    ):
187        """Write the input file to disk.
188
189        Args:
190            input_file_path:
191                Path to the input file that should be created.
192            nox_xml_file:
193                If this is a string, the NOX xml file will be created with this
194                name. If this is None, the NOX xml file will be created with the
195                name of the input file with the extension ".nox.xml".
196            add_header_default:
197                Prepend the default header comment to the input file.
198            add_header_information:
199                If the information header should be exported to the input file
200                Contains creation date, git details of BeamMe, CubitPy and
201                original application which created the input file if available.
202            add_footer_application_script:
203                Append the application script which creates the input files as a
204                comment at the end of the input file.
205            validate:
206                Validate if the created input file is compatible with 4C with FourCIPP.
207            validate_sections_only:
208                Validate each section independently. Required sections are no longer
209                required, but the sections must be valid.
210            sort_function:
211                A function which sorts the sections of the input file.
212            fourcipp_yaml_style:
213                If True, the input file is written in the fourcipp yaml style.
214        """
215
216        # Make sure the given input file is a Path instance.
217        input_file_path = _Path(input_file_path)
218
219        if self.nox_xml_contents:
220            if nox_xml_file is None:
221                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
222
223            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
224
225            # Write the xml file to the disc.
226            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
227                xml_file.write(self.nox_xml_contents)
228
229        # Add information header to the input file
230        if add_header_information:
231            self.add({"TITLE": self._get_header()})
232
233        self.fourc_input.dump(
234            input_file_path=input_file_path,
235            validate=validate,
236            validate_sections_only=validate_sections_only,
237            convert_to_native_types=False,  # conversion already happens during add()
238            sort_function=sort_function,
239            use_fourcipp_yaml_style=fourcipp_yaml_style,
240        )
241
242        if add_header_default or add_footer_application_script:
243            with open(input_file_path, "r") as input_file:
244                lines = input_file.readlines()
245
246                if add_header_default:
247                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
248
249                if add_footer_application_script:
250                    application_path = _get_application_path()
251                    if application_path is not None:
252                        lines += self._get_application_script(application_path)
253
254                with open(input_file_path, "w") as input_file:
255                    input_file.writelines(lines)
256
257    def add_mesh_to_input_file(self, mesh: _Mesh) -> None:
258        """Add a mesh to the input file.
259
260        Args:
261            mesh: The mesh to be added to the input file.
262        """
263
264        if _bme.check_overlapping_elements:
265            mesh.check_overlapping_elements()
266
267        # Compute geometry-set starting indices
268        start_indices_geometry_set = {
269            geometry_type: max(
270                (entry["d_id"] for entry in self.sections.get(section_name, [])),
271                default=0,
272            )
273            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
274                "geometry_sets_geometry_to_condition_name"
275            ].items()
276        }
277
278        # Determine global start indices
279        start_index_nodes = len(self.sections.get("NODE COORDS", []))
280
281        start_index_elements = sum(
282            len(self.sections.get(section, []))
283            for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS")
284        )
285
286        start_index_functions = max(
287            (
288                int(section.split("FUNCT")[-1])
289                for section in self.sections
290                if section.startswith("FUNCT")
291            ),
292            default=0,
293        )
294
295        start_index_materials = max(
296            (material["MAT"] for material in self.sections.get("MATERIALS", [])),
297            default=0,
298        )  # materials imported from YAML may have arbitrary numbering
299
300        # Add sets from couplings and boundary conditions to a temp container
301        mesh.unlink_nodes()
302        mesh_sets = mesh.get_unique_geometry_sets(
303            geometry_set_start_indices=start_indices_geometry_set
304        )
305
306        # Assign global indices
307        #   Nodes
308        if len(mesh.nodes) != len(set(mesh.nodes)):
309            raise ValueError("Nodes are not unique!")
310        for i, node in enumerate(mesh.nodes, start=start_index_nodes):
311            node.i_global = i
312
313        #   Elements
314        if len(mesh.elements) != len(set(mesh.elements)):
315            raise ValueError("Elements are not unique!")
316        i = start_index_elements
317        nurbs_count = 0
318
319        for element in mesh.elements:
320            element.i_global = i
321            if isinstance(element, _NURBSPatch):
322                element.i_nurbs_patch = nurbs_count
323                i += element.get_number_of_elements()
324                nurbs_count += 1
325                continue
326            i += 1
327
328        #   Materials: Get a list of all materials in the mesh,
329        #   including nested sub-materials.
330        all_materials = [
331            material
332            for mesh_material in mesh.materials
333            for material in _get_all_contained_materials(mesh_material)
334        ]
335        if len(all_materials) != len(set(all_materials)):
336            raise ValueError("Materials are not unique!")
337        for i, material in enumerate(all_materials, start=start_index_materials):
338            material.i_global = i
339
340        #   Functions
341        if len(mesh.functions) != len(set(mesh.functions)):
342            raise ValueError("Functions are not unique!")
343        for i, function in enumerate(mesh.functions, start=start_index_functions):
344            function.i_global = i
345
346        # Dump mesh to input file
347        def _dump(section_name: str, items: _List) -> None:
348            """Dump list of items to a section in the input file.
349
350            Args:
351                section_name: Name of the section
352                items: List of items to be dumped
353            """
354            if not items:  # do not write empty sections
355                return
356            dumped: list[_Any] = []
357            for item in items:
358                _dump_item_to_list(dumped, item)
359
360            # Go through FourCIPP to convert to native types
361            # TODO this can be simplified/removed by using an internal type converter
362            if section_name in self.sections:
363                existing = self.pop(section_name)
364                existing.extend(dumped)
365                dumped = existing
366
367            self.add({section_name: dumped})
368
369        #   Materials
370        _dump("MATERIALS", all_materials)
371
372        #   Functions
373        for function in mesh.functions:
374            self.add({f"FUNCT{function.i_global + 1}": function.data})
375
376        #   Couplings
377        #     If there are couplings in the mesh, set the link between the nodes
378        #     and elements, so the couplings can decide which DOFs they couple,
379        #     depending on the type of the connected beam element.
380        if any(
381            mesh.boundary_conditions.get((key, _bme.geo.point), [])
382            for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty)
383        ):
384            mesh.set_node_links()
385
386        #   Boundary conditions
387        for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items():
388            if bc_list:
389                section = (
390                    bc_key
391                    if isinstance(bc_key, str)
392                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][
393                        (bc_key, geometry_key)
394                    ]
395                )
396                _dump(section, bc_list)
397
398        #   Additional element sections (NURBS etc.)
399        for element in mesh.elements:
400            _dump_item_to_section(self, element)
401
402        #   Geometry sets
403        for geometry_key, items in mesh_sets.items():
404            _dump(
405                _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
406                    geometry_key
407                ],
408                items,
409            )
410
411        #   Nodes
412        _dump("NODE COORDS", mesh.nodes)
413        #   Elements
414        _dump("STRUCTURE ELEMENTS", mesh.elements)
415
416        # TODO: reset all links and counters set in this method.
417
418    def _get_header(self) -> dict:
419        """Return the information header for the current BeamMe run.
420
421        Returns:
422            A dictionary with the header information.
423        """
424
425        header: dict = {"BeamMe": {}}
426
427        header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
428            sep=" ", timespec="seconds"
429        )
430
431        # application which created the input file
432        application_path = _get_application_path()
433        if application_path is not None:
434            header["BeamMe"]["Application"] = {"path": str(application_path)}
435
436            application_git_sha, application_git_date = _get_git_data(
437                application_path.parent
438            )
439            if application_git_sha is not None and application_git_date is not None:
440                header["BeamMe"]["Application"].update(
441                    {
442                        "git_sha": application_git_sha,
443                        "git_date": application_git_date,
444                    }
445                )
446
447        # BeamMe information
448        beamme_git_sha, beamme_git_date = _get_git_data(
449            _Path(__file__).resolve().parent
450        )
451        if beamme_git_sha is not None and beamme_git_date is not None:
452            header["BeamMe"]["BeamMe"] = {
453                "git_SHA": beamme_git_sha,
454                "git_date": beamme_git_date,
455            }
456
457        # CubitPy information
458        if _cubitpy_is_available():
459            cubitpy_git_sha, cubitpy_git_date = _get_git_data(
460                _os.path.dirname(_cubitpy.__file__)
461            )
462
463            if cubitpy_git_sha is not None and cubitpy_git_date is not None:
464                header["BeamMe"]["CubitPy"] = {
465                    "git_SHA": cubitpy_git_sha,
466                    "git_date": cubitpy_git_date,
467                }
468
469        return header
470
471    def _get_application_script(self, application_path: _Path) -> list[str]:
472        """Get the script that created this input file.
473
474        Args:
475            application_path: Path to the script that created this input file.
476        Returns:
477            A list of strings with the script that created this input file.
478        """
479
480        application_script_lines = [
481            "# Application script which created this input file:\n"
482        ]
483
484        with open(application_path) as script_file:
485            application_script_lines.extend("# " + line for line in script_file)
486
487        return application_script_lines

An item that represents a complete 4C input file.

InputFile()
67    def __init__(self):
68        """Initialize the input file."""
69
70        self.fourc_input = _FourCInput()
71
72        # Contents of NOX xml file.
73        self.nox_xml_contents = ""
74
75        # Register converters to directly convert non-primitive types
76        # to native Python types via the FourCIPP type converter.
77        self.fourc_input.type_converter.register_numpy_types()
78        self.fourc_input.type_converter.register_type(
79            (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1
80        )

Initialize the input file.

fourc_input
nox_xml_contents
@classmethod
def from_4C_yaml( cls, input_file_path: str | pathlib._local.Path, header_only: bool = False) -> InputFile:
120    @classmethod
121    def from_4C_yaml(
122        cls, input_file_path: str | _Path, header_only: bool = False
123    ) -> InputFile:
124        """Load 4C yaml file.
125
126        Args:
127            input_file_path: Path to yaml file
128            header_only: Only extract header, i.e., all sections except the legacy ones
129
130        Returns:
131            Initialised object
132        """
133
134        obj = cls()
135        obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only)
136        return obj

Load 4C yaml file.

Arguments:
  • input_file_path: Path to yaml file
  • header_only: Only extract header, i.e., all sections except the legacy ones
Returns:

Initialised object

sections: dict
138    @property
139    def sections(self) -> dict:
140        """All the set sections.
141
142        Returns:
143            dict: Set sections
144        """
145
146        return self.fourc_input.sections

All the set sections.

Returns:

dict: Set sections

def pop(self, key: str, default_value: Any = NotSet(<class 'object'>)) -> Any:
148    def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any:
149        """Pop section of input file.
150
151        Args:
152            key: Section name to pop
153
154        Returns:
155            The section content
156        """
157
158        return self.fourc_input.pop(key, default_value)

Pop section of input file.

Arguments:
  • key: Section name to pop
Returns:

The section content

def add(self, object_to_add, **kwargs):
160    def add(self, object_to_add, **kwargs):
161        """Add a mesh or a dictionary to the input file.
162
163        Args:
164            object: The object to be added. This can be a mesh or a dictionary.
165            **kwargs: Additional arguments to be passed to the add method.
166        """
167
168        if isinstance(object_to_add, _Mesh):
169            self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
170
171        else:
172            self.fourc_input.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._local.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):
174    def dump(
175        self,
176        input_file_path: str | _Path,
177        *,
178        nox_xml_file: str | None = None,
179        add_header_default: bool = True,
180        add_header_information: bool = True,
181        add_footer_application_script: bool = True,
182        validate=True,
183        validate_sections_only: bool = False,
184        sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
185        fourcipp_yaml_style: bool = True,
186    ):
187        """Write the input file to disk.
188
189        Args:
190            input_file_path:
191                Path to the input file that should be created.
192            nox_xml_file:
193                If this is a string, the NOX xml file will be created with this
194                name. If this is None, the NOX xml file will be created with the
195                name of the input file with the extension ".nox.xml".
196            add_header_default:
197                Prepend the default header comment to the input file.
198            add_header_information:
199                If the information header should be exported to the input file
200                Contains creation date, git details of BeamMe, CubitPy and
201                original application which created the input file if available.
202            add_footer_application_script:
203                Append the application script which creates the input files as a
204                comment at the end of the input file.
205            validate:
206                Validate if the created input file is compatible with 4C with FourCIPP.
207            validate_sections_only:
208                Validate each section independently. Required sections are no longer
209                required, but the sections must be valid.
210            sort_function:
211                A function which sorts the sections of the input file.
212            fourcipp_yaml_style:
213                If True, the input file is written in the fourcipp yaml style.
214        """
215
216        # Make sure the given input file is a Path instance.
217        input_file_path = _Path(input_file_path)
218
219        if self.nox_xml_contents:
220            if nox_xml_file is None:
221                nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml"
222
223            self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
224
225            # Write the xml file to the disc.
226            with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
227                xml_file.write(self.nox_xml_contents)
228
229        # Add information header to the input file
230        if add_header_information:
231            self.add({"TITLE": self._get_header()})
232
233        self.fourc_input.dump(
234            input_file_path=input_file_path,
235            validate=validate,
236            validate_sections_only=validate_sections_only,
237            convert_to_native_types=False,  # conversion already happens during add()
238            sort_function=sort_function,
239            use_fourcipp_yaml_style=fourcipp_yaml_style,
240        )
241
242        if add_header_default or add_footer_application_script:
243            with open(input_file_path, "r") as input_file:
244                lines = input_file.readlines()
245
246                if add_header_default:
247                    lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
248
249                if add_footer_application_script:
250                    application_path = _get_application_path()
251                    if application_path is not None:
252                        lines += self._get_application_script(application_path)
253
254                with open(input_file_path, "w") as input_file:
255                    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) -> None:
257    def add_mesh_to_input_file(self, mesh: _Mesh) -> None:
258        """Add a mesh to the input file.
259
260        Args:
261            mesh: The mesh to be added to the input file.
262        """
263
264        if _bme.check_overlapping_elements:
265            mesh.check_overlapping_elements()
266
267        # Compute geometry-set starting indices
268        start_indices_geometry_set = {
269            geometry_type: max(
270                (entry["d_id"] for entry in self.sections.get(section_name, [])),
271                default=0,
272            )
273            for geometry_type, section_name in _INPUT_FILE_MAPPINGS[
274                "geometry_sets_geometry_to_condition_name"
275            ].items()
276        }
277
278        # Determine global start indices
279        start_index_nodes = len(self.sections.get("NODE COORDS", []))
280
281        start_index_elements = sum(
282            len(self.sections.get(section, []))
283            for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS")
284        )
285
286        start_index_functions = max(
287            (
288                int(section.split("FUNCT")[-1])
289                for section in self.sections
290                if section.startswith("FUNCT")
291            ),
292            default=0,
293        )
294
295        start_index_materials = max(
296            (material["MAT"] for material in self.sections.get("MATERIALS", [])),
297            default=0,
298        )  # materials imported from YAML may have arbitrary numbering
299
300        # Add sets from couplings and boundary conditions to a temp container
301        mesh.unlink_nodes()
302        mesh_sets = mesh.get_unique_geometry_sets(
303            geometry_set_start_indices=start_indices_geometry_set
304        )
305
306        # Assign global indices
307        #   Nodes
308        if len(mesh.nodes) != len(set(mesh.nodes)):
309            raise ValueError("Nodes are not unique!")
310        for i, node in enumerate(mesh.nodes, start=start_index_nodes):
311            node.i_global = i
312
313        #   Elements
314        if len(mesh.elements) != len(set(mesh.elements)):
315            raise ValueError("Elements are not unique!")
316        i = start_index_elements
317        nurbs_count = 0
318
319        for element in mesh.elements:
320            element.i_global = i
321            if isinstance(element, _NURBSPatch):
322                element.i_nurbs_patch = nurbs_count
323                i += element.get_number_of_elements()
324                nurbs_count += 1
325                continue
326            i += 1
327
328        #   Materials: Get a list of all materials in the mesh,
329        #   including nested sub-materials.
330        all_materials = [
331            material
332            for mesh_material in mesh.materials
333            for material in _get_all_contained_materials(mesh_material)
334        ]
335        if len(all_materials) != len(set(all_materials)):
336            raise ValueError("Materials are not unique!")
337        for i, material in enumerate(all_materials, start=start_index_materials):
338            material.i_global = i
339
340        #   Functions
341        if len(mesh.functions) != len(set(mesh.functions)):
342            raise ValueError("Functions are not unique!")
343        for i, function in enumerate(mesh.functions, start=start_index_functions):
344            function.i_global = i
345
346        # Dump mesh to input file
347        def _dump(section_name: str, items: _List) -> None:
348            """Dump list of items to a section in the input file.
349
350            Args:
351                section_name: Name of the section
352                items: List of items to be dumped
353            """
354            if not items:  # do not write empty sections
355                return
356            dumped: list[_Any] = []
357            for item in items:
358                _dump_item_to_list(dumped, item)
359
360            # Go through FourCIPP to convert to native types
361            # TODO this can be simplified/removed by using an internal type converter
362            if section_name in self.sections:
363                existing = self.pop(section_name)
364                existing.extend(dumped)
365                dumped = existing
366
367            self.add({section_name: dumped})
368
369        #   Materials
370        _dump("MATERIALS", all_materials)
371
372        #   Functions
373        for function in mesh.functions:
374            self.add({f"FUNCT{function.i_global + 1}": function.data})
375
376        #   Couplings
377        #     If there are couplings in the mesh, set the link between the nodes
378        #     and elements, so the couplings can decide which DOFs they couple,
379        #     depending on the type of the connected beam element.
380        if any(
381            mesh.boundary_conditions.get((key, _bme.geo.point), [])
382            for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty)
383        ):
384            mesh.set_node_links()
385
386        #   Boundary conditions
387        for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items():
388            if bc_list:
389                section = (
390                    bc_key
391                    if isinstance(bc_key, str)
392                    else _INPUT_FILE_MAPPINGS["boundary_conditions"][
393                        (bc_key, geometry_key)
394                    ]
395                )
396                _dump(section, bc_list)
397
398        #   Additional element sections (NURBS etc.)
399        for element in mesh.elements:
400            _dump_item_to_section(self, element)
401
402        #   Geometry sets
403        for geometry_key, items in mesh_sets.items():
404            _dump(
405                _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][
406                    geometry_key
407                ],
408                items,
409            )
410
411        #   Nodes
412        _dump("NODE COORDS", mesh.nodes)
413        #   Elements
414        _dump("STRUCTURE ELEMENTS", mesh.elements)
415
416        # 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.