Coverage for src / beamme / four_c / input_file.py: 87%
157 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 06:24 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 06:24 +0000
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."""
25from __future__ import annotations as _annotations
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
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
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
59if _cubitpy_is_available():
60 import cubitpy as _cubitpy
63class InputFile:
64 """An item that represents a complete 4C input file."""
66 def __init__(self):
67 """Initialize the input file."""
69 self.fourc_input = _FourCInput()
71 # Contents of NOX xml file.
72 self.nox_xml_contents = ""
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 )
81 def __contains__(self, key: str) -> bool:
82 """Contains function.
84 Allows to use the `in` operator.
86 Args:
87 key: Section name to check if it is set
89 Returns:
90 True if section is set
91 """
93 return key in self.fourc_input
95 def __setitem__(self, key: str, value: _Any) -> None:
96 """Set section.
98 Args:
99 key: Section name
100 value: Section entry
101 """
103 self.fourc_input[key] = value
105 def __getitem__(self, key: str) -> _Any:
106 """Get section of input file.
108 Allows to use the indexing operator.
110 Args:
111 key: Section name to get
113 Returns:
114 The section content
115 """
117 return self.fourc_input[key]
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.
125 Args:
126 input_file_path: Path to yaml file
127 header_only: Only extract header, i.e., all sections except the legacy ones
129 Returns:
130 Initialised object
131 """
133 obj = cls()
134 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only)
135 return obj
137 @property
138 def sections(self) -> dict:
139 """All the set sections.
141 Returns:
142 dict: Set sections
143 """
145 return self.fourc_input.sections
147 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any:
148 """Pop section of input file.
150 Args:
151 key: Section name to pop
153 Returns:
154 The section content
155 """
157 return self.fourc_input.pop(key, default_value)
159 def add(self, object_to_add, **kwargs):
160 """Add a mesh or a dictionary to the input file.
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 """
167 if isinstance(object_to_add, _Mesh):
168 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs)
170 else:
171 self.fourc_input.combine_sections(object_to_add)
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.
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 """
215 # Make sure the given input file is a Path instance.
216 input_file_path = _Path(input_file_path)
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"
222 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
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)
228 # Add information header to the input file
229 if add_header_information:
230 self.add({"TITLE": self._get_header()})
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 )
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()
245 if add_header_default:
246 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
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)
253 with open(input_file_path, "w") as input_file:
254 input_file.writelines(lines)
256 def add_mesh_to_input_file(self, mesh: _Mesh) -> None:
257 """Add a mesh to the input file.
259 Args:
260 mesh: The mesh to be added to the input file.
261 """
263 if _bme.check_overlapping_elements:
264 mesh.check_overlapping_elements()
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 }
277 # Determine global start indices
278 start_index_nodes = len(self.sections.get("NODE COORDS", []))
280 start_index_elements = sum(
281 len(self.sections.get(section, []))
282 for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS")
283 )
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 )
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
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 )
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
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
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
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
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
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.
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)
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
366 self.add({section_name: dumped})
368 # Materials
369 _dump("MATERIALS", all_materials)
371 # Functions
372 for function in mesh.functions:
373 self.add({f"FUNCT{function.i_global + 1}": function.data})
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()
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)
397 # Additional element sections (NURBS etc.)
398 for element in mesh.elements:
399 _dump_item_to_section(self, element)
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 )
410 # Nodes
411 _dump("NODE COORDS", mesh.nodes)
412 # Elements
413 _dump("STRUCTURE ELEMENTS", mesh.elements)
415 # TODO: reset all links and counters set in this method.
417 def _get_header(self) -> dict:
418 """Return the information header for the current BeamMe run.
420 Returns:
421 A dictionary with the header information.
422 """
424 header: dict = {"BeamMe": {}}
426 header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
427 sep=" ", timespec="seconds"
428 )
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)}
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 )
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 }
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 )
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 }
468 return header
470 def _get_application_script(self, application_path: _Path) -> list[str]:
471 """Get the script that created this input file.
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 """
479 application_script_lines = [
480 "# Application script which created this input file:\n"
481 ]
483 with open(application_path) as script_file:
484 application_script_lines.extend("# " + line for line in script_file)
486 return application_script_lines